preface

React application isomorphism can achieve better user experience and SEO, but the process is complicated and the code is difficult to be decoued. As for whether the project needs isomorphism, I think it depends on the corresponding scene, input and output, and this paper will not elaborate on it

There are many expressions about Isomorphic on the Internet. In my opinion, Isomorphic is a code. The server performs rendering, splices HTML and initialization data, and the browser inserts the initialization data and mount related monitoring events in the browser after receiving THE HTML and JS

For isomorphic, some people think it should be Universal Rendering, and this paper will use the word “isomorphism”.

This article will record my React app — a personal blog isomorphism practice, SSR effect, as well as the problems encountered in the process and solutions. This link is the blog of Zhou Lihan, a personal blog that went online after isomorphism

After referring to a large number of articles, the React ecosystem is indeed prosperous and there are many solutions. The solution adopted in this practice is isothermal, and two sets of Webpack are written at the same time, namely one set of React code and two sets of Webpack configuration (respectively for browser side and server side). The server side uses Express to render. Entrap browser side JS code back to the front end, essentially a set of code because it is the first rewrite isomorphism, there are suggestions also please point out that this article will progress in the following order:

  • Comparison of isomorphic effects
  • React isomorphism principle
  • Problems encountered by isomorphic processes
  • Steps or problems in practice
    • Server WebPack configuration
    • Server /index.js structure written
    • RenderToString on the server side, hydrate on the browser side
    • Customize the Ant Design handling of the theme
    • The react to the router
    • Ajax data request processing
    • The DOM processing

Comparison of effects before and after isomorphism

In order to simply compare performance and exclude the influence of network environment, the test was performed before isomorphism in the local development environment






FP FCP FMP DCL L
1766.5 ms 1766.5 ms 1950.1 ms 1706.1 ms 4400.2.5 ms






FP FCP FMP DCL L
403.8 ms 403.8 ms 403.8 ms 1343.5 ms 2442.5 ms

FMP decreased 1546.3ms before and after isomorphism!! At the same time, the speed of the front screen is significantly improved

In different environments, the test results will be different, but basically each test, after isomorphism than before the FMP decreased by 1000+ms!

React isomorphism principle

React isisomism is simply a way of concatenating HTML strings with renderToString on the server side, requesting initialization data, returning HTML and static files (js, CSS, etc.) to the browser, and executing React code on the browser side. Insert initialization data and mount related monitoring hydrate this article is highly recommended, showing ssrserver-side Rendering with React, Redux, and React-router very clearly. The following figure is also taken from the article

Application directory structure

React Router + Ant Design + CSS Module

Json │ postcss.config.js │ webpack.dev. Js │ webpack.dev ├─ SRC │ app.css │ app.jsx │ app.module. CSS │ ├─ SRC │ Favicon │ ├─public │ Favicon │ ├.html │ ├─ SRC │ app.jsx │ app.module └.css │ ├─ ├─container │ ├─ ├─source// Project static resourcesCopy the code

SRC /index.js is used to mount the browser before isometric configuration. After isometric configuration with Express, the directory is as follows

Json │ postcss.config.js │ server.js // the compiled server code │ webpack.common.js │ │ ├─ ├─ SRC // ├─ ├─ └ ├─ SRC // project source code │ ├ ─ browser / / browser side entry file index. The js │ ├ ─ server / / server entry documents │ index. The js │ └ ─ Shared / / Shared directory │ App. CSS │ App. JSX │ App. The module. The CSS │ Index.css │ RoutesConfig. js │ ├─ Components │ ├─ Container │ ├─source
Copy the code

After isomorphism, the browser accesses the Express interface, which returns HTML strings and compiled client code (static resources in dist directory). The file relationship of the browser mount event and initialization data directory structure can be expressed as follows:

Because each access to the port is dependent on static resources, each change to the code to see the effect requires webpack recompilation, which is relatively troublesome in the development process. The current idea is to configure webpackDevServe to open resource access on the local server to support hot update. Later, we will study dist as a static resource directory in this application, which contains JS packed by WebPack, CSS compiled by LessC, etc

Problems encountered by isomorphic processes

To sum up, this project uses Express + React + React Router + Ant Design + CSS Module, plus some simple logic codes (involving lazy loading of DOM and data request). The steps and problems solved are as follows:

  1. Server WebPack configuration – ESM, JSX, CSS Module
  2. Server /index.js structure written
  3. RenderTostring on the server side, hydrate on the browser side
  4. Extract the custom styled Ant Design into a separate CSS file
  5. React-router processing — Both ends are routable
  6. Ajax data request processing – Makes the back end return HTML with initialized data
  7. DOM processing – Node runs JS without DOM(Document), Window(Window, navigator, etc.), Storage(localStorage, sessionStorage, etc.) objects

Next, the reasons for the steps will be explained in order, and what plan was adopted

1. Configure webPack on the server

