服务器端渲染(SSR)是一种允许您在服务器端提供初始的 HTML、JavaScript、CSS 甚至应用程序状态的技术。即使不包含 JavaScript,渲染出完整的 HTML 也是有意义的。除了提供潜在的性能优势外,这还有助于搜索引擎优化(SEO)。
即使这个想法听起来不那么独特,但也存在技术成本。这一理念由 React 推广,之后就出现了许多 SSR 框架,比如 Next.js 和 razzle。
为了演示 SSR,您可以使用 Webpack 构建客户端代码,然后由服务器渲染,关键是服务器要遵循 React 服务端渲染的原则。通过这样做,我们足以了解 SSR 的基本原理,并发现 SSR 领域的特定问题。
SSR 不是 SEO 问题的唯一解决方案。预渲染是一种替代方案,如果它符合你的需求的话,并且该方案更容易实现。不过它不适用于高动态数据。我们可以在 Webpack 中使用 prerender-spa-plugin 插件来实现预渲染。
设置 Babel 以使用 React
我们在加载 JavaScript章节中介绍了在 Webpack 中使用 Babel 的关键要领。但是对于 React,我们还需要一些特别的配置。鉴于大多数 React 项目都依赖于 JSX 格式,我们必须在项目中启用它。
要让 React 特别是 JSX 与 Babel 合作,首先我们要安装一些预设:
npm install @babel/preset-react --save-dev
使用 Babel 配置加入预设,如下所示:
.babelrc
{
...
"presets": [
"@babel/preset-react",
...
]
}
新建一个 React Demo
首先,我们要为这个 Demo 安装两个依赖:React 和 react-dom。react-dom 是为了将应用在 DOM 中渲染。
npm install react react-dom --save
接下来,React 代码需要一个入口点。如果您在浏览器端,那么就需要将 Hello world
div 挂载到 DOM 节点上。如果它已经生效了,单击它应该会弹出一个带有 “hello” 消息的对话框。在服务器端,我们直接返回 React 组件,并且直接作为模块内容导出。
调整如下:
src/ssr.js
const React = require("react");
const ReactDOM = require("react-dom");
const SSR = <div onClick={() => alert("hello")}>Hello world</div>;
// 仅在客户端渲染, 否则直接导出
if (typeof document === "undefined") {
module.exports = SSR;
} else {
ReactDOM.hydrate(SSR, document.getElementById("app"));
}
现在你还缺少 Webpack 配置,以将这个文件转换成服务器可以接收的形式。
鉴于 ES2015 风格的导出和 CommonJS 导出不能混用,所以入口文件是用CommonJS 风格编写的。
配置 Webpack
为了让整个配置更加清晰,我们再定义一个单独的配置文件。鉴于打包的结果可能在多个环境中使用,所以我们把 UMD 做为构建的库目标:
webpack.ssr.js
const path = require("path");
const merge = require("webpack-merge");
const parts = require("./webpack.parts");
const PATHS = {
build: path.join(__dirname, "static"),
ssrDemo: path.join(__dirname, "src", "ssr.js"),
};
module.exports = merge([
{
mode: "production",
entry: {
index: PATHS.ssrDemo,
},
output: {
path: PATHS.build,
filename: "[name].js",
libraryTarget: "umd",
globalObject: "this",
},
},
parts.loadJavaScript({ include: PATHS.ssrDemo }),
]);
为了方便生成构建,我们添加一个脚本命令:
package.json
"scripts": {
"build:ssr": "webpack --config webpack.ssr.js",
...
},
执行 SSR 构建(npm run build:ssr
),您应该看到一个 ./static/index.js 新文件。下一步,我们需要设置一下服务器,来渲染这个文件。
设置服务器
为了更容易理解,您可以设置一个独立的 Express 服务器,并按照 SSR 的要求来渲染已经构建好的包。首先我们安装 Express:
npm install express --save-dev
然后,按照下面的方式实现一个简单的 Web 服务:
server.js
const express = require("express");
const { renderToString } = require("react-dom/server");
const SSR = require("./static");
server(process.env.PORT || 8080);
function server(port) {
const app = express();
app.use(express.static("static"));
app.get("/", (req, res) =>
res.status(200).send(renderMarkup(renderToString(SSR)))
);
app.listen(port);
}
function renderMarkup(html) {
return `<!DOCTYPE html>
<html>
<head>
<title>Webpack SSR Demo</title>
<meta charset="utf-8" />
</head>
<body>
<div id="app">${html}</div>
<script src="./index.js"></script>
</body>
</html>`;
}
现在运行服务器(node ./server.js
)并在浏览器访问 http://localhost:8080
,您应该可以看到熟悉的东西:
现在我们的 React 应用程序已经跑起来了,但它很难开发。如果您尝试修改代码,则不会发生任何事情。如本书前面所述,在多编译器模式下运行 Webpack 可以解决这个问题。另一种办法是针对当前配置以 watch mode 运行 Webpack,同时为服务器也设置一个监视程序。接下来,您将学习如何配置。
如果要调试服务器的输出,请设置
export DEBUG=express:application
。
按照“提取 manifest”一章中所说的,通过生成 manifest 文件,资源引用会自动写入到模板文件中。
监测 SSR 更改并刷新浏览器
监测 SSR 很容易解决,在终端中运行 npm run build:ssr -- --watch
就可以了,这迫使 Webpack 以监视模式运行。为方便起见,我们可以将这条命令添加到 NPM 脚本中。
剩下的部分会相对难一些,如何让服务器检测到更改以及如何将更改传达给浏览器呢?
browser-refresh 可以派上用场,它解决了上述两个问题。首先安装它:
npm install browser-refresh --save-dev
现在,我们需要对代码做两处改动:
server.js
server(process.env.PORT || 8080);
function server(port) {
...
// app.listen(port);
app.listen(port, () => process.send && process.send("online"));
}
function renderMarkup(html) {
return `<!DOCTYPE html>
<html>
...
<body>
...
<script src="${process.env.BROWSER_REFRESH_URL}"></script>
</body>
</html>`;
}
第一处改动告诉客户端应用程序在线并准备就绪。后一处改动是将客户端脚本地址改为动态值。browser-refresh 会动态的设置 BROWSER_REFRESH_URL。
我们在在终端中运行 node_modules/.bin/browser-refresh ./server.js
,并在浏览器中打开 http://localhost:8080
,来测试我们的设置是否已经生效。请记住,我们还要在另一个终端中以监视模式运行 Webpack (npm run build:ssr -- --watch
)。如果一切正常,您对客户端脚本(app/ssr.js)所做的任何更改都应该显示在浏览器中或导致服务器出现故障。
如果服务器崩溃,则会丢失 WebSocket 连接。在这种情况下,您必须在浏览器中强制刷新。如果服务器也是通过 Webpack 管理的,那么可以避免这个问题。
要证明 SSR 有效,请查看浏览器检查器。你应该看到熟悉的东西:
从检查器中,我们可以看到整个 HTML,而不是将应用挂载在某个 div 节点上。尽管这个特殊的例子没有太多的内容,但足以说明 SSR 的工作原理。
还可以进一步改进上述实现,在服务器中设置一个生产环境,不在动态设置脚本地址,而是将初始数据注入到 HTML 中。这样做可以避免客户端请求。
开放性问题
尽管上面的例子演示了 SSR 的基本概念,但它仍然存在悬而未决的问题:
- 如何处理样式?Node 不理解 CSS 相关的导入。
- 如何处理 JavaScript 之外的任何资源?如果服务器端是通过 Webpack 处理的,那么这不是一个问题,因为你可以利用 Webpack 来打包这些资源。
- 如何在 Node 之外的服务器运行?一种选择是将 Node 实例包装在服务中,然后通过主机环境运行。理想情况下,结果将被缓存,您可以为每个平台找到更具体的解决方案。
像这样的问题是 Next.js 或 razzle 等解决方案存在的原因。它们旨在解决与 SSR 相关的问题。
路由是 SSR 中的一个大问题,Next.js 框架解决了这一问题。Patrick Hund 还阐述了如何使用 React 和 React Router 4来解决这个问题。
总结
SSR 带来了一些技术挑战,因此,围绕它出现了具体的解决方案。Webpack非常适合 SSR。
回顾一下:
- 服务器端渲染可以为浏览器提供更多的初始加载内容。您可以立即显示页面,而不是等待 JavaScript 加载。
- 服务器端渲染还允许您将初始数据直接注入 HTML 模板中,以避免对服务器进行不必要的请求。
- Webpack 可以解决客户端的打包问题。如果需要更集成的解决方案,它也可用于服务器。诸如 Next.js 之类的框架就实现了这一解决方案。
- 服务器端渲染不是没有成本的,它会导致新的问题,因为您需要更好的方法来处理方方面面,例如样式和路由。服务器和客户端环境是大不相同的,因此,编写出的代码不能依赖于特定于平台。