服务端渲染

服务器端渲染(SSR)是一种允许您在服务器端提供初始的 HTML、JavaScript、CSS 甚至应用程序状态的技术。即使不包含 JavaScript,渲染出完整的 HTML 也是有意义的。除了提供潜在的性能优势外,这还有助于搜索引擎优化(SEO)。

即使这个想法听起来不那么独特,但也存在技术成本。这一理念由 React 推广,之后就出现了许多 SSR 框架,比如 Next.jsrazzle

为了演示 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,您应该可以看到熟悉的东西:

hello-world,Demo,Output

现在我们的 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 有效,请查看浏览器检查器。你应该看到熟悉的东西:

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 之类的框架就实现了这一解决方案。
  • 服务器端渲染不是没有成本的,它会导致新的问题,因为您需要更好的方法来处理方方面面,例如样式和路由。服务器和客户端环境是大不相同的,因此,编写出的代码不能依赖于特定于平台。
用户头像
登录后发表评论