Js can run in the browser and Node, but there are some environmental differences (such as whether there is DOM), so it needs to configure the webpack for the server (namely node). This project is mainly for the following configuration

Webpack Target property – indicates that the packed JS environment is Node instead of browser ESM – Node (v10.15.3) does not support import and export JSX CSS Module – Hashes the className on the Node side

The first three items can be said to be required, the last item depends on the corresponding project technology stack

In the webpack configuration, you can set the target property:

. entry:"./src/server/index.js",
    target: "node".Copy the code


For ESM, JSX, and CSS Modules, the WebPack Module is configured on the Node side as required on the browser side

2. Server /index.js structure is written

We access the Express API and return the corresponding HTML, where we write out the basic structure of server/index.js

import express from "express";
import { renderToString } from "react-dom/server";
import { StaticRouter } from "react-router-dom";
import React from "react";
import App from ".. /shared/App";

const app = express();
app.use(
  express.static("dist", {
    index: false}));Dist (dist); dist (dist); dist (dist); dist (dist); Index false disables the index.** file index

app.get("Path",  (req,res)=>{
    /** * the client accesses the interface, which returns HTML **/
    constreactComhtml = ... ;The react element gets the HTML string, and the reactComhtml variable value is shown below
    consttheHtml = ... ;// Concatenate to complete HTML, theHtml variable value see below
    res.setHeader("Content-Type"."text/html");// Set the response header
    res.send(theHtml); / / returns the HTML
});
Copy the code

3. RenderToString on the server side and Hydrate on the browser side

Two apis, renderToString and Hydrate renderToString render the React element as initial HTML on the server side

// server/index.js
  const reactComHtml = ReactDOMServer.renderToString(
    <StaticRouter>
      <App reqPathname={req.path} />
    </StaticRouter>
);
  const theHtml = `
      <html>
      <head>.</head>
      <body>
          <div id="App">${reactComHtml}</div>// Concatenate the HTML string<script src="{... Static resource directory}/main.js" charset="utf-8"></script>// Carry webpack wrapped browser static resources, such as js files</body>
      </html>
  `;
Copy the code


Hydrate mounts listening events for existing elements on the browser side. React expects the server to render the same content as the browser. React also fixes inconsistencies in text content, but it’s best not to introduce inconsistencies artificially, and the fix mechanism isn’t guaranteed to be correct. React will issue a Warning if inconsistent content is found, such as Warning: Expected Server HTML to contain a matching

in

. Of course, there are many reasons for this warning, such as the react-Markdown component used in this application, which processed the Markdown text, causing some inconsistency between the server and the client, and the warning was raised in the development environment. For more information about Hydrate, see the website’s description of Hydrate

// brpwser/index.js
const app = document.getElementById("App");
app? ReactDOM.hydrate(
      <BrowserRouter>
        <App />
      </BrowserRouter>,app) : false;
Copy the code


4. Customize the Ant Design handling of the theme

This project uses Ant Design and is configured with babel-plugin-import for loading on demand, so if you customize the theme, you need to change the style configuration of babel-plugin-import to true. However, this results in antD styles not being extracted as separate CSS files, but instead being run through JS, styling by inserting a

Lessc --js --modify-var=@primary-color= @link-hover-color lessc --js --modify-var=@primary-color=#ffc34e --modify-var=@link-hover-color=#ffc34e ./node_modules/antd/dist/antd.less > dist/ant.css
Copy the code

Dist is the static resource directory. At this point, the custom style file can be referenced in the returned HTML

  const theHtml = `
      <html>
      <head>
      <title>${title}</title>
      <link rel="shortcut icon" href="The ${... Static resource directory}/favicon.ico">
      <link rel="stylesheet" type="text/css" href="The ${... Static resource directory}<link rel="stylesheet" type="text/ CSS "href="The ${... Static resource directory}/main.css">
      ...
      </head>
      <body>
          <div id="App">${reactComHtml}</div>
          <script src="The ${... Static resource directory}/main.js" charset="utf-8"></script>
      </body>
      </html>
  `;
Copy the code

5.react-router

Under CSR, the route is handed over to the front end. The back end has no corresponding route, so if the front end enters a path and refreshes the web page, the back end returns 404. Here’s an example:

A project is CSR, using the React-router. The home page is www.somebody.com, and we enter the browser at www.somebody.com/test. At this time, refresh or share the link with our friends, and the result will be 404. There are no HTML files or services in the background. There are several solutions to this on StackOverflow

Isomorphism can also solve this problem. On the server side, we use routing, as do browsers

// server/index.js
const reactComHtml = renderToString(
  <StaticRouter location={req.url}>
    <App />
  </StaticRouter>
);
const theHtml = `
      <html>
      <head>
      <title>Home</title>
      <link rel="shortcut icon" href="The ${... Static resource directory}/favicon.ico">
      <link rel="stylesheet" type="text/css" href="The ${... Static resource directory}/ant.css">
      <link rel="stylesheet" type="text/css" href="The ${... Static resource directory}/main.css">
      ...
      </head>
      <body>
          <div id="App">${reactComHtml}</div>
          <script src="/main.js" charset="utf-8"></script>
      </body>
      </html>
`;

