代码仓:React SSR原生实现
CSR:Client Side Rendering,服务器端仅返回JSON数据,DATA和HTML在客户端进行渲染
SSR : Server Side Rendering,服务器端返回HTML,DATA和HTML在服务器端进行渲染
src目录下新建三个文件夹client(客户端代码),server(服务器端代码),share(同构代码)
服务端渲染
1. 客户端渲染存在的问题
- 首屏等待时间长,用户体验差
- 页面结构为空,不利于SEO
客户端渲染需要浏览器先请求后端HTML,然后在下载JS,执行完JS并将接口请求的数据填充,最后才能将页面显示在浏览器,用户存在大量的白屏时间
服务端渲染是浏览器发送请求,服务器将HTML和数据拼接好返回给客户端,浏览器执行服务器返回的结果,此时用户可以看到静态的页面,然后在下载JS,执行完JS页面就有了动态效果了。由于服务端返回的是完整的HTML文件,所以搜索引擎能爬取到相应的内容
React SSR同构:同构指的是代码复用,即实现客户端和服务器端最大程度的代码复用;
2. webpack打包配置
Node环境不支持ESModule模块化,也不支持JSX语法,需要借助webpack打包
// webpack.server.js
const path = require('path')
module.exports = {
mode: 'development',
target: 'node',
entry: './src/server/index.js',
output: {
path: path.join(__dirname, 'build'),
filename: 'bundle.js'
},
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"]
}
}
}]
}
}
3. 客户端React附加事件
在客户端对组件进行二次渲染,为组件元素附加事件;
hydrate方法在实现渲染的时候,会复用原本已经存在的DOM节点,减少重新生成节点以及删除原本DOM节点的开销
ReactDom.hydrate(<App />, document.querySelector("#root")
优化:
-
合并webpack文件代码
-
问题:优化文件体积,bundle文件太大了,因为打包的时候会把系统模块一起打包进去,在本地运行的时候,这些模块不需要一起打包进去;
解决:利用webpack的externals属性来解决;
// webpack.server.js const nodeExternals = require('webpack-node-externals') const config = { ..., externals: [nodeExternals()] }
-
问题:渲染React组件代码时独立功能,所以把它从服务器端入口文件中进行抽离
解决:将渲染的代码抽离出去
4. 实现服务器路由
在React SSR项目中需要实现两端路由
客户端路由是用于支持用户通过点击链接的形式跳转页面
服务器端路由是由于支持用户直接从浏览器地址栏中访问页面
客户端和服务器端公用一套路由规则
// 服务器端路由
// renderer.js
import React from 'react'
import { renderToString } from 'react-dom/server'
import { StaticRouter } from 'react-router-dom'
import { renderRoutes } from 'react-router-config'
import routes from '../share/routes'
export default (req) => {
const content = renderToString(
<StaticRouter location={req.path}>
{renderRoutes(routes)}
</StaticRouter>
)
return `
<html>
<head>
<title>React SSR</title>
<body>
<div id="root">${content}</div>
<script src="bundle.js"></script>
</body>
</head>
</html>
`
}
// routes.js
import Home from "./pages/Home";
import List from "./pages/List";
export default [{
path: '/',
component: Home,
exact: true
}, {
path: '/list',
component: List
}]
// 客户端路由
// client/index.js
import React from "react";
import ReactDOM from "react-dom";
import Home from "../share/pages/Home";
import routes from '../share/routes';
import { renderRoutes } from 'react-router-config';
import { BrowserRouter } from 'react-router-dom';
ReactDOM.hydrate(
<BrowserRouter>{renderRoutes(routes)}</BrowserRouter>,
document.getElementById("root")
)
// share/pages/Home.js
import React from "react";
import { Link } from 'react-router-dom'
function Home () {
return <div onClick={() => console.log('hello')}>
Home works
<Link to="/list">jump to list</Link>
</div>
}
export default Home
5. 实现Redux支持
在实现了React SSR的项目中需要实现两端Redux
客户端Redux就是通过客户端JS管理Store中的数据
服务器端Redux就是在服务器端搭建一套Redux代码,用于管理组件中的数据
客户端和服务器端公用一套Reducer代码
创建Store的代码由于参数传递不同所以不可以公用
注意:创建异步dispatch时报错,浏览器默认不支持异步函数,需要配置babel的polyfill支持
options: {
presets: [["@babel/preset-env", {
useBuiltIns: 'usage'
}], "@babel/preset-react"]
}
服务器端store数据填充
问题:服务器端创建的store是空的,组件并不能从Store中获取到任何数据
解决:服务器端在渲染组件之前获取到组件所需要的数据
- 在组件中添加loadData方法,此方法有浓郁获取组件所需数据,方法被服务器端调用
- 将loadData方法保存在当前组件的路由信息对象中
- 服务器端在接收到请求后,根据请求地址匹配出要渲染的组件的路由信息
- 从路由信息中获取组件中的loadData方法并调用方法获取组件所需数据
- 当数据获取完成以后再渲染组件并将结果响应到客户端
主要思路:借助路由信息来调用dispatch获取组件所需要的数据,在响应到客户端之前先获取渲染到组件
// server/index.js
import app from './http'
import renderer from './renderer'
import createStore from './createStore'
import routes from '../share/routes'
import { matchRoutes } from 'react-router-config'
app.get('*', (req, res) => {
const store = createStore()
// 1.请求地址 req.path
// 2.获取到路由配置信息 routes
// 3.根据请求地址匹配出要渲染的组件的路由对象信息
const promises = matchRoutes(routes, req.path).map(({route}) => {
if (route.loadData) return route.loadData(store)
})
Promise.all(promises).then(() => {
res.send(renderer(req, store))
})
})
React警告消除
警告原因:客户端Store在初始状态下是没有数据的,在渲染组件的时候生成的是空ul,但是服务器端是先获取数据再进行的组件渲染,所以生成的是有子元素的ul,hydrate方法在对比的时候发现两者不一致,所以报了个警告;
解决思路:将服务器端获取到的数据回填给客户端,让客户端拥有初始数据;
export default (req, store) => {
// 将对象先转换为字符串
const initialState = JSON.stringify(store.getState())
return `
<html>
<head>
<title>React SSR</title>
<body>
<div id="root">${content}</div>
<script>window.INITIAL_STATE = ${initialState}</script>
<script src="bundle.js"></script>
</body>
</head>
</html>
`
}
6. 防范XSS攻击
转移状态中的恶意代码
import serialize from 'serialize-javascript'
export default (req, store) => {
const initialState = serialize(store.getState())
return `
<html>
<head>
<title>React SSR</title>
<body>
<div id="root">${content}</div>
<script>window.INITIAL_STATE = ${initialState}</script>
<script src="bundle.js"></script>
</body>
</head>
</html>
`
}