Analysis and Practice of React Server Rendering Principle

  • 2021-11-01 02:06:43
  • OfStack

Most people have heard about server-side rendering, which is what we call SSR. Many students may have done server-side rendering projects in the company. Mainstream single-page applications, such as Vue or React, adopt a client-side rendering mode, which is what we call CSR.

However, this mode will bring two obvious problems. The first one is that TTFP takes a long time, and TTFP refers to the first screen display time. At the same time, it does not have the conditions for SEO ranking, and the ranking on search engines is not very good. So we can use a few tools to improve our project, the single-page application programming server-side rendering project, this can solve these problems.

At present, the mainstream server-side rendering framework, that is, SSR framework, has Nuxt. js for Vue and Next. js for React. Here, instead of using these SSR frameworks, we build a complete set of SSR frameworks from scratch to familiarize ourselves with his underlying principles.

Writing React components on the server side

If it is client rendering, the browser will first send a request to the browser, and the server will return the html file of the page, then send a request to the server in html, and the server will return the js file, and the js file will be performed in the browser to draw the page structure and render it to the browser to complete the page rendering.

If it is server-side rendering, this process is different. The browser sends a request, the server runs React code to generate pages, and then the server returns the generated pages to the browser, which renders. In this case, the React code is part 1 of the server instead of the front end.

Here, we demonstrate the code. First, we need npm init to initialize the project, and then install react, express, webpack, webpack-cli, webpack-node-externals.

We first write a component of React. . src/components/Home/index. js, because our js is executed in an node environment, we follow the CommonJS specification, using require and module. exports for import and export.


const React = require('react');

const Home = () => {
  return <div>home</div>
}

module.exports = {
  default: Home
};

The Home component developed here can't run directly in node. We need to package and compile jsx syntax into js syntax with webpack tool, so that nodejs can win recognition. We need to create an webpack. server. js file.

Using webpack on the server side requires adding a key-value pair where target is node. We know that if you use path path on the server side, you don't need to package it into js, and if you use path on the browser side, you need to package it into js, so the js that needs to be compiled on the server side and the browser side is completely different. So when we package, we should tell webpack whether it is server-side code or browser-side code.

entry entry file is our node startup file, here we write as./src/index. js, the output output file name is bundle, the directory is in the build folder with the directory.


const Path = require('path');
const NodeExternals = require('webpack-node-externals'); //  Server-side operation webpack Need to run NodeExternals,  His role is to put express Such node Modules are not packaged into js In. 

module.exports = {
  target: 'node',
  mode: 'development',
  entry: './src/server/index.js',
  output: {
    filename: 'bundle.js',
    path: Path.resolve(__dirname, 'build')
  },
  externals: [NodeExternals()],
  module: {
    rules: [
      {
        test: /.js?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          presets: ['react', 'stage-0', ['env', {
            targets: {
              browsers: ['last 2 versions']
            }
          }]]
        }
      }
    ]
  }
}

Installing dependency modules


npm install babel-loader babel-core babel-preset-react babel-preset-stage-0 babel-preset-env --save

Next, we write a simple service based on express module. ./src/server/index. js


var express = require('express');
var app = express();
const Home = require('../Components/Home');
app.get('*', function(req, res) {
  res.send(`<h1>hello</h1>`);
})

var server = app.listen(3000);

Running webpack is performed using the webpack. server. js configuration file.


webpack --config webpack.server.js

After packaging, an bundle. js will appear in our directory, and this js is the code that we packaged and generated, which can finally run. We can use node to run this file and start a 3000 port server. We can access this service by visiting 127.0. 0.1: 3000 and see the browser output Hello.


node ./build/bundile.js

The above code will be compiled using webpack before we run, so it supports ES Modules specification, and no longer forces CommonJS.

src/components/Home/index.js


import React from 'react';

const Home = () => {
  return <div>home</div>
}

export default Home;

/src/server/index. js We can use the Home component, where we first need to install react-dom, convert the Home component into a tag string with renderToString, of course, we need to rely on React here, so we need to introduce React.


import express from 'express';
import Home from '../Components/Home';
import React from 'react';
import { renderToString } from 'react-dom/server';

const app = express();
const content = renderToString(<Home />);
app.get('*', function(req, res) {
  res.send(`
    <html>
      <body>${content}</body>
    </html>
  `);
})

var server = app.listen(3000);


#  Repack 
webpack --config webpack.server.js
#  Running a service 
node ./build/bundile.js

At this time, the page shows the code of our React component.

The server-side rendering of React is a server-side rendering based on virtual DOM, and the server-side rendering will greatly accelerate the rendering speed of the first screen of the page. However, server-side rendering also has disadvantages. Client-side rendering React code is executed on the browser side, which consumes the performance of the user's browser side, but server-side rendering consumes the performance of the server side, because React code runs on the server. It consumes a lot of server performance, because React code consumes a lot of computational performance.

