I recently read about React SSR
This article example code has been uploaded to the lot, interested can see Basic | SplitChunkV
I met the React SSR
Nodejs follows the CommonJS specification, and files are imported and exported as follows:
/ / export
module.exports = someModule
/ / import
const module = require('./someModule')
Copy the code
React code usually follows the esModule specification. The files are imported and exported as follows:
/ / export
export default someModule
/ / import
import module from './someModule'
Copy the code
In order to make react code compatible with the server, you must first resolve the compatibility issues between the two specifications. In fact, react can be written directly in the CommonJS specification, for example:
const React = require('react')
Copy the code
React renders code that JSX and Node do not recognize and must be compiled once
render () {
// Node is not aware of JSX
return <div>home</div>
}
Copy the code
Use webPack to compile react code. Use Webpack to compile react code.
- will
jsx
Compiled intonode
Primordial knowledgejs
code - will
exModule
Code compiled intocommonjs
the
The sample Webpack configuration file is as follows:
// webpack.server.js
module.exports = {
// omit code...
module: {
rules: [{test: /\.js$/.loader: 'babel-loader'.exclude: /node_modules/.options: {
// React needs support
// Stage-0 needs to be converted
presets: ['react'.'stage-0'['env', {
targets: {
browsers: ['last 2 versions']}}]]}}Copy the code
Once you have this configuration file, you can have fun writing code
The first is a copy of the react code that needs to be exported to the client:
import React from 'react'
export default() = > {return <div>home</div>
}
Copy the code
The code is as simple as a plain React Stateless component
Then there is the server-side code responsible for exporting this component to the client:
// index.js
import http from 'http'
import React from 'react'
import { renderToString } from 'react-dom/server'
import Home from './containers/Home/index.js'
constcontainer = renderToString(<Home />) http.createServer((request, response) => { response.writeHead(200, {'Content-Type': 'text/html'}) response.end(` <! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, <meta HTTP-equiv =" x-UA-compatible "content=" IE =edge"> <title>Document</title> </head> <body> <div Id = "root" > ${container} < / div > < / body > < / HTML > `)}), listen (8888) the console. The log (' Server running at http://127.0.0.1:8888/)Copy the code
This code starts a Node HTTP server and responds to an HTML page with a react-related library
The React code renders the page in real time by calling the browser API. That is, the page is assembled by JS manipulating the browser DOM API. The server cannot call the browser API, so this process cannot be implemented. That’s where renderToString comes in
RenderToString is an API provided by React to convert React code into HTML strings that the browser can recognize directly. This API does what the browser needs to do in the first place. DOM strings are put together directly on the server and node outputs them to the browser
The container variable in the above code is the following HTML string:
<div data-reactroot="">home</div>
Copy the code
Therefore, node responds to the browser as a normal HTML string, which can be displayed directly by the browser. Because the browser does not need to download the React code, the code is smaller, and does not need to concatenate DOM strings in real time, it simply renders the page, so the server rendering speed is relatively fast
RenderToNodeStream renderToNodeStream supports direct rendering to node streams. In addition to renderToString, React V16.x provides a more powerful API. Rendering to stream reduces the time to the first byte (TTFB) of your content, sending the beginning to the end of the document to the browser before the next part of the document is generated. When the content is streamed from the server, the browser will start parsing the HTML document, and some articles claim that the API renders three times faster than renderToString (I haven’t tested the exact times, but it’s true that rendering is usually faster).
So, if you’re using React V16.x, you can also write:
Import HTTP from 'HTTP' import React from 'React' renderToNodeStream import {renderToNodeStream} from 'react-dom/server' import Home from './containers/Home/index.js' http.createServer((request, response) => { response.writeHead(200, {'Content-Type': 'text/html'}) response.write(` <! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, <meta HTTP-equiv =" x-UA-compatible "content=" IE =edge"> <title>Document</title> </head> <body> <div Id ="root"> ') const container = renderToNodeStream(<Home />) Container. Pipe (response, {end: false }) container.on('end', Response.end (' </div> </body> </ HTML > ')})}).listen(8888) console.log('Server running at http://127.0.0.1:8888/ ')Copy the code
BOM/DOM related logic isomorphism
With renderToString/renderToNodeStream, it might seem that server-side rendering is within reach, but it’s not nearly so, for the following react code:
const Home = (a)= > {
return <button onClick={()= > { alert(123) }}>home</button>
}
Copy the code
The expectation is that when the button is clicked, the browser will pop up with a message 123, but if you just follow the procedure above, this event will not be triggered because renderToString only parses basic HTML DOM elements, not events attached to them. That is, the onClick event is ignored
OnClick is an event. In the code we normally write (i.e., not SSR), React registers the event with an addEventListener on the element. That is, js triggers the event and calls the corresponding method. Some browser-specific operations cannot be done on the server side
But these does not affect the SSR, SSR is one of the goals in order to make the browser faster rendering a page, user interactions of enforceability don’t have to follow the page DOM at the same time to complete, so, we can execute code are relevant to this part of the browser packaged into a js file sent to the browser, the browser end after rendering a page, Then load and execute this section of JS, the entire page is naturally executable
To simplify things, let’s introduce Koa on the server side
Since the browser side also needs to run the Home component, we need to prepare a separate Home package file for the browser side to use:
// client
import React from 'react'
import ReactDOM from 'react-dom'
import Home from '.. /containers/Home'
ReactDOM.render(<Home />, document.getElementById('root'))
Copy the code
This is the usual browser-side React code that wraps the Home component again and renders it to the page node
Also, if you’re using React v16.x, the last line of the code above suggests:
// omit code...
ReactDOM.hydrate(<Home />, document.getElementById('root'))
Copy the code
The main difference between Reactdom.render and Reactdom.hydrate is that the latter has a lower performance overhead (only for server-side rendering), as you can see in more detail
This code needs to be packaged into a js code and delivered to the browser, so we also need to configure webpack for similar client-side isomorphic code:
// webpack.client.js
const path = require('path')
module.exports = {
// Import file
entry: './src/client/index.js'.// Indicates whether the development environment or production environment code
mode: 'development'.// Output information
output: {
// Output the file name
filename: 'index.js'.// Output file path
path: path.resolve(__dirname, 'public')},// ...
}
Copy the code
This configuration file is similar to the server-side configuration file webpack.server.js, except that some server-side configurations are removed
This configuration file declares to package the Home component in the public directory named index.js, so we just need to load this file in the HTML page output from the server:
// server
// omit irrelevant code...
app.use(ctx= > {
ctx.response.type = 'html'
ctx.body = ` <! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, <meta HTTP-equiv =" x-UA-compatible "content=" IE =edge"> <title>Document</title> </head> <body> <div id="root">${container}</div> <! - introduction of isomorphism code - > < script SRC = "/ index. Js" > < / script > < / body > < / HTML > `
})
app.listen(3000)
Copy the code
For Home this component, it has been running on the server side, mainly through renderToString/renderToNodeStream generates pure HTML element, is running on the client again once, mainly will be properly registered, events such as the combination, This is also called isomorphism, when the server and the client run the same set of code
Router Isomorphism
After solving the isomorphism of jS-related codes such as events, we also need to carry out isomorphism of routes
Generally, react code uses the React router to manage routes. In the same code, HashRouter/BrowserRouter is used as the common method. Here, BrowserRouter is used as an example
Definition of a route:
import React, { Fragment } from 'React'
import { Route } from 'react-router-dom'
import Home from './containers/Home'
import Login from './containers/Login'
export default (
<Fragment>
<Route path='/' exact component={Home}></Route>
<Route path='/login' exact component={Login}></Route>
</Fragment>
)
Copy the code
Browser-side code introduced:
import React from 'react'
import ReactDOM from 'react-dom'
// Using BrowserRouter as an example, HashRouter can also be used
import { BrowserRouter } from 'react-router-dom'
// Import the defined route
import Routes from '.. /Routes'
const App = (a)= > {
return (
<BrowserRouter>
{Routes}
</BrowserRouter>
)
}
ReactDOM.hydrate(<App />, document.getElementById('root'))
Copy the code
Route import mainly on the server side:
/ / use StaticRouter
import { StaticRouter } from 'react-router-dom'
import Routes from '.. /Routes'
// ...
app.use(ctx= > {
const container = renderToNodeStream(
<StaticRouter location={ctx.request.path} context={{}}>
{Routes}
</StaticRouter>
)
// ...
})
Copy the code
The react-router 4. X provides StaticRouter for the server to control the route. This API passively obtains the route of the current request through the passed location parameter to match and navigate the route. See StaticRouter for more details
Isomorphism of State
When the project is large, we usually use Redux to manage the data state of the project. In order to ensure the consistency of the state on the server side and the state on the client side, isomorphism of the state is also needed
The server-side code is intended for all users, and the data state of all users must be opened independently, otherwise all users will share the same state
// This is acceptable on the client side, but on the server side it causes all users to share the same state
// export default createStore(reducer, applyMiddleware(thunk))
export default () => createStore(reducer, applyMiddleware(thunk))
Copy the code
Note that the above code exports a function, not a store object. To get a store, simply execute this function:
import getStore from '.. /store' // ... <Provider store={getStore()}> <StaticRouter location={ctx.request.path} context={context}> {Routes} </StaticRouter> </Provider>Copy the code
This ensures that the server generates a new store every time it receives a request, which means that each request gets a separate, new state
Just above can solve the problem of state independence, but the SSR state synchronization is the key point of asynchronous data synchronization, for example, the common data interface call, this is an asynchronous operation, if you like using redux in client asynchronously to that on the server side to do the same, so although project will not an error, the page can also be normal rendering, In fact, this asynchronously retrieved data is missing from the server-side rendered page
This is quite understandable, although the server side can also carry out the data interface request operation, but because the interface request is asynchronous, and the page rendering is synchronous, it is likely that when the server responds to the output page, the data of the asynchronous request has not been returned, so the rendered page will naturally be missing data
Since the data state is lost because of the asynchronous retrieval problem, the problem is solved by ensuring that you get the correct data for the page before the server responds to it
There are actually two problems:
- You need to know which page is being requested, because different pages generally require different data, different interfaces and different logic for data processing
- You need to make sure that the server gets the data from the interface before responding to the page, that is, the processed state (
store
)
For the first problem, react-Router has actually provided a solution in SSR, that is, by configuring route /route-config combined with matchPath, find the method of requesting interface required by relevant components on the page and execute it:
In addition, matchPath provided by React-Router can only identify first-level routes. For multi-level routes, it can only identify the top level and ignore sub-level routes. Therefore, if the project does not have multi-level routes or all data acquisition and state processing are completed in top-level routes, It is fine to use matchPath, otherwise you may lose page data under sub-routing
React-router also provides a solution to this problem, whereby developers use matchRoutes provided in react-router-config instead of matchPath
The second problem, which is much easier, is the synchronization of asynchronous operations that are common in JS code. The most common promises or async/await can solve this problem
const store = getStore()
const promises = []
// Matched route
const mtRoutes = matchRoutes(routes, ctx.request.path)
mtRoutes.forEach(item= > {
if (item.route.loadData) {
promises.push(item.route.loadData(store))
}
})
// Here the server requests the data interface, gets the data needed for the current page, and populates it into the Store for rendering the page
await Promise.all(promises)
// Server-side output page
await render(ctx, store, routes)
Copy the code
However, after solving this problem, another problem arises
As mentioned above, the PROCESS of SSR should ensure that the data state of the server side and the client side is consistent. According to the above process, the server side will eventually output a complete page with data state, but the code logic of the client side is to render a page shelf without data state first. Then I’m going to make a data interface request in a hook function like componentDidMount to get the data, do state processing, and finally get a page that matches the output on the server
So before the client code gets the data, the data state on the client side is actually empty, while the data state on the server side is intact, so the inconsistent data state on both ends will cause problems
The process of solving this problem is actually dehydration and water injection of data
On the server side, when the server requests the interface for data and handles the data status (such as store updates), it retains that status and sends it to the browser when the server responds to the page HTML, a process called Dehydrate. On the browser side, the React component is initialized with this dehydrate data, so the client does not need to request the React processing status, because the server already does this. This process is called Hydrate.
The server sends state to the browser, along with HTML, via global variables:
ctx.body = ` <! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, <meta httpquiv=" x-UA-compatible "content=" IE =edge"> <title>Document</title> </head> <body> <div id="root">${data.toString()}</div> <! <script> window. Context = {state:The ${JSON.stringify(store.getState())}} </script> <! - introduction of isomorphism code - > < script SRC = "/ index. Js" > < / script > < / body > < / HTML > `
Copy the code
After receiving the page from the server, the browser can directly obtain the state from the window object, and then use this state to update the state of the browser itself.
export const getClientStore = (a)= > {
// Get the dehydration data from the server output page
const defaultState = window.context.state
// As store initial data (i.e. water injection)
return createStore(reducer, defaultState, applyMiddleware(thunk))
}
Copy the code
The introduction of the style
The introduction of styles is simpler and can be considered from two perspectives:
- Output on the server side
html
Document at the same timehtml
Add a<style>
Tag, which writes a style string internally and is passed to the client - Output on the server side
html
Document at the same timehtml
Add a<link>
Tag, of this taghref
Points to a style file that is the page’s style file
These two operations are similar in general and are similar to the process of introducing styles in client rendering. The main method is to extract styles from the React component using webpack and loader plug-in. Cs-loader, style-loader, extract-text-webpack-plugin/mini-CSS-extract-plugin. Sass-loader or less-loader may also be required. These complications are not considered here and only the most basic CSS will be introduced
Inline style
For the first type of inline style, directly embed the style in the page, need to use CSS –loader and style loader, CSS-loader can continue to use, but style loader because there are some browser-related logic, so can not continue to use on the server side. However, isomorphic-style-loader has long been a substitute plug-in, which is similar to style-loader, but supports the use of server side
Isomorphic-style-loader will convert the imported CSS file into an object for the component to use. Part of the properties are the class name, and the value of the properties is the CSS style corresponding to the class. Therefore, styles can be directly introduced into the component according to these properties. SSR needs to call the _getCss method to get the style string and pass it to the client
Since the process described above (that is, summarizing and converting CSS styles to strings) is a generic process, a HOC component withStyles.js is proactively provided within the plug-in project to simplify the process
What this component does is also very simple, mainly for the two methods in isomorphic-style-loader: __insertCss and _getCss provide an interface that uses Context as a medium to pass styles referenced by individual components, which are finally summarized on the server and client sides so that styles can be output on both sides
Server:
import StyleContext from 'isomorphic-style-loader/StyleContext'
// ...
const css = new Set(a)const insertCss = (. styles) = > styles.forEach(style= > css.add(style._getCss()))
const container = renderToNodeStream(
<Provider store={store}>
<StaticRouter location={ctx.request.path} context={context}>
<StyleContext.Provider value={{ insertCss}} >
{renderRoutes(routes)}
</StyleContext.Provider>
</StaticRouter>
</Provider>
)
Copy the code
Client:
import StyleContext from 'isomorphic-style-loader/StyleContext'
// ...
const insertCss = (. styles) = > {
const removeCss = styles.map(style= > style._insertCss())
return (a)= > removeCss.forEach(dispose= > dispose())
}
const App = (a)= > {
return (
<Provider store={store}>
<BrowserRouter>
<StyleContext.Provider value={{ insertCss}} >
{renderRoutes(routes)}
</StyleContext.Provider>
</BrowserRouter>
</Provider>)}Copy the code
Isomorphic-style-loader’s README. Md is used to specify the Context ([email protected] is the previous version of the Context API). 5.0.1 and later, the new Context API) and the use of HOC, a higher-level component
Outreach style
In general, most of the production environment is the use of external style, using the tag on the page to introduce the style file, which is in fact and the above external js approach is the same processing logic, compared with the introduction of CSS inline more simple to understand, the server and client processing process is basically the same
Mini-css-extract-plugin is a common webpack plug-in that extracts component styles. Because this plug-in essentially extracts style strings from components and integrates them into a style file, it is only the operation of JS Core, so there is no server-side and browser-side term. There is no need for isomorphism, how to use this plug-in in pure client before, now how to use in SSR, here is not to say
The code segment
An important purpose of SSR is to speed up the rendering of the first screen, so the optimization measures of the original client rendering should also be used in SSR, one of the key points is code segmentation
There are a lot of code breakups in React, such as babel-plugin-syntax-dynamic-import, react-loadable, loadable components, etc
I used to use the react-loadable library, but I encountered some problems when using it. When I wanted to check issues, I found that this project had closed issues, so I abandoned it and used the more modern loadable components library. Also take into account the SSR situation, and support to render pages in the form of renderToNodeStream, just follow the document to do ok, very easy to get started, there is no more to say, see SplitChunkV
conclusion
SSR configuration is still quite troublesome, not only for the front-end configuration, but also for things related to back-end programs, such as logon state, high concurrency, load balancing, memory management, etc. Winter once said that she was not optimistic about SSR, which is mainly used for SEO and not recommended for server rendering. It can be used in a few scenarios, and the cost is too high
Therefore, for practical development, I would prefer to use relatively mature wheels in the industry, such as React’S Next-js and Vue’s nuxt.js