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.

  • willjsxCompiled intonodePrimordial knowledgejscode
  • willexModuleCode compiled intocommonjsthe

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 sidehtmlDocument at the same timehtmlAdd a<style>Tag, which writes a style string internally and is passed to the client
  • Output on the server sidehtmlDocument at the same timehtmlAdd a<link>Tag, of this taghrefPoints 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