preface

Before we read the article, let’s understand a few concepts.

  • SPA (Single Page Application) : Single Page Application, is a solution proposed when the front and back ends are separated. In an application or site, there is only one complete HTML page, and that page has a container root node into which you can insert code snippet that needs to be loaded. The working principle of SPA: the front-end routing hop rotor page system, by changing the URL of the page, without requesting the page again, to achieve local update page view.

  • SEO (Search Engine Optimization) : The use of Search Engine rules to improve the natural ranking of a site within the relevant Search engines. SEO works: When a web page is requested, the content sent from the server can be crawled into the data by the search engine crawler. At this time, the keywords searched from the search engine are included in the content, so the information of the URL is more easily displayed in the search results.


Why are we back to server-side rendering now?

Let’s review the evolution of page rendering:

Traditional server rendering: It is also called back-end template rendering (such as JSP or PHP), which is the earliest web. When the client requests, the template engine is used on the server to splicing the template and data into complete HTML, and then it is sent to the client. After receiving the template, the client can directly parse the HTML and display it on the browser. No additional asynchronous requests are required to retrieve data. But at this time, the Web can only achieve simple information display, to make the Web interactive, the client needs to use JS to manipulate dom or render other dynamic parts.

The traditional server rendering process is as follows:

Servers of this era had templates, back-end code for reading data, and a lot of JS code. This leads to the following shortcomings:

  1. Unclear responsibilities at the front and back ends
  2. The front and back end code is mixed up
  3. Projects are difficult to manage and maintain

Still, there are some benefits to this rendering:

  1. The client can quickly render the pages rendered by the server, reducing the white screen time, which provides a good user experience
  2. SEO friendly, server render HTML with page content sent from the server, can improve search ranking.

Client-side rendering: This refers to the use of JS to render a large part of a page, representing popular SPA single-page applications such as React and Vue. When the client requests, the server does not do any processing, but directly returns the HTML generated after the front-end resources are packaged to the client. At this time, there is no web content in the HTML, and the client needs to load and execute THE JS code to render and generate the page content, and at the same time complete the event binding. The client then requests data from the backend API via Ajax to update the view.

The client rendering process is as follows:

In this way, the front and back end code is decoupled, and both ends interact with Ajax for partial refreshes. But this approach also has some drawbacks:

  1. The first screen is slow to load because the page can’t be rendered until js is loaded
  2. SEO is not friendly, spA-mode client rendering from the server is just an empty shell without content, most of the page content through JS rendering, search engines naturally crawl things
  3. The client renders at least three HTTP requests, the first for the page, the second for the JAVASCRIPT script in the page (dom rendering and event binding), and the third for dynamic data Ajax requests. Server-side rendering reduces the traditional 3 serial HTTP requests to a single HTTP request. The client only needs to request the page and parse the HTML returned by the server.
  4. The rendering needs to be repeated because ajax requests are sent to get data in the background, and typically in the life cycle of componentDidMounted, the page has already been rendered once, and when the view is updated after the data is retrieved, the page needs to be rendered again

It is interesting to look at the pros and cons of traditional server rendering versus client rendering. The pros of server rendering are the cons of client rendering, the cons of server rendering are the cons of client rendering, and vice versa. Then why not combine the advantages of the traditional pure server side straight out of the first screen with the advantages of SPA in-station experience to obtain the optimal solution? This leads to the current popular Server Side Rendering, or “isomorphic Rendering”, which is more accurate.


What is isomorphic rendering?

Isomorphic rendering: In layman’s terms, a set of React code runs once on the server and again in the browser. The server renders the page structure and the client renders the binding event. On the basis of SPA, it makes use of server rendering to render the first screen directly, which removes the dilemma faced by single page application in the first screen rendering.

The isomorphic rendering process is as follows:


Why isomorphic rendering?

