React natively implements server-side rendering
Article source: Lagou big front-end high-paying training camp
1. Server-side rendering starts quickly
1. Implement React SSR
- Introduce the React components to be rendered
- Convert React components to HTML strings through the renderToString method
- Respond the resulting HTML string to the client
renderToString
React assembly method for converting an HTML string, by react-dom/server
introduction.
2. Webpack packaging configuration
Problem: Node environment does not support ESModule module system, and does not support JSX syntax
3. Project startup command configuration
- Configure server-side packaging commands:
"dev:server-build": "webpack --config webpack.server.js --watch"
- 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
-
webpack configuration
Packaging purpose: convert JSX syntax, convert advanced JavaScript syntax not recognized by browsers.
Package destination: public folder -
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
- 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
- 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
- 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))
- 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.
- 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
- Save the loadData method in the routing information object of the current component.
- After the server receives the request, it matches the routing information of the component to be rendered according to the request address
- Obtain the loadData method in the component from the routing information and call the method to obtain the data required by the component
- 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.
- 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>
`
}
- 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())