React native implementation of SSR

Posted by Bjom on Thu, 03 Mar 2022 15:01:38 +0100

Native implementation of React server rendering

github project code address

How to implement ssr

1. Create a node service instance

  • express.static('public ') specifies the access path of static files. For example, when accessing the src attribute of script in html, this folder will be used as the root path
// Create node service instances to facilitate application in other places
import express from 'express'
const app = express()
app.use(express.static('public'))
// The service runs on port 3000
app.listen(3000, ()=> console.log('app is running on 3000 port'))
export default app

2. Process the request according to the route

  • First, you need to know the routing address of the request
  • Match the currently configured route according to the requested address
  • After getting the configured route, get the data required by the corresponding component of the route
  • After getting the data, you can render
    • '/': the entry file of the server-side application, '*': receive all routing addresses
    • req.path: get request address
    • routes: get route configuration information
app.get('*',(req,res)=>{
    const store = createStore()
    const promise = matchRoutes(routes,req.path).map(({route})=>{
        if(route.loadData) return route.loadData(store)
    })
    Promise.all(promise).then(()=>{
        // Here we know that the data acquisition is completed and an html content is returned
        res.send(render(req,store))
    })
})
  • The following is the dual end routing configuration. The attribute loadData is a method defined in the component to obtain page data
import List from "./pages/List";
export default [
		...
    {
        path:'/List',
        component:List.component,
        loadData:List.loadData
    }
]
  • The following is the List page of double ended components
import React,{useEffect} from "react";
import { connect } from "react-redux";
import { fetchUser } from "../store/actions/user.action";
// The common components of client and server belong to isomorphic code
function List ({user,dispatch}) {
    useEffect(() => {
        dispatch(fetchUser())
    }, []);
    return <div>
                <ul>
                    {user.map(item=><li key={item.id}>{item.name}</li>)}
                </ul>
            </div>
}
function loadData (store) {
    return store.dispatch(fetchUser())
}
const mapStateToProps = (state) => ({user: state.user});
export default {
    component: connect(mapStateToProps)(List),
    loadData
}

3. What does render do after the data request is completed?

  • You need to return an html page with content
  • script is required in this page to replace ssr page
  • Initialization data is required in this page to avoid warning problems in hydrate comparison
  • To convert initialization data, xss attack should be prevented
    • The return result of renderToString is the string after converting the component into html
    • renderRoutes: returns the routing rules in the form of components
    • StaticRouter: convert routing rules in array form into routing rules in component form
    • serialize: convert data to prevent xss attacks
export default (req,store) => {
    const content = renderToString(
        <Provider store={store}>
            <StaticRouter location={req.path}>
                {renderRoutes(routes)}
            </StaticRouter>
        </Provider>
    )
    const initalState = serialize(store.getState());
    return `
        <html>
            <head>
                <title>React SSR</title>
            </head>
            <body>
                <div id="root">${content}</div>
                <script>window.INITIAL_STATE = ${initalState}</script>
								// Client succeeding ssr page
                <script src="bundle.js"></script>
            </body>
        </html>
    `
}
  • When the browser script runs, the initialized data is the data window generated by the server rendering INITIAL_ STATE
const store = createStore(reducers, window.INITIAL_STATE, applyMiddleware(thunk))

II What are the configurations of webpack

1. Public configuration webpack base. js

// The public configuration of webpack can be merge d with other configurations where necessary
const path = require('path')
module.exports = {
    mode:'development', // development environment 
    module: {
        rules: [{
            test:/\.js$/, // Match js ending file
            exclude:/node_modules/, // Exclude node_modules file
            use:{
                loader:'babel-loader', //Use the Babel loader loader to handle js ending file
                options:{// Options: related configuration items of Babel loader
                    presets:[
                        ["@babel/preset-env",{
                            useBuiltIns:"usage" // This configuration enables the browser to support asynchronous functions
                        }], // @Babel / preset env preset: converts high-level js syntax
                        "@babel/preset-react", // @Babel / preset react preset: converting jsx syntax
                    ]
                }
            }
        }]
    }
}

2. The client packages and configures webpack client. js

const path = require('path')
const merge = require('webpack-merge')
const baseConfig = require('./webpack.base')
const config  = {
    entry:'./src/client/index.js', //Entry file
    output: {
        // __ dirname: project root path, path: export file path: under the build folder under the project root path,
        path: path.join(__dirname,'public'),
        // filename: package file name
        filename:'bundle.js',
    },
}
// Merge public configuration and export
module.exports = merge(baseConfig,config)

3. The server is packaged and configured with webpack server. js

  • nodeExternals(): the server - side package file contains the Node system module As a result, the packaged file itself is bulky, and the Node module in the packaged file can be eliminated through webpack configuration
const path = require('path')
const merge = require('webpack-merge')
const baseConfig = require('./webpack.base')
const nodeExternals = require('webpack-node-externals')
const config = {
    target:'node', //Apply to node
    entry:'./src/server/index.js', //Entry file
    output: {
        // __ dirname: project root path, path: export file path: under the build folder under the project root path,
        path: path.join(__dirname,'build'),
        // filename: package file name
        filename:'bundle.js',
    },
    externals:[nodeExternals()]
}
// Merge public configuration and export
module.exports = merge(baseConfig,config)

4. One command realizes the packaging of client and server

  • npm run dev merge project startup command executes all commands starting with 'dev:'
    • Dev: server build monitors the changes of the entry configuration file of the service webpack and packages the code
    • Dev: client build monitors the changes of the entry configuration file of the client webpack and packages the code
    • Dev: server run listens to the code packaged by the server and executes the packaged file. The port number is 3000
"scripts": {
  "dev": "npm-run-all --parallel dev:*",
  "dev:server-build": "webpack --config webpack.server.js --watch",
  "dev:client-build": "webpack --config webpack.client.js --watch",
  "dev:server-run": "nodemon --watch build --exec \"node build/bundle.js\"",
},

III Which codes can be isomorphic

  • Pages component pages can be isomorphic, because they are rendered in the same way both on the server side and on the client side
  • actions and reducers in state management can be isomorphic Because the logic of processing data can be reused However, it is inconvenient to reuse createStore during initialization, because it is troublesome to ensure that the data state of the client and server is consistent after being taken over by the client
  • Routes can be isomorphic and configured in the form of arrays
    • Match or convert with matchRoutes and StaticRouter respectively in the server and client to obtain the corresponding page components