SSR can be realized, there are two important premises, one of which is indispensable:

  • As mentioned earlier, the React. Js code for homogeneous rendering projects is executed once on both the client and server sides. This isomorphism is made possible by running JavaScript code in the Node environment. Why does JavaScript run in a Node environment? JS is a scripting language that requires a parser to run. For JS written in HTML pages, the browser acts as a parser. For JS that need to run independently, NodeJS is a parser. Each parser is a runtime environment that not only allows JS to define various data structures and perform various calculations, but also allows JS to do something with built-in objects and methods provided by the runtime. For example, JS running in a browser is used to manipulate the DOM, and browsers provide built-in objects like Document. The purpose of JS running in NodeJS is to operate disk files or build HTTP servers. NodeJS provides built-in objects such as FS and HTTP accordingly.

  • React code that directly operates on the DOM will generate an error in the Node environment. If you React code that directly operates on the DOM will generate an error in the Node environment. I can’t make it isomorphic. Fortunately, the React framework introduced the concept of the virtual DOM

Virtual DOM: The virtual DOM is essentially a JavaScript object that describes the real DOM.

For example, now we need to describe a button, which is very simple in HTML syntax:

<button class="btn btn-blue"> 
 <em>Confirm</em> 
</button>
Copy the code

This contains the element’s type and attributes, and when we use JavaScript to describe the element, the element can simply be represented as a pure JSON object, which still contains the element’s type and attributes:

{ 
 type: 'button'.props: { 
   className: 'btn btn-blue'.children: [{ 
     type: 'em'.props: { 
     children: 'Confirm'}}}}]Copy the code

This allows us to create Virtual DOM elements in JavaScript.

React doesn’t actually manipulate the DOM directly when performing page operations. Instead, it does data changes by manipulating the virtual DOM, that is, by manipulating ordinary JavaScript objects, which makes isomorphic rendering possible.

Other frameworks, such as Vue, enable SSR by introducing the same virtual DOM technology as React.


React how to render isomorphism?

First we need to know which parts of the application should be isomorphic:

  • routing
  • view
  • Data acquisition
  • State management

Let’s go down and see how each of them is isomorphic in this way.

1. Isomorphic routes

Both client side and server side, we need to match the route and find the corresponding routing component when the user initiates the request. On the server side, routing components are found through the request path, while on the client side, routing components are found through the url in the browser. They are two completely different mechanisms.

First of all, we need to obtain routing rules, this part of the code server and client can be common, its most basic responsibility is to render UI in the URL and its path corresponding, can use reduced form file system routing read, get configuration files like the following route:

// Routes.js
import React from 'react';
import {Route} from 'react-router-dom'
import Home from './containers/Home';
import Login from './containers/Login'

export default (
  <div>
    <Route path='/' exact component={Home}></Route>
    <Route path='/login' exact component={Login}></Route>
  </div>
)
Copy the code

In client-side routing, we use the React-router-dom BrowserRouter, which is stateful. We type a URL into the browser address bar, and BrowserRouter will automatically match the corresponding routing component from the browser address.

// client/index.js
import React from 'react';
import { BrowserRouter } from 'react-router-dom'
import Routes from '.. /Routes'

const App = () = > {
  return (
    <BrowserRouter>
      {Routes}
    </BrowserRouter>)}Copy the code

In server-side routing, we use the React-router-dom StaticRouter, which is stateless and requires us to pass location (the current request path) to the StaticRouter component. This allows StaticRouter to figure out who the currently required component is based on the path. So we need to set up a middleware so that when the server receives the request, it passes the current request path as props to StaticRouter.

// server/index.js
import express from 'express';
import {render} from './utils';

const app = express();
app.use(express.static('public'));
// Pass the request path to StaticRouter
app.get(The '*'.function (req, res) {
   res.send(render(req));
});

app.listen(3001.() = > {
  console.log('listen:3001')});Copy the code
// server/utils.js
import Routes from '.. /Routes'
import { StaticRouter } from 'react-router-dom'; 
import React from 'react'

export const render = (req) = > {
  const App = () = > {
    return (
        <StaticRouter location={req.path}>
            {Routes}
        </StaticRouter>)}}Copy the code

2. Isomorphic view

When the client and server match routes to the react Component, we need to perform different operations on it.

In client rendering, we use reactdom.render () to render the React Component directly into the real DOM.

// client/index.js
import { BrowserRouter } from 'react-router-dom';

ReactDom.render(<App />.document.getElementById('root'))
Copy the code

