React natively implements server-side rendering

React natively implements server-side rendering

Article source: Lagou big front-end high-paying training camp

Practice code address

1. Server-side rendering starts quickly

1. Implement React SSR

  1. Introduce the React components to be rendered
  2. Convert React components to HTML strings through the renderToString method
  3. Respond the resulting HTML string to the client

renderToStringReact assembly method for converting an HTML string, by react-dom/serverintroduction.

2. Webpack packaging configuration

Problem: Node environment does not support ESModule module system, and does not support JSX syntax

3. Project startup command configuration

  1. Configure server-side packaging commands: "dev:server-build": "webpack --config webpack.server.js --watch"
  2. Configure the server start command: "dev:server-run": "nodemon --watch build --exec\"node build/bundler.js\""

Two, client-side React additional events

1. Realization analysis

Perform secondary "rendering" of the component on the client side and attach events to the component elements

2. The second "rendering" hydrate of the client

Use the hydrate method to render the component and attach events to the component elements.
The hydrate method reuses the existing DOM nodes when rendering, reducing the overhead of regenerating nodes and deleting the original DOM nodes.
Import hydrate through react-dom

ReactDOM.hydrate(<Home/>, document.getElementById('#root'))

3. Client React packaging configuration

  1. webpack configuration
    Packaging purpose: convert JSX syntax, convert advanced JavaScript syntax not recognized by browsers.
    Package destination: public folder

  2. Package startup command configuration

"dev:client-build": "webpack --config webpack.client.js --watch"

4. Add client package file request link

Add a script tag to the HTML code in the response to the client, requesting the client JavaScript to package the file.

  <html>
    <head>
      <title> React SSR</title>
    </head>
    <body>
      <div id="root">${content}</div>
      <script src="bundle.js"></script>
    </body>
  </html>

5. Server-side access to static resources

The server-side program implements the static resource access function, and the client-side JavaScript packaged file will be used as a static resource.

app.use(express.static('public'))

Three, optimization

1. Merge webpack configuration

The server-side webpack configuration and the client-side webpack configuration are duplicated, and the duplicate configuration is abstracted into the webpack.base.js configuration file

2. Merge project start command

Purpose: Use one command to start the project, solve the cumbersome problem of multiple commands to start, through the npm-run-all tool.

"dev": "npm-run-all --parallel dev:*"

3. Server-side packaged file volume optimization

Problem: The server-side package file contains Node system modules, which causes the package file itself to be huge.
Solution: Eliminate the Node module in the package file through webpack configuration.

const nodeExternals = require('webpack-node-externals')
const config = {
    
    
  externals: [nodeExternals()]
}
module.exports = merge(baseConfig, config)

4. Modularly split the startup server code and rendering code

Optimize code organization. Rendering React component code is an independent function, so it is removed from the server-side entry file.

Four, routing support

1. Realization analysis

In the React SSR project, routing at both ends needs to be implemented.
Client-side routing is used to support users to jump to pages by clicking on links.
Server-side routing is used to support users to access pages directly from the browser address bar.
The client and server share a set of routing rules.

2. Write routing rules

share/routes.js

import Home from './pages/Home'
import List from './pages/List'

export default [
  {
    
    
    path: '/',
    component: Home,
    exact: true
  }, {
    
    
    path: '/list',
    component: List,
  }
]

3. Implement server-side routing

  1. Express routing accepts any request
    Express routing accepts any request

Express routing accepts all Get requests, and server-side React routing matches the components to be rendered through the request path

  1. Server-side routing configuration
import React from 'react'
import {renderToString} from 'react-dom/server'
import { StaticRouter } from "react-router-dom";
import routes from '../share/routes'
import { renderRoutes } from "react-router-config";

export default (req) => {
  const content = renderToString(
    <StaticRouter location={req.path}>
      {renderRoutes(routes)}
    </StaticRouter>
  )
  return `
  <html>
    <head>
      <title> React SSR</title>
    </head>
    <body>
      <div id="root">${content}</div>
      <script src="bundle.js"></script>
    </body>
  </html>
  `
}

4. Implement client routing

Add client routing configuration

import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from "react-router-dom"
import { renderRoutes } from "react-router-config";
import routes from '../share/routes'

ReactDOM.hydrate(
  <BrowserRouter>
    {renderRoutes(routes)}
  </BrowserRouter>
  , document.getElementById('root'))

Five, Redux support

1. Realization analysis

In projects that implement React SSR, you need to implement Redux at both ends. The
client Redux is to manage the data in the Store through client-side JavaScript. The
server-side Redux is to build a set of Redux code on the server to manage the data in the component.
Client Both the server and the server share a set of Reducer code. The code for
creating the Store cannot be shared due to different parameter passing.

An error is reported when creating an asynchronous dispatch, because the browser does not support asynchronous functions by default

Uncaught ReferenceError: regeneratorRuntime is not defined
at eval (user.action.js:17)
at fetchUser (user.action.js:44)
at eval (List.js:16)
at invokePassiveEffectCreate (react-dom.development.js:23482)
at HTMLUnknownElement.callCallback (react-dom.development.js:3945)
at Object.invokeGuardedCallbackDev (react-dom.development.js:3994)
at invokeGuardedCallback (react-dom.development.js:4056)
at flushPassiveEffectsImpl (react-dom.development.js:23569)
at unstable_runWithPriority (scheduler.development.js:646)
at runWithPriority$1 (react-dom.development.js:11276)

