React server-side rendering

1. What is it

In SSR (opens new window), we learned about Server-Side Rendering, referred to as SSR, which means server-side rendering

Refers to the page processing technology that completes the HTML structure splicing of the page on the server side, sends it to the browser, and then binds status and events to it to become a fully interactive page.

There are two main problems it solves:

  • SEO, since search engine crawlers can view fully rendered pages directly
  • Accelerate the loading of the first screen and solve the problem of white screen on the first screen

2. How to do it

In react, there are two main forms of implementing SSR:

  • Manually build an SSR framework
  • Use a mature SSR framework such as Next.JS

This is mainly implemented by manually building a SSR framework.

First, start a app.js file through express to listen for requests on port 3000. When the root directory is requested, HTML is returned, as follows:

const express = require('express')
const app = express()
app.get('/', (req,res) => res.send(`
<html>
   <head>
       <title>ssr demo</title>
   </head>
   <body>
       Hello world
   </body>
</html>
`))

app.listen(3000, () => console.log('Exampleapp listening on port 3000!'))

Then write the react code in the server and reference it in app.js

import React from 'react'

const Home = () =>{

    return <div>home</div>

}

export default Home

In order for the server to recognize JSX, you need to use webpakc to package and convert the project, create a configuration file webpack.server.js and make related changes Configuration, as follows:

const path = require('path') //node's path module
const nodeExternals = require('webpack-node-externals')

module.exports = {
    target:'node',
    mode:'development', //development mode
    entry:'./app.js', //entry
    output: { //Package export
        filename:'bundle.js', //Packaged file name
        path:path.resolve(__dirname,'build') //The build folder stored in the root directory
    },
    externals: [nodeExternals()], //Keep the reference method of require in node
    module: {
        rules: [{ //Packaging rules
           test: /\.js?$/, //Package all js files
           loader:'babel-loader', //Use babel-loader for packaging
           exclude: /node_modules/,//Do not package js files in node_modules
           options: {
               presets: ['react','stage-0',['env', {
                                  //Additional packaging rules for loader, converting react, JSX, ES6
                    targets: {
                        browsers: ['last 2versions'] //Compatible with the last two versions of mainstream browsers
                    }
               }]]
           }
       }]
    }
}

Then with the help of react-dom, the renderToString method of server-side rendering is provided, which is responsible for parsing the React component into html

import express from 'express'
import React from 'react'//Introducing React to support JSX syntax
import { renderToString } from 'react-dom/server'//Introduce renderToString method
import Home from'./src/containers/Home'

const app= express()
const content = renderToString(<Home/>)
app.get('/',(req,res) => res.send(`
<html>
   <head>
       <title>ssr demo</title>
   </head>
   <body>
        ${content}
   </body>
</html>
`))

app.listen(3001, () => console.log('Exampleapp listening on port 3001!'))

In the above process, the component has been successfully rendered to the page.

However, some event processing methods cannot be completed on the server side, so the component code needs to be executed again in the browser. This way of sharing a set of code between the server side and the client is called isomorphism.

In layman’s terms, refactoring means running a set of React code once on the server, and then running it again on the browser:

  • Server-side rendering completes the page structure
  • Browser rendering completion event binding

The way the browser implements event binding is to let the browser pull the JS file for execution and let the JS code control it, so it is necessary to introduce script Label

The react code executed by the client is introduced into the page through the script tag, and the static middleware of express is used for js file configuration routing, modify as follows:

import express from 'express'
import React from 'react'//Introducing React to support JSX syntax
import { renderToString } from'react-dom/server'//Introduce renderToString method
import Home from './src/containers/Home'
 
const app = express()
app.use(express.static('public'));
//Use the static middleware provided by express. The middleware will point the routing of all static files to the public folder.
 const content = renderToString(<Home/>)
 
app.get('/',(req,res)=>res.send(`
<html>
   <head>
       <title>ssr demo</title>
   </head>
   <body>
        ${content}
   <script src="/index.js"></script>
   </body>
</html>
`))

 app.listen(3001, () =>console.log('Example app listening on port 3001!'))

Then execute the following react code on the client, and create a new webpack.client.js as the webpack configuration file of the client React code as follows:

const path = require('path') //node's path module

module.exports = {
    mode:'development', //development mode
    entry:'./src/client/index.js', //entry
    output: { //Package export
        filename:'index.js', //Packaged file name
        path:path.resolve(__dirname,'public') //The build folder stored in the root directory
    },
    module: {
        rules: [{ //Packaging rules
           test: /\.js?$/, //Package all js files
           loader:'babel-loader', //Use babel-loader for packaging
           exclude: /node_modules/, //Do not package js files in node_modules
           options: {
               presets: ['react','stage-0',['env', {
                    //Additional packaging rules for loader, here convert react and JSX
                    targets: {
                        browsers: ['last 2versions'] //Compatible with the last two versions of mainstream browsers
                    }
               }]]
           }
       }]
    }
}

This method can easily implement react server-side rendering of the homepage. The process corresponds to the following figure:

After the initial rendering is completed, an application will have routing. The configuration information is as follows:

import React from 'react' //Introducing React to support JSX
import { Route } from 'react-router-dom' //Introduce route
import Home from './containers/Home' //Introduce Home component

export default (
    <div>
        <Route path="/" exact component={Home}></Route>
    </div>
)

Then you can reference the routing information through index.js, as follows:

import React from 'react'
import ReactDom from 'react-dom'
import { BrowserRouter } from'react-router-dom'
import Router from'../Routers'

const App= () => {
    return (
        <BrowserRouter>
           {Router}
        </BrowserRouter>
    )
}

ReactDom.hydrate(<App/>, document.getElementById('root'))

At this time, there will be an error message in the console. The reason is that each Route component is wrapped with a layer of div, but this div is not included in the code returned by the server.

The solution is to execute the routing information once on the server, use StaticRouter instead of BrowserRouter, and pass parameters through context

import express from 'express'
import React from 'react'//Introducing React to support JSX syntax
import { renderToString } from 'react-dom/server'//Introduce renderToString method
import { StaticRouter } from 'react-router-dom'
import Router from '../Routers'
 
const app = express()
app.use(express.static('public'));
//Use the static middleware provided by express. The middleware will point the routing of all static files to the public folder.

app.get('/',(req,res)=>{
    const content = renderToString((
        //Pass in the current path
        //context is a required parameter, used for server-side rendering parameter transfer
        <StaticRouter location={req.path} context={<!-- -->{}}>
           {Router}
        </StaticRouter>
    ))
    res.send(`
   <html>
       <head>
           <title>ssr demo</title>
       </head>
       <body>
       <div id="root">${content}</div>
       <script src="/index.js"></script>
       </body>
   </html>
    `)
})


app.listen(3001, () => console.log('Exampleapp listening on port 3001!'))

This completes the server-side rendering of the route.

3. Principles

The overall react server-side rendering principle is not complicated, as follows:

node server receives the client request, gets the current request url path, then finds the corresponding component in the existing routing table, gets the data that needs to be requested, and Data is passed into the component as props, context or store

Then based on react‘s built-in server-side rendering method renderToString(), the component is rendered into a html string and the final htmlData needs to be injected into the browser before outputting

The browser starts rendering and node comparison, and then completes event binding and some interactions within the component. The browser reuses the html node output by the server, and the entire process ends.