In server-side rendering, we use the renderToString API to convert the React Component into an HTML string, use renderToStaticMarkup to supplement the header and introduce the JS script, and then send it to the client to render into the real DOM.

// server/utils.js
import { renderToString } from 'react-dom/server';

// ...
  const content = renderToString(
    <App />
  );
  // Add headers and introduce front-end resources packaged CSS and JS resources
  const html = ReactDOMServer.renderToStaticMarkup(
    <html>
      <head>
        <link href={"assets/style.css" />
      </head>
      <body>
        <! Fill the root node with the server render return page content -->
        <div id="root" dangerouslySetInnerHTML={
          {__html: content}
        } />
        <! Import import file after front-end resource package -->
        <script src={"assets/index.js"} ></script>
      </body>
    </html>
  );
  return html;

Copy the code

How does the React Component convert to an HTML string? The React-DOM (React-DOM/Server) package has two React apis that do this: renderToString and renderToStaticMarkup. Both functions take a React Component argument and return an HTML String. But there is a difference: the DOM of HTML generated by renderToString has additional attributes: Each DOM has a data-checksum attribute, which is composed of monotonically increasing ids. The text node also has a react-text and id. The first DOM also has a data-checksum attribute. If two components have the same props and DOM structure, the adler32 algorithm gives the same checksum, similar to the hash algorithm. Let’s take a look at the code to see what it looks like:

renderToString(
  <div>
  	Thisissome<span>server-generated</span><span>HTML.</span>
  </div>
);
Copy the code

This section generates the following HTML:

<div data-react-root="" data-react-id="1"
  data-react-checksum="122239856"> <! --react-text:2-->Thisissome<! --/react-text--><span data-react-id="3">server-generated</span><! --react-text:4-- > <! --/react-text--><span data-react-id="5">HTML.</span>
</div>
Copy the code

When rendering, the client checks whether the HTML DOM has the same data-react-checksum. If they do, the client can directly use the DOM tree generated by the server without repeated rendering. If not, the client rerenders the entire HTML. This keeps the view rendered on the server side consistent with the view rendered on the client side.

RenderToStaticMarkup generates HTML DOM with no additional attributes, saving the size of the HTML string. Components rendered on the server side using renderToStaticMarkup will not have the data-react-checksum attribute, and the client will rerender the component, overwriting the server-side component. Therefore, when the page is not rendering a static page, it is best to use the renderToString method.

In React 16, all the ids are removed from the nodes, which makes it easy to read and significantly reduces the size of the HTML file. Client-side rendering in React 16 uses a difference algorithm to check the accuracy of server-generated nodes, which is also looser than React 15; For example, the node attributes generated by the server are not required to be in exactly the same order as the client. When the React 16 client renderer detects node mismatches, it only tries to modify the mismatched HTML subtree, rather than the entire HTML tree. Check out this article on the differences between React15 and React 16 SSR

3. Isomorphic data acquisition

In a typical React client development, we typically fetch asynchronous data in the component’s componentDidMount lifecycle function. However, the server never executes componentDidMount, which results in the server not getting the data. Now our job is to get the server to perform the data retrieval operation again to achieve a true server rendering.

To do this, we need to modify the route by assigning it a loadData parameter, which represents the server’s function to fetch data.

Each time the server matches a route to render a component, we check whether the component corresponding to the route has a loadData method. If so, we call this function on the server to get the data, and then render the routing component as props. There is a problem with this. The client and server use the same react code. The routing component has no props to access data during client rendering.

To make sure the props on the server and the client are consistent, we can add a script tag to the HTML code returned by the loadData on the server. Assign the front props generated by a server to the window.APP_PROPS object of the client. When the client initializes the root component, use this APP_PROPS to pass the props of the root component.

// server/utils.js
const propsScript = 'window.APP_PROPS = ' + JSON.stringify(props);

const html = ReactDOMServer.renderToStaticMarkup(
        <html>
            <! -... -->
            <body>
         	<! -... -->
                <! Add HTML source code to the server and add data to the window.
                <script dangerouslySetInnerHTML={
                    {__html: propsScript}
                }></script>
            </body>
        </html>
    );
 
    return html;
 }
Copy the code

