笔记来源:拉勾教育 - 大前端就业集训营
文章内容:学习过程中的笔记、感悟、和经验
提示:项目实战类文章资源无法上传,仅供参考
ReactSSR - React服务器渲染
概述
- 客户端渲染(CSR):服务器端仅返回JSON数据,客户端接收到DATA后将数据与HTML进行组合然后渲染
- 服务器端渲染(SSR):服务器直接将需要使用的DATA数据和HTML进行组合,然后把组合后的HTML返回给客户端,客户端只需要直接使用即可
服务器端渲染存在问题
- 首屏等待时间长,用户体验差
- 页面结构为空,搜索引擎爬爬不到任何内容,不利于SEO
ReactSSR同构 - 服务器端渲染同构
- 同构指代码复用,即实现客户端和服务器端最大程度代码复用,客户端和服务器端都可以使用
项目结构初始化
- 项目结构
- src源代码文件夹
- client - 客户端代码目录
- server - 服务器端代码目录
- share - 同构代码目录
将课程提供的package.json
和package.lock.json
文件拷贝指导项目中,然后npm i
安装依赖
实现ReactSSR雏形
创建node服务器 - express
- 确定是否已经安装了express依赖
- server目录下创建http.js文件,在内部创建node服务器
// 引入依赖
import express from 'express';
// 创建node服务器实例对象
const app = express();
//
//app.use(express.static('public'));
// 监听3000端口,并在成功后打印内容
app.listen(3000, () => console.log('app is running on 3000 port'));
// 导出node
export default app;
- server目录下创建index.js文件,作为node服务器入口文件
// 引入服务器实例
import app from './http';
// 接受客户端发来的请求(请求对象、响应对象),请求地址为/
app.get('/', (req, res) => {
});
// 到目前为止,node服务器就创建好了
实现ReactSSR
总体步骤
- 引入需要渲染的React组件
- 通过renderToString方法将React组件转换为HTML字符串
- 将结果HTML字符串响应到客户端
renderToString用于将react组件转换为HTML字符串,通过react-dom/server导入
具体实现
-
因为这个组件时客户端与服务端通用的,所以属于同构代码,书写在share目录中
-
shart目录下创建pages目录放置页面组件
-
pages下面创建Home.js文件作为首页组件
import React, { Component } from 'react' // 首页组件 export class Home extends Component { render() { return <div>Home内容</div> } } export default Home
-
server/index.js文件中引入Home组件、renderToString方法
-
在app.get方法中使用renderToString转换Home组件,然后使用res.send返回HTML页面结构
// 引入服务器实例 import app from './http' import { renderToString } from 'react-dom/server' import Home from '../share/pages/Home' // 接受客户端发来的请求(请求对象、响应对象),请求地址为/ app.get('/', (req, res) => { // 将首页转换为字符串 const content = renderToString(<Home />) // 响应回去(直接返回HTML,把首页转换后的字符串插进去) res.send(` <html> <head> <title>ReactSSR</title> </head> <body> <div id='root'>${content}</div> </body> </html> `) })
服务端程序webpack打包配置
node环境下不支持ESModule模块系统和JSX语法,所以写要进行配置
-
在项目根目录下创建
webpack.server.js
文件// 引入path以便使用API const path = require('path') // 配置对象 module.exports = { // 生产环境:开发环境 mode: 'development', // 代码运行环境:node target: 'node', // 入口文件 entry: './src/server/index.js', // 导出 output: { // 打包路径:使用path的API进行路径拼接 path: path.join(__dirname, 'build'), // 打包文件名 filename: 'bundle.js', }, // 配置打包规则 module: { rules: [ { // js文件规则 test: /\.js$/, // 忽略node_modulse文件夹 exclude: /node_modulse/, use: { // 使用babel转换 loader: 'babel-loader', // 配置babel options: { // babel预制 presets: ['@babel/preset-env', '@babel/preset-react'], }, }, }, ], }, }
-
package.js
书写命令,执行打包......... "scripts": { // 执行webpack.server.js配置进行打包 "dev:server-build": "webpack --config webpack.server.js", }, .........
-
使用node执行打包后的文件
npm run dev:server-build
,此时应该会报错 -
在src/server/index.js中引入react让node支持jsx语法,避免报错
// 引入服务器实例 import app from './http' import { renderToString } from 'react-dom/server' import Home from '../share/pages/Home' // 引入react让程序支持jsx语法 import React from 'react' // 接受客户端发来的请求(请求对象、响应对象),请求地址为/ app.get('/', (req, res) => { // 将首页转换为字符串 const content = renderToString(<Home />) // 响应回去(直接返回HTML,把首页转换后的字符串插进去) res.send(` <html> <head> <title>ReactSSR</title> </head> <body> <div id='root'>${content}</div> </body> </html> `) })
-
package.js
添加自动化命令"scripts": { // 添加监听,一旦文件发生变化重新打包 "dev:server-build": "webpack --config webpack.server.js --watch", // 监听build文件夹,一旦发生变化重新执行node build/bundle.js "dev:server-run": "nodemon --watch build --exec \"node build/bundle.js\"" },
为组件添加事件
直接在组件上添加事件是无法生效的,查看源代码可以发现js代码不存在打包好的文件中
实现思路:在客户端对组件进行二次渲染,为组件元素添加事件
- 使用hydrate方法(通过react-dom引入)在客户端对组件进行二次渲染,为组件元素添加事件
- 原本我们使用的是render方法渲染,但是render无法复用已经生成的节点,而hydrate可以复用原本已经生成的DOM节点,节省性能开销
目标实现
- 客户端书写一个js文件,书写二次渲染代码
- 客户端react的JSX和高级js语法需要进行webpack配置才能运行,目标位置 - public目录
- 在相应给客户端的HTML代码中添加script标签,让其调用打包后的js文件
- 服务器端实现静态资源响应,客户端js打包文件作为静态资源使用
// src/share/pages/Home.js 组件添加点击事件
import React, { Component } from 'react'
// 首页组件
// export class Home extends Component {
// render() {
// return <div onClick={() => console.log('点击事件触发')}>Home内容</div>
// }
// }
function Home() {
// 添加点击事件,打印一段话
return <div onClick={() => console.log('点击事件触发')}>Home内容</div>
}
export default Home
// 老师使用的函数组件,未避免不必要报错这里改成函数组件,
// src/client/index.js 客户端目录新建文件书写二次渲染
import React from 'react'
import ReactDom from 'react-dom'
import Home from '../share/pages/Home'
// 使用hydrate在客户端进行二次渲染
// hydrate 参数1:目标组件,参数2:找到指定元素
ReactDom.hydrate(<Home />, document.getElementById('root'))
// webpack.client.js 书写客户端需要的webpack配置,这里修改下路径即可,在浏览器运行不需要写运行环境
// 引入path以便使用API
const path = require('path')
// 配置对象
module.exports = {
// 生产环境:开发环境
mode: 'development',
// 入口文件
entry: './src/client/index.js',
// 导出
output: {
// 打包路径:使用path的API进行路径拼接
path: path.join(__dirname, 'public'),
// 打包文件名
filename: 'bundle.js',
},
// 配置打包规则
module: {
rules: [
{
// js文件规则
test: /\.js$/,
// 忽略node_modulse文件夹
exclude: /node_modulse/,
use: {
// 使用babel转换
loader: 'babel-loader',
// 配置babel
options: {
// babel预制
presets: ['@babel/preset-env', '@babel/preset-react'],
},
},
},
],
},
}
// package.json 添加客户端打包命令
"scripts": {
"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\""
},
// src/server/index.js服务器端响应回的html结构添加script标签,调用客户端打包好的文件
// 引入服务器实例
import app from './http'
import { renderToString } from 'react-dom/server'
import Home from '../share/pages/Home'
// 引入react让程序支持jsx语法
import React from 'react'
// 接受客户端发来的请求(请求对象、响应对象),请求地址为/
app.get('/', (req, res) => {
// 将首页转换为字符串
const content = renderToString(<Home />)
// 响应回去(直接返回HTML,把首页转换后的字符串插进去)
res.send(`
<html>
<head>
<title>ReactSSR</title>
</head>
<body>
<div id='root'>${content}</div>
<script src='bundle.js'></script>
</body>
</html>
`)
})
// src/server/http.js 将上面打包文件作为静态文件相应给客户端
// 引入依赖
import express from 'express'
// 创建node服务器实例对象
const app = express()
// 将public文件夹作为静态资源使用
app.use(express.static('public'))
// 监听3000端口,并在成功后打印内容
app.listen(3000, () => console.log('app is running on 3000 port'))
// 导出node
export default app
组件书写事件 => 书写二次渲染代码 => 配置打包参数 => 配置打包命令 => (执行打包)=> 将打包文件作为静态文件使用 => 使用打包后的文件
优化代码
合并webpack配置
- 服务端和客户端webpack有重复配置项,将重复的配置抽象到一个文件中 - webpack.base.js
- 使用webpack-merge进行合并配置操作
// webpack.base.js 将重复的配置项单独提取一个文件
// 导出重复配置项
module.exports = {
// 生产环境:开发环境
mode: 'development',
// 配置打包规则
module: {
rules: [
{
// js文件规则
test: /\.js$/,
// 忽略node_modulse文件夹
exclude: /node_modulse/,
use: {
// 使用babel转换
loader: 'babel-loader',
// 配置babel
options: {
// babel预制
presets: ['@babel/preset-env', '@babel/preset-react'],
},
},
},
],
},
}
// webpack.client.js 使用提取的配置然后书写自己私有配置合并导出
// 引入path以便使用API
const path = require('path')
// 引入重复配置
const baseConfig = require('./webpack.base')
// 引入合并配置方法
const merge = require('webpack-merge')
// 配置对象,存到一个对象中
const config = {
// 入口文件
entry: './src/client/index.js',
// 导出
output: {
// 打包路径:使用path的API进行路径拼接
path: path.join(__dirname, 'public'),
// 打包文件名
filename: 'bundle.js',
},
}
// 合并导出新的配置
module.exports = merge(baseConfig, config)
// webpack.server.js 使用提取的配置然后书写自己私有配置合并导出
// 引入path以便使用API
const path = require('path')
// 引入重复配置
const baseConfig = require('./webpack.base')
// 引入合并配置方法
const merge = require('webpack-merge')
// 配置对象,存到一个对象中
const config = {
// 代码运行环境:node
target: 'node',
// 入口文件
entry: './src/server/index.js',
// 导出
output: {
// 打包路径:使用path的API进行路径拼接
path: path.join(__dirname, 'build'),
// 打包文件名
filename: 'bundle.js',
},
}
// 合并导出新的配置
module.exports = merge(baseConfig, config)
合并项目启动命令
当前想要启动项目需要开启三个命令行工具,都需要输入启动命令,这里将全部命令合并到一个命令中,解决多个启动命令繁琐问题
通过工具 - npm-run-all
实现,已经安装好,直接在package.json肿的scripts添加新命令即可
"scripts": {
// dev 执行 npm-run-all
// parallel 参数表示同时执行多个命令
// dev:* 表示所有以 dev: 开头的命令
"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\""
},
服务器端打包文件体积优化
- Q:文件打包时,包含了node系统模块,导致文件体积较大
- A:在webpack中,剔除打包文件中的node模块,因为服务端代码是运行在node环境的,没有必要再打包一份node模块
// 引入剔除方法,在配置中添加调用该方法即可
// 引入path以便使用API
const path = require('path')
// 引入重复配置
const baseConfig = require('./webpack.base')
// 引入合并配置方法
const merge = require('webpack-merge')
// 引入剔除打包 node 模块方法!!!!!!!!!!!!!!!!!!
const nodeExternals = require('webpack-node-externals')
// 配置对象,存到一个对象中
const config = {
// 代码运行环境:node
target: 'node',
// 入口文件
entry: './src/server/index.js',
// 导出
output: {
// 打包路径:使用path的API进行路径拼接
path: path.join(__dirname, 'build'),
// 打包文件名
filename: 'bundle.js',
},
// 调用方法执行剔除打包node模块!!!!!!!!!!!!!!!!
externals: [nodeExternals()],
}
// 合并导出新的配置
module.exports = merge(baseConfig, config)
代码拆分
- Q:将启动服务器代码喝渲染代码进行模块化拆分
- A:渲染react组件代码是独立的功能,所以单独存放到一个文件中
// src/server/renderer.js 单独抽离渲染代码
import { renderToString } from 'react-dom/server'
import Home from '../share/pages/Home'
// 引入react让程序支持jsx语法
import React from 'react'
// 直接导出一个函数
export default () => {
// 将首页转换为字符串
const content = renderToString(<Home />)
// 返回需要的HTML结构
return `
<html>
<head>
<title>ReactSSR</title>
</head>
<body>
<div id='root'>${content}</div>
<script src='bundle.js'></script>
</body>
</html>
`
}
// 引入渲染代码并使用
// 引入服务器实例
import app from './http'
// 引入渲染方法
import renderer from './renderer'
// 接受客户端发来的请求(请求对象、响应对象),请求地址为/
app.get('/', (req, res) => {
// 直接调用渲染组件方法使用其返回的HTML结构
res.send(renderer())
})
路由支持
实现思路
- 再React SSR项目中要实现两端路由,即客户端路由和服务器端路由
- 客户端路由:支持用户点击链接像是跳转页面
- 服务器端路由:支持用户直接从浏览器地址栏访问页面
- 两个路由要公用同一套路由规则 - 同构代码
服务器端路由实现
- Share/pages目录中新建两个组件
- Share目录下创建文件书写路由规则 - 对象格式,因为是同构代码所以放在Share中
- 修改服务器接收其他路由,并向下传递req(访问信息)
- 渲染代码使用路由 - StaticRouter,因为node不能直接使用对象作为渲染,使用renderRoutes将路由配置转换为组件
- 注意:测试时需要设置浏览器禁用Javascript,否则会出错
// src/share/pages/List 新建一个组件
import React from 'react'
function List() {
return <div>List内容</div>
}
export default List
// src/share/router.js 书写路由规则,因为是公共路由规则,放在share中
// 两个组件
import Home from './pages/Home'
import List from './pages/List'
// 路由规则
export default [
{
path: '/',
component: Home,
exact: true,
},
{
path: '/list',
component: List,
},
]
// src/server/index.js 修改接受路由,传递参数
// 引入服务器实例
import app from './http'
// 引入渲染方法
import renderer from './renderer'
// 接受客户端发来的请求(请求对象、响应对象)
// 修改可接收所有请求
app.get('*', (req, res) => {
// 直接调用渲染组件方法使用其返回的HTML结构,把req传递下去,req包含路由信息
res.send(renderer(req))
})
// src/server/renderer.js 渲染代码将原本的HOME替换为路由匹配组件
import { renderToString } from 'react-dom/server'
// 引入react让程序支持jsx语法
import React from 'react'
// 实现路由功能
import { StaticRouter } from 'react-router-dom'
// 路由配置信息
import router from '../share/router'
// 将路由配置对象转换为组件
import { renderRoutes } from 'react-router-config'
// 直接导出一个函数
export default req => {
// 将首页转换为字符串
const content = renderToString(
// 替换掉原本的Home组件,StaticRouter根据req.path匹配组件
// renderRoutes(router)将需要的组件从对象形式转换为组件
<StaticRouter location={req.path}>{renderRoutes(router)}</StaticRouter>
)
// 返回需要的HTML结构
return `
<html>
<head>
<title>ReactSSR</title>
</head>
<body>
<div id='root'>${content}</div>
<script src='bundle.js'></script>
</body>
</html>
`
}
客户端路由实现
- 直接修改客户端index文件,将原本的Home组件替换为路由匹配组件
- 在Home组件中添加一个链接点击跳转到list
// src/client/index.js
import React from 'react'
import ReactDom from 'react-dom'
import Home from '../share/pages/Home'
// 客户端路由匹配方法
import { BrowserRouter } from 'react-router-dom'
// 转换路由对象为组件
import { renderRoutes } from 'react-router-config'
// 路由规则
import router from '../share/router'
// 使用hydrate在客户端进行二次渲染
// hydrate 参数1:目标组件,参数2:找到指定元素
ReactDom.hydrate(
// BrowserRouter用于匹配规则。直接包裹需要使用的路由组件
// renderRoutes(router)把组件从对象转换为组件
<BrowserRouter>{renderRoutes(router)}</BrowserRouter>,
document.getElementById('root')
)
// src/share/pages/Home.js
import React, { Component } from 'react'
import { Link } from 'react-router-dom'
// 首页组件
// export class Home extends Component {
// render() {
// return <div onClick={() => console.log('点击事件触发')}>Home内容</div>
// }
// }
function Home() {
// 添加点击事件,打印一段话
return (
<div onClick={() => console.log('点击事件触发')}>
Home内容
{/* 添加一个跳转链接 */}
<Link to='/list'>跳转list</Link>
</div>
)
}
export default Home
redux支持
实现思路
-
React SSR项目中需要实现两端Redux
- 客户端Redux:之前学习过
- 服务器端Redux:服务器端搭建一套Redux代码,用于管理组件中的数据
-
客户端和服务器端可共用一套Reducer代码
-
但是创建Store代码由于参数传递不同所以不可共用,需要在两端单独创建
客户端Redux实现
- 客户端创建文件书写Store创建代码
- 在客户端index文件中传递store,使用thunk中间件
- reducer属于重构代码,所以写在share中(reducer、action)
- 分别创建reducr和action目录,书写响应代码
- reducer最终需要合并导出
- 可能会报错,因为浏览器默认不支持异步函数,可能需要设置一下bable预制器
// src/client/crateStore.js 新建文件创建store
import { createStore, applyMiddleware } from 'redux'
// 中间件使用thunk
import thunk from 'redux-thunk'
// 引入合并后的reducer
import Reducer from '../share/store/reducers'
// 创建store
const store = createStore(Reducer, {}, applyMiddleware(thunk))
export default store
// src/client/index.js 将创建store传递下去
import React from 'react'
import ReactDom from 'react-dom'
// 客户端路由匹配方法
import { BrowserRouter } from 'react-router-dom'
// 转换路由对象为组件
import { renderRoutes } from 'react-router-config'
// 路由规则
import router from '../share/router'
import { Provider } from 'react-redux'
import store from './crateStore'
// 使用hydrate在客户端进行二次渲染
// hydrate 参数1:目标组件,参数2:找到指定元素
ReactDom.hydrate(
// 传递store
<Provider store={store}>
{/* BrowserRouter用于匹配规则。直接包裹需要使用的路由组件
renderRoutes(router)把组件从对象转换为组件 */}
<BrowserRouter>{renderRoutes(router)}</BrowserRouter>
</Provider>,
document.getElementById('root')
)
// src/share/store/actions/user.action.js 新建文件创建action
import axios from 'axios'
// 保存用户action
export const saveUser = 'save_user'
// 获取用户指令
export const getUser = () => async dispatch => {
// 请求数据
const { data } = await axios.get('https://jsonplaceholder.typicode.com/users')
// 调用另一条action
dispatch({ type: saveUser, payload: data })
}
// src/share/store/reducers/user.reducer.js 新建文件创建reducer
import { saveUser } from '../actions/user.action'
// reducer
const userReducer = (state = [], action) => {
switch (action.type) {
case saveUser:
return action.payload
default:
return state
}
}
export default userReducer
// src/share/store/reducers/index.js 新建文件合并reducer
import { combineReducers } from 'redux'
import userReducer from './user.reducer'
// 合并reducer
export default combineReducers({
user: userReducer,
})
// src/share/pages/List.js 组件就挂在后获取数据并使用
import React, { useEffect } from 'react'
import { connect } from 'react-redux'
import { getUser } from '../store/actions/user.action'
function List({ user, dispatch }) {
// 挂载后触发指令获取数据
useEffect(() => {
dispatch(getUser())
}, [])
return (
<div>
List内容
<ul>
{/* 遍历用户创建结构 */}
{user.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
)
}
// 获取store数据
const data = state => ({
user: state.user,
})
export default connect(data)(List)
// webpack.base.js 修改公共webpack配置,以支持异步函数(浏览器默认不支持)
presets: [
// 改造第一个参数,设置支持异步函数
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
},
],
'@babel/preset-react',
],
服务器端Redux实现
-
store创建函数:新建文件创建store创建函数,后面接受请求时候才正式创建
-
配置store:
- 服务端接收到请求时创建store,然后把store传递给渲染逻辑代码
- 在渲染逻辑中将store传递下去
-
服务器端store数据填充:当前创建的store是空的,组件无法从store中获取数据,需要在服务器端渲染组件之前就获取需要使用的数据
- 在组件中添加一个loadData方法,用于获取组件所需要使用的数据,方法被服务器端调用
- 将loadData方法保存在当前组件路由信息对象中
- 服务器端接受请求后,根据地址匹配要渲染的路由信息
- 从路由信息中获取loadData方法并调用获取数据
- 数据获取完成后再进行组件渲染响应给客户端
-
消除警告:客户端store在初始状态下是没有数据的,在渲染组件的时候生成一个空的ul,但是服务器端是先获取数据才进行渲染的,所以生成的是有子元素的ul,hydrate在做对比的时候发现两者不一致,所以报警告,只需要将服务器端获取到的数据回填给客户端,让客户端也拥有初始数据
- 服务器端渲染之前获取一下store状态数据
- 然后把这个数据保存给window对象,注意字符串转换
- 客户端在创建store的时候吧window属性传递给第二个参数
// src/server/createStore.js 新建文件创建服务器端store创建函数
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
// 和客户端共用的reducer
import reducers from '../share/store/reducers'
// 导出一个函数,函数返回store
export default () => createStore(reducers, {}, applyMiddleware(thunk))
// src/server/index.js 服务端创建store并传递给渲染,进行渲染前准备工作
// 引入服务器实例
// 匹配路由规则信息
import { matchRoutes } from 'react-router-config'
// 路由规则
import router from '../share/router'
// store创建函数
import createStore from './createStore'
import app from './http'
// 引入渲染方法
import renderer from './renderer'
// 接受客户端发来的请求(请求对象、响应对象),请求地址为/
// 修改可接收多个路由
app.get('*', (req, res) => {
// 创建 store
const store = createStore()
// 获取全部路由信息,参数1:路由规则,参数2:匹配规则,匹配当前路由所有相关信息 - 数组
// 遍历所有匹配成功信息
// 最终返回一个Promise数组
const promises = matchRoutes(router, req.path).map(({ route }) => {
// 如果存在loadData则执行,参数store
if (route.loadData) return route.loadData(store)
})
// 遍历Promise数组,当能执行到then时候说明没问题,继续向下执行渲染
Promise.all(promises).then(() => {
// 直接调用渲染组件方法使用其返回的HTML结构,把req传递下去,req包含路由信息
res.send(renderer(req, store))
})
})
//src/server/renderer.js 渲染逻辑向下传递接受的store,并存储初始store
import { renderToString } from 'react-dom/server'
// 引入react让程序支持jsx语法
import React from 'react'
// 实现路由功能
import { StaticRouter } from 'react-router-dom'
// 路由配置信息
import router from '../share/router'
// 将路由配置对象转换为组件
import { renderRoutes } from 'react-router-config'
import { Provider } from 'react-redux'
// 直接导出一个函数
export default (req, store) => {
// 获取当前的store内容
const myState = store.getState()
// 将首页转换为字符串
const content = renderToString(
// 替换掉原本的Home组件,StaticRouter根据req.path匹配组件
// renderRoutes(router)将需要的组件从对象形式转换为组件
// Provider传递store
<Provider store={store}>
<StaticRouter location={req.path}>{renderRoutes(router)}</StaticRouter>
</Provider>
)
// 返回需要的HTML结构,将获取到的store内容存储到window中,给客户端使用,避免报警
return `
<html>
<head>
<title>ReactSSR</title>
</head>
<body>
<div id='root'>${content}</div>
<script>window.myState=${JSON.stringify(myState)}</script>
<script src='bundle.js'></script>
</body>
</html>
`
}
// src/share/pages/List.js 组件创建方法,供服务端调用初始化store
import React, { useEffect } from 'react'
import { connect } from 'react-redux'
// action
import { getUser } from '../store/actions/user.action'
function List({ user, dispatch }) {
// 挂载后触发指令获取数据
useEffect(() => {
dispatch(getUser())
}, [])
return (
<div>
List内容
<ul>
{/* 遍历用户创建结构 */}
{user.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
)
}
// 获取store数据
const data = state => ({
user: state.user,
})
// 创建函数,给服务端调用,服务端可以直接调用这个方法进行store操作
// 参数 - store
function loadData(store) {
// 直接调用action获取数据
return store.dispatch(getUser())
}
// 返回数组,是结构更清晰
export default { component: connect(data)(List), loadData }
// src/share/router.js 路由规则修改list组件规则
// 两个组件
import Home from './pages/Home'
import List from './pages/List'
// 路由规则
export default [
{
path: '/',
component: Home,
exact: true,
},
{
path: '/list',
// 这里直接展开List
...List,
},
]
// src/client/crateStore.js 服务端store创建时直接使用服务端存储的store
import { createStore, applyMiddleware } from 'redux'
// 中间件使用thunk
import thunk from 'redux-thunk'
// 引入合并后的reducer
import Reducer from '../share/store/reducers'
// 创建store,第二个参数使用服务端存储在window伤的数据,避免报警
const store = createStore(Reducer, window.myState, applyMiddleware(thunk))
export default store
防止XSS攻击
- 当服务器端返回的数据中有恶意的js代码,这些代码可能会被浏览器渲染
- 通过serialize-javascript对数据进行处理,既可避免
- 在服务器端获取到的store数据进行处理,然后使用处理后的结果
// src/server/renderer.js 存储store的时候对数据进行处理,避免XSS攻击
import { renderToString } from 'react-dom/server'
// 引入react让程序支持jsx语法
import React from 'react'
// 实现路由功能
import { StaticRouter } from 'react-router-dom'
// 路由配置信息
import router from '../share/router'
// 将路由配置对象转换为组件
import { renderRoutes } from 'react-router-config'
import { Provider } from 'react-redux'
import serialize from 'serialize-javascript'
// 直接导出一个函数
export default (req, store) => {
// 获取当前的store内容,使用serialize处理store避免XSS攻击
const myState = serialize(store.getState()) // const myState = JSON.stringify(store.getState())
// 将首页转换为字符串
const content = renderToString(
// 替换掉原本的Home组件,StaticRouter根据req.path匹配组件
// renderRoutes(router)将需要的组件从对象形式转换为组件
// Provider传递store
<Provider store={store}>
<StaticRouter location={req.path}>{renderRoutes(router)}</StaticRouter>
</Provider>
)
// 返回需要的HTML结构,将获取到的store内容存储到window中,给客户端使用,避免报警
return `
<html>
<head>
<title>ReactSSR</title>
</head>
<body>
<div id='root'>${content}</div>
<script>window.myState=${myState}</script>
<script src='bundle.js'></script>
</body>
</html>
`
}