preface

Since the emergence of front-end frameworks (React,Vue,Angelar), each framework carries different ideas and is divided into three camps. The previous era of using JQuery has become the past. In the past, each page is an HTML, and corresponding JS and CSS are introduced, while DOM is written in HTML. And because of that, every time a user comes in, it doesn’t really feel like a slow response because of the DOM in the HTML.

But since using the framework, no matter how many pages, it is a single page, namely SPA. All DOM elements in HTML must be rendered by calling React. Render () after the client downloads javascript, so there is a long loading animation on many websites.

In order to solve this not very friendly problem, the community has proposed a lot of solutions, such as pre-render, SSR, isomorphism.

Of course, this article is about building a React server rendering isomorphism from scratch.

options

Solution one uses the community selected framework nex.js

Next. Js is a lightweight React server-side rendering application framework. If you are interested, you can go to the official website of nex.js.

Scheme two isomorphism

There are two schemes for isomorphism:

Babel escape node side code and React code after execution

let app = express();
app.get('/todo', (req, res) => {
     let html = renderToString(
     <Route path="/" component={ IComponent } >
        <Route path="/todo" component={ AComponent }>
        </Route>
    </Route>)
     res.send( indexPage(html) )
    }
})  

Copy the code

There are two issues to deal with here:

  • NodeNo front-end supportimportSyntax needs to be introducedbabelSupport.
  • NodeTag syntax cannot be resolved.

Therefore, when executing Node, you need to use Babel to escape. If something goes wrong, you can’t detect it. Personally, I don’t recommend this.

So the second option is used here

Webpack does the compilation

Use WebPack to package two copies of code, one for Node for server rendering and one for browser rendering.

Below specific detailed explanation.

Setting up a Node Server

Due to usage habits, Egg framework is often used, and Koa is the underlying framework of Egg, so here we use Koa framework to build the service.

Set up a basic Node service.

const Koa = require('koa');
const app = new Koa();

app.listen(3000, () = > {console.log("The server is started, please visit http://127.0.0.1:3000")});Copy the code

Configuration webpack

As we all know, React code needs to be packaged and compiled to execute, but the code that the server and client run is only partially the same, and some code doesn’t need to be packaged at all. In this case, you need to separate the client code from the code that the server runs, and you have two WebPack configurations

Webpack packs the same code into two copies with different WebPack configurations, serverConfig and clientConfig.

ServerConfig and clientConfig configurations

As we know from the WebPack documentation, WebPack can compile not only Web side code but also other content.

Here we set target to node.

Configure entry file and exit location:

const serverConfig = {
  target: 'node'.entry: {
    page1: './web/render/serverRouter.js',
  },
  resolve,
  output: {
    filename: '[name].js'.path: path.resolve(__dirname, './app/build'),
    libraryTarget: 'commonjs'}}Copy the code

Note ⚠

The server configuration needs to configure libraryTarget and set commonJS or UMD for the server reference to require, otherwise require value is {}.

There is no difference between client and server configuration. Target (the default Web environment) does not need to be configured. The other entry files are inconsistent with the output files.

const clientConfig = {
  entry: {
    page1: './web/render/clientRouter.js'
  },
  output: {
    filename: '[name].js'.path: path.resolve(__dirname, './public')}}Copy the code

Configure the Babel

Because you are packaging React code, you also need to configure Babel. Create a. Babelrc file.

{
  "presets": ["@babel/preset-react"["@babel/preset-env", {"targets": {
        "browsers": [
          "ie >= 9"."ff >= 30"."chrome >= 34"."safari >= 7"."opera >= 23"."bb >= 10"]}}]],"plugins": [["import",
      { "libraryName": "antd"."style": true}}]]Copy the code

This configuration is shared between the server and client to handle React and escape to ES5 and browser compatibility issues.

Handle server references

The server uses the CommonJS specification, and the server code does not need to be built, so the dependencies in node_modules do not need to be packaged, so the webpack third-party module webpack-nod-externals is used to handle this. After such processing, The size of the two built files is quite different.

With CSS

The difference between a server and a client may lie in the default processing, the need to separate the CSS into a file, and the handling of the CSS prefix.