If your project is completely unnecessary to use SEO optimization and your project access speed is already very fast, it is recommended not to use SSR technology, because its cost is still relatively large.

After each modification of our code above, we need to re-execute webpack packaging and start the server, which is too troublesome to debug. In order to solve this problem, we need to do 1 automatic packaging of webpack and restart node. We add the build command to package. json, and automatically package files by--watch listening for changes.


{
  ...
  "scripts": {
    "build": "webpack --config webpack.server.js --watch"
  }
  ...
}

Just repackaging is not enough. We also need to restart the node server. Here we need to use the nodemon module. Here we use the global installation of nodemon and add an start command in the package. json file to start our node server. Use nodemon to listen to the build file and re-run exec "node./build/bundile. js" after the change. It is necessary to keep double quotation marks here, and just translate 1.


const Path = require('path');
const NodeExternals = require('webpack-node-externals'); //  Server-side operation webpack Need to run NodeExternals,  His role is to put express Such node Modules are not packaged into js In. 

module.exports = {
  target: 'node',
  mode: 'development',
  entry: './src/server/index.js',
  output: {
    filename: 'bundle.js',
    path: Path.resolve(__dirname, 'build')
  },
  externals: [NodeExternals()],
  module: {
    rules: [
      {
        test: /.js?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          presets: ['react', 'stage-0', ['env', {
            targets: {
              browsers: ['last 2 versions']
            }
          }]]
        }
      }
    ]
  }
}

0

At this point, we start the server. Here, we need to run the following commands in two windows, because no other commands are allowed after build.


const Path = require('path');
const NodeExternals = require('webpack-node-externals'); //  Server-side operation webpack Need to run NodeExternals,  His role is to put express Such node Modules are not packaged into js In. 

module.exports = {
  target: 'node',
  mode: 'development',
  entry: './src/server/index.js',
  output: {
    filename: 'bundle.js',
    path: Path.resolve(__dirname, 'build')
  },
  externals: [NodeExternals()],
  module: {
    rules: [
      {
        test: /.js?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          presets: ['react', 'stage-0', ['env', {
            targets: {
              browsers: ['last 2 versions']
            }
          }]]
        }
      }
    ]
  }
}

1

At this time, the page will be updated automatically after we modify the code.

However, the above process is still troublesome. We need two windows to execute the commands. We want one window to execute the two commands. We need a third-party module npm-run-all, which can be installed globally. Then modify 1 in package. json.

We are packaging and debugging should be in the development environment, we create an dev command, which executes npm-run-all,--parallel means parallel execution, executing all commands at the beginning of dev. We add an dev before start and build: At this time, I want to start the server and listen for file changes. Just run npm run dev.


const Path = require('path');
const NodeExternals = require('webpack-node-externals'); //  Server-side operation webpack Need to run NodeExternals,  His role is to put express Such node Modules are not packaged into js In. 

module.exports = {
  target: 'node',
  mode: 'development',
  entry: './src/server/index.js',
  output: {
    filename: 'bundle.js',
    path: Path.resolve(__dirname, 'build')
  },
  externals: [NodeExternals()],
  module: {
    rules: [
      {
        test: /.js?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          presets: ['react', 'stage-0', ['env', {
            targets: {
              browsers: ['last 2 versions']
            }
          }]]
        }
      }
    ]
  }
}

2

What is isomorphism

For example, in the following code, we bind an click event to div, and hope to pop up an click prompt when clicking. But after running, we will find that this event is not bound, because the server can't bind the event.

src/components/Home/index.js


import React from 'react';

const Home = () => {
  return <div onClick={() => { alert('click'); }}>home</div>
}

export default Home;

1 Our approach is to render the page first, and then run the same code in the browser like the traditional React project 1 again, so that this click event will have.

This leads to a concept of isomorphism. My understanding is that a set of React code is executed once on the server side and once again on the client side.

Isomorphism can solve the problem of invalid click event. First, the server can display the page normally once, and the client can bind the event once again.

We can load an index. js when the page is rendered, and use app. use to create the access path of the static file, so that the accessed index. js will be requested in the/public/index. js file.


const Path = require('path');
const NodeExternals = require('webpack-node-externals'); //  Server-side operation webpack Need to run NodeExternals,  His role is to put express Such node Modules are not packaged into js In. 

module.exports = {
  target: 'node',
  mode: 'development',
  entry: './src/server/index.js',
  output: {
    filename: 'bundle.js',
    path: Path.resolve(__dirname, 'build')
  },
  externals: [NodeExternals()],
  module: {
    rules: [
      {
        test: /.js?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          presets: ['react', 'stage-0', ['env', {
            targets: {
              browsers: ['last 2 versions']
            }
          }]]
        }
      }
    ]
  }
}