This is called “water injection” of data, which means that the server data is injected into the Window global environment, the client receives the HTML source code, and when it has the data, it can be used as props for the root component of the client. This is called “dehydration” processing.

// client/index.js
// Get the interface data returned by the server
const APP_PROPS = window.APP_PROPS || {};

// When the client initializes the root component, it passes the props of the root component with this APP_PROPS
ReactDom.render(<App initialData={APP_PROPS.initialData}/>.document.getElementById('root'))
Copy the code

Above, we said that the data of the page is fetched by the loadData function during server-side rendering. Data acquisition, and on the client side, still need to be done, because if this page is your visit to the first page you see is the content of the server side rendering, but if after the react – the router routing jump the second page, then this page is completely renders the client, so the client also need to take data.

ComponentDidMount is not executed on the server, unlike componentWillMount, which is executed on both the client and the server. So we don’t have to worry about componentDidMount conflicting with loadData, which is why fetching data should be componentDidMount instead of componentWillMount. The data acquisition conflict between the server and the client can be avoided.

4. Isomorphic state management

The store of isomorphic state management is very similar to the props of isomorphic data acquisition. It is necessary to render the entire state tree after the server prefetch data to the page, and then use this tree as the initial state when the front end initialises the state manager store, so as to ensure the consistency between the front-end rendering result and the back end. However, when creating a store on the server side, be careful not to write as follows for creating a store on the client side.

const store=createStore(reducer,defaultState);
export default store;
Copy the code

In client rendering, there’s always only one Store in the user’s browser, so you can write this in code, whereas on the server side, it’s a little bit of a problem, because the Store on the server side is what all the users are going to use, and if you build a Store like this, the Store becomes a singleton, When all users share the Store, obviously there’s a problem. So in server-side rendering, Store creation should look like this, returning a function that is re-executed each time a user accesses it, providing a separate Store for each user:

const getStore=(req) = >{
	return createStore(reducer,defaultState);
}
export default getStore;
Copy the code


Review and summary

1. What is isomorphic rendering? Isomorphic rendering is the same react code that performs static rendering on the server side and event binding on the client side.

2. Why does react need to be executed on the server and again on the client? / Why is the server rendered page structure event binding invalid? RenderToString is used by the server to render the page. RenderToString under the React-DOM /server does not handle events, so there is no event binding for the content returned to the browser. The rendered page is just a static HTML page. After a client renders the React component and initializes the React instance, it can update the component’s props and state, initialize the React event system, and implement the virtual DOM re-rendering mechanism to make the React component “live”.

3. The React component has been rendered once on the server. If the React component is rendered again on the client, will the React component be rendered twice? The answer is no. The trick is to use the data-react-checksum attribute. If renderToString is used to render a component, the first DOM of the component will have the data-react-checksum attribute. This attribute is calculated using the Adler32 algorithm: If two components have the same props and DOM structure, the adler32 algorithm evaluates the same checksum, similar to the hash algorithm. When rendering the React component, the client first calculates the checksum value of the component, and then retrievalthe HTML DOM to see if there is a data-react-checksum attribute with the same value. If there is one, the component will only be rendered once. A warning exception is thrown. That is, when the server and client render components with the same props and DOM structure, the React component will render only once.

4. The most important feature of isomorphism application? The most important features of homogeneous applications are: only the first screen is rendered on the server side (purpose: to speed up rendering on the first screen and optimize SEO), and the subsequent page jump is carried out through the front end route, following SPA (purpose: support local refresh, separation of the front and back ends, and reduce the pressure on the server.

5. Problems caused by isomorphism application? The advantages of isomorphic rendering are only mentioned above, but in fact there are many problems caused by isomorphic rendering, such as:

  • DOM and BOM apis, such as Document and Window objects, cannot be manipulated during server rendering and should be avoided in the first screen logic
  • Increased code complexity, some code operations need to be run in different environments
  • Be careful not to package external extension libraries that only run on the server, as this will result in a large file size after the build

6. Suggestions for using isomorphic applications

  • First screen rendering speed really matters
  • SEO is really needed


The resources

ReactDOMServer

The pain and joy of creating homogeneous applications in ReactJS

Start from scratch and thoroughly understand server-side rendering