React 原生实现服务端渲染

React 原生实现服务端渲染

文章出处: 拉 勾 大前端 高薪训练营

练习代码地址

一、服务器端渲染快速开始

1. 实现 React SSR

  1. 引入要渲染的 React 组件
  2. 通过 renderToString 方法 将 React 组件转换为 HTML字符串
  3. 将结果 HTML 字符串响应到客户端

renderToString 方法用于将 React 组件转换为 HTML 字符串,通过 react-dom/server 导入.

2. webpack 打包配置

问题: Node 环境不支持 ESModule 模块系统,不支持 JSX 语法

3. 项目启动命令配置

  1. 配置服务器端打包命令: "dev:server-build": "webpack --config webpack.server.js --watch"
  2. 配置服务端启动命令: "dev:server-run": "nodemon --watch build --exec\"node build/bundler.js\""

二、客户端 React 附加事件

1. 实现思路分析

在客户端对组件进行二次“渲染”,为组件元素附加事件

2. 客户端二次“渲染” hydrate

使用 hydrate 方法对组件进行渲染,为组件元素附加事件。
hydrate 方法在实现渲染的时候,会复用原本已经存在的 DOM 节点,减少重新生成节点以及删除原本 DOM 节点的开销。
通过 react-dom 导入 hydrate

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

3. 客户端 React 打包配置

  1. webpack 配置
    打包目的:转换 JSX 语法,转换浏览器不识别的高级 JavaScript 语法
    打包目标位置:public文件夹

  2. 打包启动命令配置

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

4. 添加客户端包文件请求链接

在响应给客户端的 HTML 代码中添加 script 标签,请求客户端 JavaScript 打包文件。

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

5. 服务器端实现静态资源访问

服务器端程序实现静态资源访问功能,客户端 JavaScript 打包文件会被作为静态资源使用。

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

三、优化

1. 合并 webpack 配置

服务器端 webpack 配置和客户端 webpack 配置存在重复,将重复配置抽象到 webpack.base.js 配置文件中

2. 合并项目启动命令

目的:使用一个命令启动项目,解决多个命令启动的繁琐问题,通过 npm-run-all 工具实现。

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

3. 服务器端打包文件体积优化

问题:在服务器端打包文件中,包含了 Node 系统模块,导致打包文件本身体积庞大。
解决:通过 webpack 配置剔除打包文件中的 Node 模块。

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

4. 将启动服务器代码和渲染代码进行模块化拆分

优化代码组织方式,渲染 React 组件代码是独立功能,所以把它从服务器端入口文件中进行抽离。

四、路由支持

1. 实现思路分析

在 React SSR 项目中需要实现两端路由。
客户端路由是用于支持用户通过点击链接的形式跳转页面。
服务器端路由是用于支持用户直接从浏览器地址栏中访问页面。
客户端和服务器端共用一套路由规则。

2. 编写路由规则

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. 实现服务器端路由

  1. Express 路由接受任何请求
    Express 路由接受任何请求

Express 路由接受所有 Get 请求,服务器端 React 路由通过请求路径匹配要进行渲染的组件

  1. 服务器端路由配置
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. 实现客户端路由

添加客户端路由配置

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'))

五、Redux 支持

1. 实现思路分析

在实现了React SSR 的项目中需要实现两端 Redux.
客户端 Redux 就是通过客户端 JavaScript 管理 Store 中的数据.
服务器端 Redux 就是在服务器端搭建一套 Redux 代码,用于管理组件中的数据.
客户端和服务器端共用一套 Reducer 代码.
创建 Store 的代码由于参数传递不同所以不可以共用.

创建异步 dispatch 时报错,因为浏览器默认不支持异步函数

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 开启 polyfill 支持:

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

2. 实现服务器端 Redux

  1. 创建 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. 配置 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. 服务器端 store 数据填充

问题:服务器端创建的store 是空的,组件并不能从 Store 中获取到任何数据。
解决:服务器端在渲染组件之前获取到组件所需要的数据。

  1. 在组件中添加loadData方法,此方法用于获取组件所需数据,方法被服务器端调用
  2. 将loadData方法保存在当前组件的路由信息对象中.
  3. 服务器端在接收到请求后,根据请求地址匹配出要渲染的组件的路由信息
  4. 从路由信息中获取组件中的loadData方法并调用方法获取组件所需数据
  5. 当数据获取完成以后再渲染组件并将结果响应到客户端

4. React 警告消除

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)

警告原因:客户端 Store 在初始状态下是没有数据的,在渲染组件的时候生成的是空 ul ,但是服务器端是先获取数据再进行的组件渲染,
所以生成的是有子元素的 ul , hydrate 方法在对比的时候发现两者不-致, 所以报了个警告.
解决思路:将服务器端获取到的数据回填给客户端,让客户端拥有初始数据.

  1. 服务器响应 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";
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. 客户端设置 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. 防止 XSS 攻击

转移状态中的恶意代码

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

const initialState = serialize(store.getState())

猜你喜欢

转载自blog.csdn.net/jal517486222/article/details/112798533