原理实践 搭建简答的SSR
API须知
ReactDOMServer.renderToString(element)
将一个 React 元素渲染成其初始的 HTML。React 将返回一个 HTML 字符串。你可以使用这种方法在服务器上生产 HTML,并在初始请求中发送标记。以加快页面加载速度,并允许搜索引擎以 SEO 为目的抓取你的页面。
StaticRouter
静态路由,通过初始传入的
location地址找到相应组件。区别于客户端的动态路由。
hydrate 变为 hydrateRoot(18版本)
// Before
import { hydrate } from 'react-dom';
const container = document.getElementById('app');
hydrate(<App tab="home" />, container);
// After
import { hydrateRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = hydrateRoot(container, <App tab="home" />);
// Unlike with createRoot, you don't need a separate root.render() call here.
前期代码
页面文件
// src/pages/Home
import React from "react";
const Home = () => {
return(
<div>首页</div>
)
}
export default Home
// src/pages/Personal
import React from "react";
const Personal = () => {
return(
<div>个人中心页面</div>
)
}
export default Personal
路由文件
// src/routes
import React from "react";
import {Routes,Route,Link} from "react-router-dom"
import Home from "./pages/Home"
import Personal from "./pages/Personal"
const RoutesList = () => {
return (
<div>
<ul>
<li>
<Link to="/">首页</Link>
</li>
<li>
<Link to="/personal">个人中心</Link>
</li>
</ul>
<Routes>
<Route exact path="/" element={<Home/>}></Route>
<Route path="personal" element={<Personal/>}></Route>
</Routes>
</div>
)
}
export default RoutesList
服务端文件
// src/server
import React from 'react'
import ReactDOMServer from "react-dom/server"
import RoutesList from './routes'
import {StaticRouter} from "react-router-dom/server"
const express = require('express')
const app = new express()
const port = 3000
app.get('*', (req,res) => {
const content = ReactDOMServer.renderToString(
<StaticRouter location={req.url}>
<RoutesList/>
</StaticRouter>
)
const html = `
<html>
<head></head>
<body>
<div id="root">${content}</div>
</body>x
</html>
`
res.send(html)
})
app.listen(port, () => {
console.log(`Server is running at http://localhost:${port}`)
})
配置文件
// webpack.server.js 服务端配置
const path = require('path')
const webpackNodexternals = require('webpack-node-externals')
module.exports = {
target:'node',
mode:process.env?.NODE_ENV === 'production' ? 'production' : 'development',
entry:path.resolve(__dirname,"../src/server.js"),
output:{
filename:'bundle_server.js',
path:path.resolve(__dirname,'../dist')
},
module:{
rules:[
{
test:/\.js$/,
loader:'babel-loader',
exclude:'/node_modules/'
}
]
},
// webpack-node-externals作用:将后端的模块不被打包
externals:[webpackNodexternals()]
}
// .babelrc配置
{
"presets": [
"@babel/preset-react",
"@babel/preset-env"
]
}
代码解析
-
访问页面回向服务端发起请求,
req.url即为访问页面的路径app.get('*', (req,res) => { //... } ) -
将
React元素转化为HTML字符串形式- 通过
StaticRouter在已有的路由表中查找对应的组件
- 通过
-
组装成完整的
HTML代码并从服务端发送给客户端
绑定事件
在原有的代码上添加点击事件:
// src/pages/Home
import React from "react";
const Home = () => {
const handleClick = () => {
console.log('点击')
}
return(
<div>首页 <button onClick={handleClick}>点我</button></div>
)
}
export default Home
但是在页面中点击按钮,发现控制台中并没有打印"点击",我们查看服务端返回的html代码,发现没有相关事件的代码
<html>
<head></head>
<body>
<div id="root">
<div>
<ul>
<li><a href="/">首页</a></li>
<li><a href="/personal">个人中心</a></li>
</ul>
<div>首页 <button>点我</button></div>
</div>
</div>
</body>
</html>
解析:
这是因为后端给前端的页面是脱水后的,需要React接管页面后,需要将数据传入组件,使客户端的组件正式由React管理
-
向组件内注水
提示:以下代码是v18后
hydrateRoot的用法// src/client import React from "react"; import { hydrateRoot } from 'react-dom/client'; import { BrowserRouter } from "react-router-dom" import RoutesList from "./routes"; // 类似render方法,但是基于ssr,只是恢复原本已经存在的DOM节点 hydrateRoot( document.querySelector('#root'), (<BrowserRouter> <RoutesList/> </BrowserRouter>) )前后端双路由,后端根据路由获取对应的数据、渲染组件并转化为
html代码;前端接收html再次进行渲染 -
webpack进行打包
作用:避免浏览器无法解析代码
const path = require('path') module.exports = { target:'web', mode:process.env?.NODE_ENV === 'production' ? 'production' : 'development', entry:path.resolve(__dirname,"../src/client.js"), output:{ filename:'bundle_client.js', path:path.resolve(__dirname,'../dist/public') }, module:{ rules:[ { test:/\.js$/, loader:'babel-loader', exclude:'/node_modules/' } ] } } -
浏览器获取客户端代码(
src/client.js)此处我们使用
<script>进行导入// src/server // 代理静态目录 app.use(express.static('dist/public')) app.get('*', (req,res) => { const content = ReactDOMServer.renderToString( <StaticRouter location={req.url}> <RoutesList/> </StaticRouter> ) const html = ` <html> <head></head> <body> <div id="root">${content}</div> <script src="bundle_client.js"></script> </body> </html> ` res.send(html) })注意:
<script src="bundle_client.js"></script>一定要置于<div id="root">${content}</div>之后,因为在client.js中使用节点的选择(document.querySelector('#root'))。如果互换了,<div id="root"></div>节点并未完成渲染,会出现报错
-
配置命令,方便执行
"scripts": { "webpack:server": "webpack --config ./config/webpack.server.js --watch", "webpack:client": "webpack --config ./config/webpack.client.js --watch", "webpack:start": "nodemon --watch dist --exec node dist/bundle_server.js", "dev": "npm-run-all --parallel webpack:*" },