文章出处: 拉 勾 大前端 高薪训练营
练习代码地址
renderToString
方法用于将 React 组件转换为 HTML 字符串,通过 react-dom/server
导入.
问题: Node 环境不支持 ESModule 模块系统,不支持 JSX 语法
"dev:server-build": "webpack --config webpack.server.js --watch"
"dev:server-run": "nodemon --watch build --exec\"node build/bundler.js\""
在客户端对组件进行二次“渲染”,为组件元素附加事件
使用 hydrate 方法对组件进行渲染,为组件元素附加事件。
hydrate 方法在实现渲染的时候,会复用原本已经存在的 DOM 节点,减少重新生成节点以及删除原本 DOM 节点的开销。
通过 react-dom 导入 hydrate
ReactDOM.hydrate( , document.getElementById('#root'))
webpack 配置
打包目的:转换 JSX 语法,转换浏览器不识别的高级 JavaScript 语法
打包目标位置:public文件夹
打包启动命令配置
"dev:client-build": "webpack --config webpack.client.js --watch"
在响应给客户端的 HTML 代码中添加 script 标签,请求客户端 JavaScript 打包文件。
<html>
<head>
<title> React SSRtitle>
head>
<body>
<div id="root">${content}div>
<script src="bundle.js">script>
body>
html>
服务器端程序实现静态资源访问功能,客户端 JavaScript 打包文件会被作为静态资源使用。
app.use(express.static('public'))
服务器端 webpack 配置和客户端 webpack 配置存在重复,将重复配置抽象到 webpack.base.js 配置文件中
目的:使用一个命令启动项目,解决多个命令启动的繁琐问题,通过 npm-run-all 工具实现。
"dev": "npm-run-all --parallel dev:*"
问题:在服务器端打包文件中,包含了 Node 系统模块,导致打包文件本身体积庞大。
解决:通过 webpack 配置剔除打包文件中的 Node 模块。
const nodeExternals = require('webpack-node-externals')
const config = {
externals: [nodeExternals()]
}
module.exports = merge(baseConfig, config)
优化代码组织方式,渲染 React 组件代码是独立功能,所以把它从服务器端入口文件中进行抽离。
在 React SSR 项目中需要实现两端路由。
客户端路由是用于支持用户通过点击链接的形式跳转页面。
服务器端路由是用于支持用户直接从浏览器地址栏中访问页面。
客户端和服务器端共用一套路由规则。
share/routes.js
import Home from './pages/Home'
import List from './pages/List'
export default [
{
path: '/',
component: Home,
exact: true
}, {
path: '/list',
component: List,
}
]
Express 路由接受所有 Get 请求,服务器端 React 路由通过请求路径匹配要进行渲染的组件
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(
{renderRoutes(routes)}
)
return `
React SSR
${content}
`
}
添加客户端路由配置
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(
{renderRoutes(routes)}
, document.getElementById('root'))
在实现了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'
]
}
}
}
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))
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 `
React SSR
${
content}
`
}
问题:服务器端创建的store 是空的,组件并不能从 Store 中获取到任何数据。
解决:服务器端在渲染组件之前获取到组件所需要的数据。
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 方法在对比的时候发现两者不-致, 所以报了个警告.
解决思路:将服务器端获取到的数据回填给客户端,让客户端拥有初始数据.
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 `
React SSR
${
content}
`
}
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
转移状态中的恶意代码
let response = {
data: [{
id: 1, name: ''}]
}
import serialize from 'serialize-javascript'
const initialState = serialize(store.getState())