// browser/index.js
const app = document.getElementById("App");
app ? ReactDOM.hydrate(
      <BrowserRouter>
        <App />
      </BrowserRouter>,app): false;
Copy the code

By filling in the path with the location property of StaticRouter, we can get the corresponding route HTML, which is concatenated into HTML and returned to the browser

6.Ajax data request processing

The front end basically pulls data from the back end through Ajax request, while the HTML we want to return under SSR contains the initial data. There are two questions: a. How do I request data from the server? B. How to present the data?

A. How do I request data on the server

You can also request data to the port via Ajax, but be aware of asynchrony and environmental issues. In the case of async, when multiple interfaces need to be requested, you need to handle async to ensure that the requested data can be passed to HTML. For example, you can use Promise to handle this. For environment issues, you need to use Ajax libraries that can run on Node, not only on the browser environment. I’m using Axios here

app.get("Path",(req, res)=>{
  axios.get(url)// Ajax request interface
      .then((response) = >{// Get data
          const data = response.data;
          const context = data;
          // How to present it below
          constreactComHtml = ... ;consttheHtml = ... ; res.setHeader("Content-Type"."text/html"); res.send(theHtml); })})Copy the code


B. How to present the data

__ROUTE_DATE__ (__ROUTE_DATA__) ¶ There are many options for how to render data. Here I have a StaticRouter context attribute + custom window.__route_date__ attribute. You can supplement the above code with any property that does not conflict with the Window object

  // server/index.js
  constreactComHtml = renderToString( <StaticRouter location={req.url} context={context}> <App reqPathname={req.path} /> </StaticRouter> ); const theHtml = ` <html> <head> <title>${title}</title> <link rel="shortcut icon" href="${... }/favicon.ico"> <link rel="stylesheet" type="text/ CSS "href="${... <link rel="stylesheet" type="text/ CSS "href="${... }/main.css"> <script> window.__route_data__ = ${json.stringify (context)}</script> // pass to HTML as a string, Using this data on the browser end < / head > < body > < div id = "App" > ${reactComHtml} < / div > < script SRC = "${... Static resource directory} / main js "charset =" utf-8 "> < / script > < / body > < / HTML > `; / / example: Shared/container/Article/index. The JSX/Article / : articleId routing of the corresponding Component class Article extends Component {constructor (props) { super(props); const staticContext = props.staticContext; If (staticContext) {// const {title, content, time, tag} = staticContext; this.state = { title: title, content: content, time: time, tag: tag }; } else {//B runs const data = window.__route_data__; this.state = { content: data.content, title: data.title, time: data.time, tag: data.tag }; delete window.__ROUTE_DATA__; }}... .Copy the code

Run on the server, props. StaticContext is also available only on the server. The server uses the staticContext value, renderToString outputs the HTML string, and the browser uses the window. Hydrate mounts listening events for existing elements

7. The DOM

Because node does not have DOM, Window, and Storage objects, it is necessary to determine whether the DOM processing encapsulation function is in the browser environment

const isBrowser = (a)= >{
    return typeof window! = ="undefined";
}
export default isBrowser;
Copy the code

In the corresponding code involving DOM, isBrowser is used to determine whether the current browser environment is used. If yes, the corresponding operation is performed; if no, no operation is performed

  constructor(props) {
    super(props);
    / / for
    if (isBrowser() === true) {
      this.handleDom = this.handleDom.bind(this);
      this.handleDom(); }}Copy the code

conclusion

After this practice, I have a clearer understanding of isomorphism, server rendering, Node, React, React Router, etc., and really feel the first screen speed improvement brought by SSR. But the isomorphism process is also relatively cumbersome, this rewrite is only a very simple blog application, there is no performance problem, if the complex application is bound to be a big project. The benefit brought by SSR is the improvement of SEO and the speed of the first screen, and the process of isomorphism for the original application is also very tedious. At this stage, whether it is necessary to change CSR into SSR private depends on specific scenarios and whether the input and output are worthwhile. In the future, with the upgrading of browsers, the progress of search engine crawlers and the improvement of the network, can we directly solve the problems of CSR in SEO and first-screen speed? But is that future too far away? Will there be a new solution? The future is really curious to see the above improper place please big guy pointed out.

The resources

This study referred to more articles, many implementation methods are different, there are also corresponding problems can be optimized. Server-side Rendering with React, Redux, And React-Router (recommended) User-centric Performance Metrics Ant Design custom topics react-Router official website — Server Rendering Stack Overflow React-router urls don’t work when refreshing or writing manually An Introduction to React Server-Side React Router 4 with server-side Rendering ReactCasts #12 – Server Side Rendering React Server Side Rendering with Express(recommended introductory practice article, but with unnecessary mustache syntax in the middle) Tips for server side rendering with React