4

public/index.js


const Path = require('path');
const NodeExternals = require('webpack-node-externals'); //  Server-side operation webpack Need to run NodeExternals,  His role is to put express Such node Modules are not packaged into js In. 

module.exports = {
  target: 'node',
  mode: 'development',
  entry: './src/server/index.js',
  output: {
    filename: 'bundle.js',
    path: Path.resolve(__dirname, 'build')
  },
  externals: [NodeExternals()],
  module: {
    rules: [
      {
        test: /.js?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          presets: ['react', 'stage-0', ['env', {
            targets: {
              browsers: ['last 2 versions']
            }
          }]]
        }
      }
    ]
  }
}

5

Based on this situation, we can execute the React code once in the browser, and we will create a new the/src/client/index. js here. Post the code executed by the client. Here our isomorphic code uses hydrate instead of render.


const Path = require('path');
const NodeExternals = require('webpack-node-externals'); //  Server-side operation webpack Need to run NodeExternals,  His role is to put express Such node Modules are not packaged into js In. 

module.exports = {
  target: 'node',
  mode: 'development',
  entry: './src/server/index.js',
  output: {
    filename: 'bundle.js',
    path: Path.resolve(__dirname, 'build')
  },
  externals: [NodeExternals()],
  module: {
    rules: [
      {
        test: /.js?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          presets: ['react', 'stage-0', ['env', {
            targets: {
              browsers: ['last 2 versions']
            }
          }]]
        }
      }
    ]
  }
}

6

Then we also need to create an webpack. client. js file in the root directory. The entry file is./src/client/index. js, and the exit file is public/index. js


const Path = require('path');
const NodeExternals = require('webpack-node-externals'); //  Server-side operation webpack Need to run NodeExternals,  His role is to put express Such node Modules are not packaged into js In. 

module.exports = {
  target: 'node',
  mode: 'development',
  entry: './src/server/index.js',
  output: {
    filename: 'bundle.js',
    path: Path.resolve(__dirname, 'build')
  },
  externals: [NodeExternals()],
  module: {
    rules: [
      {
        test: /.js?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          presets: ['react', 'stage-0', ['env', {
            targets: {
              browsers: ['last 2 versions']
            }
          }]]
        }
      }
    ]
  }
}

7

Add 1 command to the package. json file to package the client directory


{
  ...
  "scripts": {
    "dev": "npm-run-all --parallel dev:**",
    "dev:start": "nodemon --watch build --exec node \"./build/bundile.js\"",
    "dev:build": "webpack --config webpack.server.js --watch",
    "dev:build": "webpack --config webpack.client.js --watch",
  }
  ...
}

In this way, when we start, we will compile the files that client runs. When you visit the page again, you can bind the events.

Let's sort out the code of the above project. There are many duplicates in the above files webpack. server. js and webpack. client. js. We can use webpack-merge plug-ins to merge the contents.

webpack.base.js


const Path = require('path');
const NodeExternals = require('webpack-node-externals'); //  Server-side operation webpack Need to run NodeExternals,  His role is to put express Such node Modules are not packaged into js In. 

module.exports = {
  target: 'node',
  mode: 'development',
  entry: './src/server/index.js',
  output: {
    filename: 'bundle.js',
    path: Path.resolve(__dirname, 'build')
  },
  externals: [NodeExternals()],
  module: {
    rules: [
      {
        test: /.js?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          presets: ['react', 'stage-0', ['env', {
            targets: {
              browsers: ['last 2 versions']
            }
          }]]
        }
      }
    ]
  }
}

9

webpack.server.js


const Path = require('path');
const NodeExternals = require('webpack-node-externals'); //  Server-side operation webpack Need to run NodeExternals,  His role is to put express Such node Modules are not packaged into js In. 

const merge = require('webpack-merge');
const config = require('./webpack.base.js');

const serverConfig = {
  target: 'node',
  mode: 'development',
  entry: './src/server/index.js',
  output: {
    filename: 'bundle.js',
    path: Path.resolve(__dirname, 'build')
  },
  externals: [NodeExternals()],
}

module.exports = merge(config, serverConfig);

webpack.client.js


const Path = require('path');
const merge = require('webpack-merge');
const config = require('./webpack.base.js');

const clientConfig = {
  mode: 'development',
  entry: './src/client/index.js',
  output: {
    filename: 'index.js',
    path: Path.resolve(__dirname, 'public')
  }
};

module.exports = merge(config, clientConfig);

In src/server, the server-run code is placed, and in src/client, the browser-run js is placed.


Related articles: