上一篇文章我们写了一个 React 的开发服务器, 本篇我们会在上一篇的基础上加入 SSR 功能.
一个单纯的 React 应用, 当用户访问其网站的时候, 服务器返回给用户的是一个没有"内容"的 html 文件, html 文件里只会包含一个 js 文件(即 React 打包出来的 js 文件), 而页面的具体内容是通过这个 js 文件渲染出来的.
<html>
<head>
...
</head>
<body>
<div id="root"></div>
<script src="./index.js"></script>
</body>
</html>
这种做法对于一些后台应用来说没什么问题, 但是对于前台应用来说就会有一些问题, 比如没法做搜索引擎优化, 因为搜索引擎"看"不到这个网站中的内容.
所以就有了 SSR 服务端渲染. SSR 应用 和 单纯的 React 应用相比, 当访问其网站时, 服务器返回给用户的 html 文件中不仅包含 js 文件还会包含页面的"内容".
<html>
<head>
...
</head>
<body>
<div id="root">
内容...
</div>
<script src="./index.js"></script>
</body>
</html>
如何让服务器返回页面的内容呢? 对于上一篇中的开发服务器来说, 相当于给 express 应用增加了一个中间件(middleware). 中间件会根据访问的路径来返回相应的内容.
// 增加一个中间件, 返回 ssr 的内容, 仅用于演示
app.use((req, res, next) => {
const { path } = req
if (path === '/about') {
res.end("<body>SSR Content For <strong>ABOUT PAGE</strong><body/>");
return;
}
if (path === '/') {
res.end("<body>SSR Content For <strong>HOME PAGE</strong></body>");
return;
}
next()
})
只不过我们要把这个内容换成真正的 React-Router 匹配到的内容.
要得到 React-Router 匹配到的内容, 可以使用 react-router-dom/server
提供的 StaticRouter 组件. 然后通过 ReactDOMServer.renderToString
方法将 React 组件转化成字符串.
hydration 水合
点击上一个例子中的 to about 链接切换到 about 页面, 在 about 页面中点击按钮看看会发生什么?
点击按钮失效了! 点击的次数根本没有累加.
这是因为服务器只返回了 html 片段, 里面并没有 js 文件来使点击事件生效.
这就需要用到 hydration 水合, 即服务器同时生成一段 js 文件给客户端, 让 js 跟 html 片段相结合, 让这些事件都生效.
有几点需要注意的是:
- 需要设置几个静态文件夹, 用来返回静态文件.
app.use(express.static(path.join(__dirname, 'public'))); app.use('/serverDist', express.static(path.join(__dirname, 'serverDist'))) app.use('/clientDist', express.static(path.join(__dirname, 'clientDist')))
- 在水合的时候, 装载的容器需要和服务器返回的 html 片段相一致.
因为水合的时候容器用的 document.documentElement, 所以 App 组件内返回的是 html 以内的内容.hydrateRoot( document.documentElement, <BrowserRouter> <App /> </BrowserRouter> );
export default function App() { return ( <> <head> <meta charSet="UTF-8" /> <link rel="icon" type="image/x-icon" href="/favicon.svg" /> <title>演示 SSR</title> </head> <body> <Routes /> <script src='/clientDist/client.bundle.js' /> </body> </> ) }
以上就是一个基础的 SSR 应用, 这是理解 SSR 的一个很好的开始.