Babel opens polyfill support:

{
    
    
  test: /\.js$/,
  exclude: /node_modules/,
  use: {
    
    
    loader: 'babel-loader',
    options: {
    
    
      presets: [
        [
          '@babel/preset-env',
          {
    
    
            useBuiltIns: 'usage'
          }
        ],
        '@babel/preset-react'
      ]
    }
  }
}

2. Realize server-side Redux

  1. Create Store

server/createStore.js

import {
    
     createStore, applyMiddleware } from "redux";
import thunk from 'redux-thunk'
import reducer from '../share/store/reducers'

export default () => createStore(reducer, {
    
    }, applyMiddleware(thunk))
  1. Configure Store

server/index.js

import app from './http'
import renderer from './renderer'
import createStore from './createStore'

app.get('*', (req, res) => {
    
    
  const store = createStore()
  res.send(renderer(req, store))
})

server/renderer.js

import React from 'react'
import {
    
    renderToString} from 'react-dom/server'
import {
    
     StaticRouter } from "react-router-dom";
import routes from '../share/routes'
import {
    
     renderRoutes } from "react-router-config";
import {
    
     Provider } from "react-redux";

export default (req, store) => {
    
    
  const content = renderToString(
    <Provider store={
    
    store}>
      <StaticRouter location={
    
    req.path}>
        {
    
    renderRoutes(routes)}
      </StaticRouter>
    </Provider>
  )
  return `
  <html>
    <head>
      <title> React SSR</title>
    </head>
    <body>
      <div id="root">${
      
      content}</div>
      <script src="bundle.js"></script>
    </body>
  </html>
  `
}

3. Server-side store data filling

Problem: The store created on the server side is empty, and the component cannot get any data from the store.
Solution: The server obtains the data required by the component before rendering the component.

  1. Add the loadData method to the component, this method is used to obtain the data required by the component, the method is called by the server
  2. Save the loadData method in the routing information object of the current component.
  3. After the server receives the request, it matches the routing information of the component to be rendered according to the request address
  4. Obtain the loadData method in the component from the routing information and call the method to obtain the data required by the component
  5. When the data acquisition is complete, render the component and respond to the client with the result

4. React warning elimination

react-dom.development.js:67 Warning: Did not expect server HTML to contain a

  • in
    • .
      at ul
      at div
      at List (webpack://react-ssr/./src/share/pages/List.js?:19:19)
      at Connect(List) (webpack://react-ssr/./node_modules/react-redux/es/components/connectAdvanced.js?:231:68)
      at Route (webpack://react-ssr/./node_modules/react-router/esm/react-router.js?:464:29)
      at Switch (webpack://react-ssr/./node_modules/react-router/esm/react-router.js?:670:29)
      at Router (webpack://react-ssr/./node_modules/react-router/esm/react-router.js?:93:30)
      at BrowserRouter (webpack://react-ssr/./node_modules/react-router-dom/esm/react-router-dom.js?:59:35)
      at Provider (webpack://react-ssr/./node_modules/react-redux/es/components/Provider.js?:16:20)

Reason for warning: The client store has no data in the initial state. When rendering the component, it generates an empty ul, but the server side obtains the data first and then performs the component rendering,
so the generated ul, hydrate with child elements When the method was compared, it was found that the two are not consistent, so a warning was reported.
Solution: Backfill the data obtained by the server to the client, so that the client has the initial data.

  1. Server responds to Store initial state

server/renderer.js

import React from 'react'
import {
    
    renderToString} from 'react-dom/server'
import {
    
     StaticRouter } from "react-router-dom";
import routes from '../share/routes'
import {
    
     renderRoutes } from "react-router-config";
import {
    
     Provider } from "react-redux";
import serialize from 'serialize-javascript'

export default (req, store) => {
    
    
  const content = renderToString(
    <Provider store={
    
    store}>
      <StaticRouter location={
    
    req.path}>
        {
    
    renderRoutes(routes)}
      </StaticRouter>
    </Provider>
  )
  const initialState = JSON.stringify(JSON.parse(serialize(store.getState())))
  return `
  <html>
    <head>
      <title> React SSR</title>
    </head>
    <body>
      <div id="root">${
      
      content}</div>
      <script>window.INITIAL_STATE = ${
      
      initialState} </script>
      <script src="bundle.js"></script>
    </body>
  </html>
  `
}
  1. The client sets the initial state of the Store

client/createStore.js

import {
    
     createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import reducer from '../share/store/reducers'

const store = createStore(reducer, window.INITIAL_STATE, applyMiddleware(thunk))

export default store

4. Prevent XSS attacks

Malicious code in the transition state

let response = {
    
    
  data: [{
    
    id: 1, name: '<script>alert(1)</script>'}]
}
import serialize from 'serialize-javascript'

const initialState = serialize(store.getState())

Guess you like

Origin blog.csdn.net/jal517486222/article/details/112798533