Server Configuration

  {
    test: /\.(css|less)$/.use: [{loader: 'css-loader'.options: {
          importLoaders: 1}}, {loader: 'less-loader',}}]Copy the code

Client Configuration

  {
    test: /\.(css|less)$/.use: [{loader: MiniCssExtractPlugin.loader,
      },
      {
        loader: 'css-loader'
      },
      {
        loader: 'postcss-loader'.options: {
          plugins: [
            require('precss'),
            require('autoprefixer')],}}, {loader: 'less-loader'.options: {
          javascriptEnabled: true.// modifyVars: Theme //antd Default theme style}}}],Copy the code

The difference between client rendering and server rendering routing code in SSR

Realize the React of SSR architecture, we need to make the same code on the client side and service side each perform again, but their execution again here, does not include routing code, cause this is mainly because the client through the address bar to render different components, and the service side is through the component rendering request path. Therefore, we use BrowserRouter to configure the route on the client and StaticRouter to configure the route on the server.

Client Configuration

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from "react-router-dom";
import Router from '.. /router';

function ClientRender() {
  return (
      <BrowserRouter >
        <Router />
      </BrowserRouter>)}Copy the code

Server Configuration

import React from 'react';
import { StaticRouter } from 'react-router'
import Router from '.. /router.js';

function ServerRender(req, initStore) {

  return (props, context) = > {
    return (
        <StaticRouter location={req.url} context={context} >
          <Router />  
        </StaticRouter>)}}export default ServerRender;

Copy the code

Configure Node again for server rendering

The server configured above is simply started with a service, without further configuration.

The introduction of ReactDOMServer


const Koa = require('koa');
const app = new Koa();
const path = require('path');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const koaStatic = require('koa-static');
const router = new KoaRouter();

const routerManagement = require('./app/router');
const manifest = require('./public/manifest.json');
/** * Processing link * @param {* The name of the file to render the server is the default file in the build folder} fileName */
function handleLink(fileName, req, defineParams) {
  let obj = {};
  fileName = fileName.indexOf('. ')! = =- 1 ? fileName.split('. ') [0] : fileName;

  try {
    obj.script = `<script src="${manifest[`${fileName}.js`]}"></script>`;
  } catch (error) {
    console.error(new Error(error));
  }
  try {
    obj.link = `<link rel="stylesheet" href="${manifest[`${fileName}.css`]}"/ > `;
    
  } catch (error) {
    console.error(new Error(error));
  }
  // Server rendering
  const dom = require(path.join(process.cwd(),`app/build/${fileName}.js`)).default;
  let element = React.createElement(dom(req, defineParams));
  obj.html = ReactDOMServer.renderToString(element);

  return obj;
}

/** * Set static resources */
app.use(koaStatic(path.resolve(__dirname, './public'), {
  maxage: 0.// Browser cache max-age (in milliseconds)
  hidden: false.// Allow transfer of hidden files
  index: 'index.html'.// Default file name, default is 'index.html'
  defer: false.// If true, return next() after, allowing any downstream middleware to respond first.
  gzip: true.// If the client supports gZIP and a request file whose extension name is.gz exists, try to automatically provide the gZIP compressed version of the file. The default is true.
}));

/** * Processing response ** **/
app.use((ctx) = > {
    let obj = handleLink('page1', ctx.req, {});
    ctx.body = ` <! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, Word-wrap: break-word! Important; "> < img style =" text-align: center;${obj.link}
        </head>
        
        <body>
          <div id='app'>
             ${obj.html}
          </div>
        </body>
        ${obj.script}
        </html>
    `
})

app.listen(3000, () = > {console.log("The server is started, please visit http://127.0.0.1:3000")});Copy the code

This involves a manifest file, which is generated by the Webpack plug-in webpack-manifest-plugin and contains the compiled addresses and files. The structure looks something like this:

{
  "page1.css": "page1.css"."page1.js": "page1.js"
}
Copy the code

We introduce it into clientConfig and add the following configuration:

. plugins: [// Extract the style and generate a separate file
    new MiniCssExtractPlugin({
        filename: `[name].css`.chunkFilename: `[name].chunk.css`
    }),
    new ManifestPlugin()
]
Copy the code

In the server code above, we curlize serverrender. js. The purpose of this is that we use ServerRender’s StaticRouter, which is recognized by the server, and set the location parameter. And location takes a parameter URL. Therefore, we need to pass the REQ in the renderToString so that the server can properly parse the React component.

  let element = React.createElement(dom(req, defineParams));
  obj.html = ReactDOMServer.renderToString(element);
Copy the code

By parsing the handleLink, we can get an obj with three parameters, link (CSS link), script (JS link) and HTML (Dom element generation).

Render the HTML with ctx.body.

renderToString()

Render the React element into its initial HTML. This function should only be used on the server. React will return an HTML string. You can use this method to generate HTML on the server and send tags on the initial request to speed up page loading and allow search engines to crawl your page for SEO purposes.

If you call reactdom.hydrate () on a node that already has this server rendering tag, React will keep it and attach only event handlers, giving you a very high-performance first load experience.

renderToStaticMarkup()

Similar to renderToString, except this does not create the extra DOM properties that React uses internally, such as data-Reactroot. This is useful if you want to use React as a simple static page generator, as stripping out extra attributes saves a few bytes.

However, if this method is used after a browsing visit, it will completely replace the server-side rendered content and therefore cause the page to flicker, so it is not recommended.

renderToNodeStream()

Render the React element into its original HTML. Returns a readable stream that outputs an HTML string. The stream (stream) output HTML completely equivalent to ReactDOMServer. RenderToString will return the content.

We can also use renderToNodeSteam above to remake it:

  let element = React.createElement(dom(req, defineParams));
  
  ctx.res.write('   
       < img style =" text-align: center; 
      
'
); / / the component rendering into a stream, and give the Response const stream. = ReactDOMServer renderToNodeStream (element); stream.pipe(ctx.res, { end:'false'}); // When React renders, send the rest of the HTML to the browser stream.on('end', () => { ctx.res.end('</div></body></html>'); }); Copy the code

renderToStaticNodeStream()

Similar to renderToNodeStream, except this does not create additional DOM properties that React uses internally, such as data-Reactroot. This is useful if you want to use React as a simple static page generator, as stripping out extra attributes saves a few bytes.

The stream (stream) output HTML completely equivalent to ReactDOMServer. RenderToStaticMarkup will return the content.

Add a state management redux

This is fine for developing a static website or a relatively simple project, but it’s not enough for a complex project, so let’s add global state management Redux.

The order is synchronized in server rendering, so the data must be prepared in advance for the first screen data rendering to occur during rendering.

  • Get data ahead of time
  • Initialize the store
  • Display components by route
  • Combine data and components to generate HTML that is returned once

Adding redux to the client is no different than regular redux, except that it adds an initial window.__init_store__ for the store.

let initStore = window.__INIT_STORE__;
let store = configStore(initStore);

function ClientRender() {
  return (
    <Provider store={store}>
      <BrowserRouter >
        <Router />
      </BrowserRouter>
    </Provider>)}Copy the code

For the server, promise.all () can be used to make concurrent requests after the initial data is obtained. When the request is complete, the data is populated in a script tag named window.__init_store__.

`<script>window.__INIT_STORE__ = ${JSON.stringify(initStore)}</script>`
Copy the code

Then reconfigure the store on the server.

function ServerRender(req, initStore) {
  let store = CreateStore(JSON.parse(initStore.store));

  return (props, context) = > {
    return (
      <Provider store={store}>
        <StaticRouter location={req.url} context={context} >
          <Router />  
        </StaticRouter>
      </Provider>)}}Copy the code

Finishing Koa

Considering the convenience of later development, add the following features:

  • Function of the Router
  • HTML template

Add Koa – the Router

/** * Register routes */
const router = new KoaRouter();
const routerManagement = require('./app/router'); . routerManagement(router); app.use(router.routes()).use(router.allowedMethods());Copy the code

To ensure that the interface is organized during development, all routes are written in a new file. And guarantee the following format:

/** ** @param {router instantiated object} router */

const home = require('./controller/home');

module.exports = (router) = > {
  router.get('/',home.renderHtml);
  router.get('/page2',home.renderHtml);
  router.get('/favicon.ico',home.favicon);
  router.get('/test',home.test);
}

Copy the code

The process template

Putting HTML into the code doesn’t feel very friendly, so the service template KOa-Nunjucks-2 is also introduced.

It also has a layer of middleware on top of it to pass parameters and handle various static resource links.

. const koaNunjucks =require('koa-nunjucks-2'); ./** * Server render, render HTML, render template * @param {*} CTX */
function renderServer(ctx) {
  return (fileName, defineParams) = > {
    let obj = handleLink(fileName, ctx.req, defineParams);
    // Handle custom parameters
    defineParams = String(defineParams) === "[object Object]" ? defineParams : {};
    obj = Object.assign(obj, defineParams);
    ctx.render('index', obj); }}.../** * Template rendering */
app.use(koaNunjucks({
  ext: 'html'.path: path.join(process.cwd(), 'app/view'),
  nunjucksConfig: {
    trimBlocks: true}}));/** * Render Html */
app.use(async (ctx, next) => {
  ctx.renderServer = renderServer(ctx);
  await next();
});
Copy the code

When the user accesses the server, the renderServer function is called to handle the link, and at the end, ctx.render is called to complete the rendering.


/** * Render react page */

 exports.renderHtml = async (ctx) => {
    let initState = ctx.query.state ? JSON.parse(ctx.query.state) : null;
    ctx.renderServer("page1", {store: JSON.stringify(initState ? initState : { counter: 1})}); } exports.favicon =(ctx) = > {
   ctx.body = null;
 }

 exports.test = (ctx) = > {
   ctx.body = {
     data: 'Test data'}}Copy the code

As for KOA-Nunjucks-2, when rendering HTML, the < > will be processed safely, so we need to filter the data we pass in.


      
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Koa-react server rendering</title>
  {{ link | safe }}
</head>

<body>
  <div id='app'>
    {{ html | safe }}
  </div>
</body>
<script>
  window.__INIT_STORE__ = {{ store | safe }}
</script>
{{ script | safe }}
</html>
Copy the code

The document structure

├ ─ ─ the README. Md ├ ─ ─ the app// Node service code│ ├ ─ ─ build │ │ ├ ─ ─ page1. Js │ │ └ ─ ─ page2. Js │ ├ ─ ─ controller │ │ └ ─ ─ home. Js │ ├ ─ ─ the router. The js │ └ ─ ─ the view │ └ ─ ─ Index.html ├ ─ ─ index. Js ├ ─ ─ package. The json ├ ─ ─ the public// Front-end static resources│ ├ ─ ─ the manifest. Json │ ├ ─ ─ page1. CSS │ ├ ─ ─ page1. Js │ ├ ─ ─ page2. CSS │ └ ─ ─ page2. Js ├ ─ ─ the web// Front-end source code│ ├ ─ ─ the action//redux -action│ ├─ class.htm │ ├─ class.htm │ ├─ exercises/ / component│ ├─ PDF │ ├─ index.htm │ ├─ index.htm │ ├─ pages/ / the main page│ │ ├ ─ ─ page │ │ │ ├ ─ ─ index. The JSX │ │ │ └ ─ ─ but less │ │ └ ─ ─ page2 │ │ ├ ─ ─ index. The JSX │ │ └ ─ ─ but less │ ├ ─ ─ reducer//redux -reducer│ │ ├── class.htm │ │ ├─ index.htm │ ├─ render// WebPack entry file│ │ ├── Clientrouter.js │ ├── Route.js │ ├─ Route.js │ ├─ route.js// Front-end routing│ └ ─ ─ store//store│ ├ ─ index.js │ ├ ─ index.jsCopy the code

The last

Currently, this architecture can only manually start Koa services and start WebPack.

If you need to put Koa and Webpack together, this is a different topic, and you can check out my original article here.

Koa and Webpack?

If you need to know what a full server needs, check out my earlier articles.

How to Create a Reliable and Stable Web Server

The GITHUB address is as follows:

React server rendering based on KOA

References:

React Chinese Documents Webpack Chinese Documents React Isomorphism (SSR) Principle Context Review Redux