1.1 Understanding isomorphism
1.1.1 History and development of anterior and posterior separation
Front and back end no separation (JSP MVC)-> Front and back end separation (AJAX)-> SPA(front-end routing)-> SSR(front-end and back end rendering isomers)
1.1.2 Appearance of isomorphic rendering
Issues and Background
- SEO problems
- The first screen to hang
- Nodejs
- mvvm ssr
Isomorphism CSR + SSR
- Isomorphism: The same js code runs in different environments
- CSR: Client – Side Rendering
- SSR: Server – Side Rendering
- The Node mid-tier renders dynamic pages with data
advantages
- The first screen fast
The server Intranet interface data renders the page without waiting for the COMPLETION of JS execution
- SEO
The first screen is rich, convenient crawler
- Retain SPA benefits
Only the first screen is rendered by the server, and then the front-end route is still taken, without refreshing the switching content
disadvantages
- High threshold
Need to understand server rendering and be compatible with server and client differences
- Difficult to transform
Old SPA projects are difficult to transform into server-side isomorphic rendering
- Occupying server resources
Dynamic pages are generated on the server side
Is isomorphism the only solution?
You can also try pre-render techniques that suit every user and will return the same content
2.2 Realization principle of isomorphism
2.2.1 Client rendering
Simple page client rendering
impot React from 'react'
import ReactDoM from 'react-dom'
ReactDoM.render(
<h1>Hello world<h1>,
document.getElementById('root'))Copy the code
SPA client rendering
Load HTML->js-> request data ->render
The process of loading JS to Render is white screen time
2.2.2 Server rendering
HTML->js->render
The server cannot access the DOM, so it returns the created string to the browser. The advantage of server-side rendering is that users can see the content faster; Since server-side rendering is performance intensive, you can’t do this for every page, so let’s look at isomorphic rendering.
2.2.3 PRINCIPLE of SSR isomorphic rendering
Server rendering + SPA = server-side rendering
For the first time, the user will send the request to the Node server. After receiving the request, the Node service will request the data and render the first screen. After rendering, it will return to the browser and the user will see the content of the first screen. The page loads JS to bind events to the DOM and takes over routing and other operations. This becomes known as SPA. At this time, we have eliminated the white screen time of SPA, and can switch the page without refreshing on the client. In this process, it benefits from the server-side rendering capability provided by MVVM framework of virtual DOM. On the server side, the virtual DOM converts strings, and on the client side, the real DOM converts.
advantages
- SEO: The first screen HTML content is rich
- White screen time: No white screen time, the page content is directly visible
- None Refresh route: Inherits the advantages of SAP
- Isomorphism: a set of code that runs on both ends
SSR isomorphism difficulty
- Server-side development: Node develops capabilities and understands server-side rendering techniques provided by the framework
- Performance and monitoring: server rendering performance, server exception monitoring and handling
- Routing isomorphism: How does the same routine be performed by compatible Node environments and browser environments
- Requests and cookies: How can the server cache the request user’s identity and forward the cookie
- Status data sharing: How can a server store be shared with a client
- Build and deploy: Build js on both sides, deploy Node services, and deploy JS clients
2.3 the React isomorphism
Overview of both rendering methods
// client
import ReactDOM from 'react-dom'
// server
import ReactDOMServer from 'react-dom/server'
Copy the code
ReactDOM provides client-side rendering methods to render components as real DOM
ReactDOMServer provides server-side rendering methods that render components as static tags
2.3.1 React server rendering method
Basic API
// Pass both parameters to the component. Mandatory string
ReactDOMServer.renderToStaticMarkup(element);
ReactDOMServer.renderToString(element)
Copy the code
- RenderToStaticMarkup (for purely static pages)
import ReactDOMServer from 'react-dom/server'
const App= () = >(<h1>Hello</h1>)
const str = ReactDOMServer.renderToStaticMarkup(<App/>)
console.log(str)
// <h1>Hello<h1>
Copy the code
- Render React elements as HTML strings
- Additional DOM properties that are not created inside React, such as data-reactroot
- RenderToString (for interactive pages)
import ReactDOMServer from 'react-dom/server'
const App= () = >(<h1>Hello</h1>)
const str = ReactDOMServer.renderToString(<App/>)
console.log(str)
// <h1 data-reactroot>Hello<h1>
Copy the code
- Render React elements as HTML strings
- And an additional DOM property created inside React, data-Reactroot
- What it does: it tells the client that reusing the page improves performance. The data-reactroot property tells the client that the server has already rendered it, so the client can reuse the component directly and bind only the event.
2.3.2 React client rendering method
Basic API
// Two render methods
import ReactDOM from 'react-dom'
/ / 1
ReactDOM.render(
element,
container[,callback]
)
// Callback: executed after the component has been rendered or updated. React >15
/ / 2
ReactDOM.hydrate(
element,
container[,callback]
)
// Perform the HTML content hydrate operation in the container rendered by ReactDOMServer.
React tries to bind an event listener to an existing tag
Copy the code
- ReactDOM.render
import ReactDOM from 'react-dom'
const App= () = >(<h1>Hello</h1>)
const root = doucment.getElementById('root')
ReactDOM.render(<App/>,root)
Copy the code
- ReactDOM.hydrate
ReactDOM. Hydrate is used only for the first render, the HTML binding event for the server rendering.
import ReactDOM from 'react-dom'
const App= () = >(<h1>Hello</h1>)
const root = doucment.getElementById('root')
ReactDOM.render(<App/>,root)
Copy the code
React renders both ends differently
suppressHydrationWarning
The following example is the server rendering time, while the client rendering time has passed some time, so how to solve this problem?
The suppressHydrationWarning attribute can be used to resolve differences between the two sides of a single element. Text differences can be resolved, but attribute differences are not guaranteed to be resolved.
/ / component
const App=() = >{
<h1 suppressHydrationWarning>
{new Date().getTime()}
</h1>
}
// Server rendering
ReactDOMServer.renderToString(<App/>)
// First client render
const root = document.getElementById('root')
ReactDOM.hydrate(<App/>,root)
Copy the code
On both ends of the rendering
When there are large text differences, use the following method: componentDidMount the hook will only be executed when the client renders; Only constructor is executed on the server side; So you can use the componentDidMount hook to render differential content.
class App extends React.PureComponent{
constructor(props){
super(props)
this.state={mounted:false}}componentDidMount(){
this.setState({mounted:true})}return (
<div>
hello:
{mounted && <Todo>}
</div>)}Copy the code
Summary: React isomorphic rendering process:
- The server uses ReactDOM.
renderToString
Render the HTML string - The client uses ReactDOM for the first time.
hydrate
Bind events for it - The next time you update the DOM, use ReactDOM.
render
2.1 Implement a simple isomorphic rendering page
2.1.1 Starting the Node Server Using the Express Service
Source address: / examples/react/simpleDemo
const express = require('express')
const app = express()
app.get('/'.(req,res) = >{
res.send('hello world')
})
app.listen(3001)
Copy the code
Start the service: nodemon. /server.js
2.1.2 Use the React component and API rendering on the server
1. Create the document.js file
import React from 'react'
const Document = () = > {
return (
<html>
<head></head>
<body>
<h1>hello ssr</h1>
</body>
</html>)}export default Document
Copy the code
2. server.js
const express = require('express')
const ReactDOMserver=require('react-dom/server')
const Document = require('./documnet')
const app = express()
RenderToStaticMarkup applies to purely static pages
const html = ReactDOMserver.renderToStaticMarkup(<Document/>)
app.get('/'.(req,res) = >{
res.send(html)
})
app.listen(3001)
Copy the code
The following error is reported when running the server.js file because JSX syntax is not supported
const html = ReactDOMserver.renderToStaticMarkup(<Document/>)
^
SyntaxError: Unexpected token '<'at wrapSafe (internal/modules/cjs/loader.js:915:16) at Module._compile (internal/modules/cjs/loader.js:963:27) at Object.Module._extensions.. js (internal/modules/cjs/loader.js:1027:10) at Module.load (internal/modules/cjs/loader.js:863:32) at Function.Module._load (internal/modules/cjs/loader.js:708:14) at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12) at internal/main/run_main_module.js:17:47Copy the code
Resolve the Node JSX error
- Install Babel yarn add @babel/ core-@babel/register@babel /preset- env.babel /preset- react-d
- Babel valid range. The current Babel file is invalid
- Split the Expres router into separate files and execute the React server rendering API inside the router
3. New serverRouter. Js
const express = require('express')
import React from 'react'
import ReactDOMserver from 'react-dom/server'
import Document from './documnet'
const router = express.Router()
const html = ReactDOMserver.renderToStaticMarkup(<Document/>)
router.get('/'.(req,res) = >{
res.send(html)
})
module.exports=router
Copy the code
4. Rewrite server. Js
require('@babel/register') ({presets: ['@babel/preset-env'.'@babel/preset-react']})const express = require('express')
const app = express()
const serverRouter = require('./serverRouter')
app.use('/',serverRouter)
app.listen(3001)
Copy the code
Start the service and open http://localhost:3001/ to see the React render hello SSR
Although the server returns a string and displays the contents, there are no interaction events, i.e., no js is loaded
:::warning Why cannot the event be bound on the server?
- The server does not have the DOM and cannot bind events
- The server returns a string
- The server has no script
- The browser only loads the HTML and does not load any script to load the execution JS
: : :
2.1.3 Isomorphic rendering of interactive events
Source address: / examples/react/simpleDemo
- The new app. Js
import React from 'react';
const App = () = > {
return (<div onClick={()= > alert('hello')}>
client
</div> );
}
export default App;
Copy the code
- A new client. Js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './components/app'
// shenhu is not going to render the DOM again
ReactDOM.hydrate(<App />.document.getElementById('root'))
Copy the code
- We built our client-side rendering component in WebPack, packaged as main.js
// Download webpack, webpack-cli
const path = require('path')
const CopyPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin =require('html-webpack-plugin')
module.exports = {
entry: './src/client.js'.output: {
// Put the packaged main.js under the build file
path: path.resolve(__dirname, 'build'),
filename: 'main.js'
},
module: {
rules: [{test: /\.(js|jsx)$/,
use: 'babel-loader'.exclude: /node_modules/,}]}};Copy the code
Now that we have finished rendering on the client, what should we do on the server
- document.js
import React from 'react'
const Document = ({ children }) = > (
<html lang="en">
<head>
<meta charSet="UTF-8" />
<meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
<title>simple-ssr</title>
</head>
<body>DangerouslySetInnerHTML is used to insert strings into the DOM, similar to vue's V-HTML<div id="root" dangerouslySetInnerHTML={{ __html: children}} / >
</body>// Load the client packaged main.js<script src="./main.js"></script>
</html>
)
export default Document
Copy the code
- serverRouter.js
const express = require('express')
import React from 'react'
import ReactDOMServer from 'react-dom/server'
import Document from './components/Document'
import App from './components/App'
const router = express.Router();
// Render app.js. The server is responsible for rendering and the client is responsible for binding events
RenderToString is mainly used for pages that require interaction. RenderToStaticMarkup is mainly used for simply displaying pages */
const appString = ReactDOMServer.renderToString(<App/>)
const html = ReactDOMServer.renderToStaticMarkup(<Document>
{appString}
</Document>)
router.get("/".function (req, res, next) {
res.status(200).send(html);
});
module.exports = router
Copy the code
Nodemon. / SRC /server.js starts the service, you can see the page with SSR rendering, and click event
2.2 SPA isomorphic rendering
Source address: / examples/react/simpleDemo2
- React-router Is a basic client-side routing implementation
- Understand stateless components
- React-router is used to realize server routing
2.2.1 Client routing
React-router-dom: React-router-dom can be used on both the client and server
yarn add react-router-dom
Copy the code
App.js
import React from 'react'
import { Route, Switch, NavLink } from 'react-router-dom';
import routes from '.. /core/routes.js'
const App = () = > {
return (
<div>
<ul>
<li>
<NavLink to="/">to Home</NavLink>
</li>
<li>
<NavLink to="/user">to User</NavLink>
</li>
</ul>
<Switch>
{routes.map(route => (
<Route key={route.path} exact={route.path= = ='/'} {. route} / >
))}
</Switch>
</div>)}export default App
Copy the code
routes.js
import Home from '.. /components/Home'
import User from '.. /components/User'
import NotFound from '.. /components/NotFound'
const routes = [
{
path: "/".component: Home,
},
{
path: "/user".component: User,
},
{
path: "".component: NotFound,
},
];
export default routes
Copy the code
2.2.2 Server Routing
StaticRouter
- Stateless component
- What is stateless: it never changes location, there is no user click on the server to switch routes, and already rendered routing components do not change
- location: string | object
- context: object
<StaticRouter
location={req.url}
context={context}
>
<App/>
</StaticRouter>
Copy the code
serverRouter.js
const express = require('express')
import React from 'react'
import ReactDOMServer from 'react-dom/server'
import Document from './components/documnet'
import App from './components/app'
import { StaticRouter } from 'react-router-dom'
const router = express.Router()
router.get("*".function (req, res, next) {
// The server renders the first time the page is loaded or refreshed, and then the client takes over the route to jump to the rendered page
const appString = ReactDOMServer.renderToString(
<StaticRouter
location={req.url}
>
<App />
</StaticRouter>)
const html = ReactDOMServer.renderToStaticMarkup(<Document>
{appString}
</Document>)
console.log('html', html)
res.status(200).send(html);
});
module.exports = router
Copy the code
So far, we use React-router-DOM to implement server routing and client routing
2.3 When to Request Asynchronous Data
Source address: / examples/react/simpleDemo3
2.3.1 Timing and implementation of client requests
Recommended: Send requests in componentDidmount and useEffect
Is not recommended: componentWillmount, componentWillReceiveProps, componentWillUpdate
Why don’t you request data at componentWillmount?
- After executing the componentWillmount, the render method will be executed immediately, at this time the interface data has not returned, the advance request does not reduce the call to the Render method
- ComponentWillmount, componentWillReceiveProps, componentWillUpdate expiration warning, in the new version of the react will remove these life cycle;
The Fiber architecture will be adopted in the new release: The Fiber architecture will result in multiple executions of these lifecycles.
Sync: Rendering all components at once
Asynchrony: Shard multiple renderings, high priority tasks can interrupt the render (when a task such as click or scroll responds to the user as a high priority task, the browser is free to continue rendering again, so the last 3 life cycles will be executed multiple times)
2.3.2 Timing and implementation of server requests
The server will not execute componentDidmount, useEffect, so the server needs to get the data before rendering the component
Axios sends the request (both server and client)
yarn add axios
Copy the code
- New apiRouter. Js
Simulate some interfaces and return some data
const express = require('express')
const router = express.Router();
router.get("/home".function (req, res, next) {
res.json({ title: 'Home'.desc: 'Here's the home page'})}); router.get("/user".function (req, res, next) {
res.json({ name: 'Joe'.age: '21'.id: '1'})});module.exports = router
Copy the code
- Rewrite server. Js
require('@babel/register')({ presets:['@babel/preset-env','@babel/preset-react'] }) const express = require('express') const app = express() const serverRouter = require('./server/serverRouter') const apiRouter = Require ('./server/apiRouter') // API interface+ app.use("/api/", apiRouter);App.use ("/build", express.static('build')); Use ('/',serverRouter) app.Listen (3003)Copy the code
- api.js
Encapsulation of request data
import axios from 'axios'
const req = axios.create({
baseURL:'http://localhost:3003/api'}); req.interceptors.response.use(function (response) {
return response.data;
});
// Request the first page
export const fetchHome = () = > req.get('/home')
// Request user information
export const fetchUser = () = > req.get('/user')
Copy the code
- The user component
import React,{useEffect} from 'react';
import { fetchUser } from '.. /core/api'
const User = ({staticContext}) = > {
// staticContext is used for server rendering. StaticContext is the value returned by the request interface. See serverRouter.js for details
console.log('staticContext',staticContext)
// When the client requests, useEffect will not be executed while the server is rendering
useEffect(() = >{
fetchUser().then(data= >console.log('User data:',data))
},[])
return (
<main>
<h1>User</h1>
<button onClick={()= >{alert('user! ')}}>click me</button>
</main>
)
}
User.getData = fetchUser
export default User
Copy the code
- serverRouter.js
const express = require('express')
import React from 'react'
import ReactDOMServer from 'react-dom/server'
import Document from '.. /components/documnet'
import App from '.. /components/app'
import { StaticRouter,matchPath } from 'react-router-dom'
import routes from '.. /core/routes'
const router = express.Router()
router.get("*".async function (req, res, next) {
let data = {}
let getData = null
// Match the current route and get the static getData property of the component to be rendered; GetData is the interface function for the request
routes.some(route= > {
const match = matchPath(req.path, route);
if (match) {
getData = (route.component || {}).getData
}
return match
});
if (typeof getData === 'function') {
try {
data = await getData()
} catch (error) { }
}
const appString = ReactDOMServer.renderToString(
<StaticRouter
location={req.url}
// contextThe value passed in the componentstaticContextYou can get the corresponding valuecontext={data}
>
<App />
</StaticRouter>)
const html = ReactDOMServer.renderToStaticMarkup(<Document data={data}>
{appString}
</Document>)
res.status(200).send(html);
});
module.exports = router
Copy the code
Conclusion:
- Source address: / examples/react/simpleDemo – 3
- Server-side rendering requests data before rendering the component and then exploits it
context
Pass the value to the corresponding component, thus rendering the component with data. - Client rendering is available in
ComponentDidmount, useEffect
Request data for client rendering.
2.4 Client Overuses server data
Source address: / examples/react/simpleDemo – 4
How does the server pass data to the client
- Through the window global variable
Use the window global variable to pass data
- Rewrite serverRouter. Js
const html = ReactDOMServer.renderToStaticMarkup(<Document data={data}>
{appString}
</Document>)
Copy the code
- Rewrite the doucment. Js
We can convert the passed data into a JSON string and assign it to window.__app_data; Put it in the script tag, and the following code will be executed on the client.
import React from 'react' const Document = ({ children ,data}) => { return ( <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, Word-wrap: break-word! Important; word-wrap: break-word! Important; word-wrap: break-word! Important; "> <div id=" dangerouslySetInnerHTML={{__html: children }}></div>+
+ dangerouslySetInnerHTML={{
+ __html: `window.__APP_DATA__=${JSON.stringify(data)}`,
+}}
+ />
<script src="/build/main.js"></script>
</body>
</html>
)
}
export default Document
Copy the code
- Rewrite home. Js
import React, { useState } from 'react';
import {fetchHome} from '.. /core/api'
const Home = ({staticContext}) = > {
console.log('staticContext',staticContext)
const getInitialData = () = > {
// The server renders the obtained data
if (staticContext) {
return staticContext
}
// The client renders and gets the data from the server
if (window.__APP_DATA__) {
return window.__APP_DATA__
}
return{}}const [data, setData] = useState(getInitialData())
return (
<main>
<div>{data.title}</div>
<div>{data.desc}</div>
</main>
)
}
Home.getData = fetchHome
export default Home
Copy the code
The client obtains the route jump data
Is there something wrong with the way home.js is written?
-
The home. Js client renders data from window.__app_data__. If home jumps to user, where does user.js data get from? Cannot be obtained from window.__app_data__, user.js requires different data.
-
Window. __APP_DATA__ can only be used to get data from the first screen.
- New useData. Js
Usedata.js is an enclosed hooks used to process data
import { useState, useEffect } from 'react'
const useData = (staticContext, initial, getData) = > {
// Initialize the data
const getInitialData = () = > {
// server render
if (staticContext) {
return staticContext
}
// client first render
if (window.__APP_DATA__) {
return window.__APP_DATA__
}
return initial
}
const [data, setData] = useState(getInitialData())
useEffect(() = > {
// After the first execution of the client, clear window.__APP_DATA__; The next route will request the data
if (window.__APP_DATA__) {
window.__APP_DATA__ = undefined
return
}
if (typeof getData === 'function') {
console.log('spa render')
getData().then(res= > setData(res)).catch()
}
}, [])
return [data, setData]
}
export default useData
Copy the code
- Rewrite home. Js
import React, { useState } from 'react';
import {fetchHome} from '.. /core/api'
import useData from '.. /core/useData'
const Home = ({staticContext}) = > {
const [data, setData] = useData(staticContext, { title: ' '.desc: ' '}, fetchHome)
return (
<main>
<h1>{data.title}</h1>
<p>{data.desc}</p>
</main>
)
}
Home.getData = fetchHome
export default Home
Copy the code
package.json
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"."build": "rm -rf build && webpack --config ./webpack.config.js"."start": "npm run build && nodemon ./src/server.js"
},
Copy the code
The project address