构建服务端渲染项目

其实在前一节中,大部分的主要代码都已经通过示例列出来了,在这一节里再重新完整整理一遍。

Tip

本书中服务端渲染的示例都采用Bun运行时 + Elysia框架的高性能服务组织形式,这套组合与网络上常见的NodeJS运行时 + Express框架的服务组织形式有所不同,例如NodeJS运行时是不支持ReadableStream的,所以在server.ts中就需要使用renderToPipeableStream来获取NodeJS运行时支持的PipeableStream输出渲染完成的HTML片段。

另外Bun运行时是原生提供了Typescript的支持的,所以书中的Typescript文件可以直接运行,但是如果使用NodeJS运行时就需要使用Babel等工具进行实时转译或者在编译后执行。

传统 React 项目的构建

构建一个 React SSR 项目的基础还是一个传统的 React 项目,这里采用 Vite 构建这个 React 项目。对于本节的示例来说,可以使用以下命令来创建。

bun create vite react-ssr-example --template react-ts

Tip

创建项目使用的模板还可以使用react-swc-ts以提高React项目的编译性能,亦或者不指定模板,在选择模板的时候选择Others > create-vite-extra来直接创建一个直接支持SSR的项目。

构建好的项目中一般会存在index.html文件,在src目录下会存在main.tsx文件和App.tsx文件。

接下来,就需要对这个项目做一些改造,这个改造主要按照以下思路进行:

  1. src/App.tsx文件改造成 SSR 的渲染入口,也就是包含启动 HTML 的文件。
  2. src/main.tsx文件改造成通用 React 组件树入口。
  3. 增加一个server.ts文件用于应用服务器的启动文件。

改造 SSR 渲染入口

这里还是要使用前一节中所说到的全部文档托管的渲染入口组织形式。所以App.tsx文件的内容如下所示。

import { StrictMode } from "react";
import { Route, Routes, StaticRouter } from "react-router-dom";
import { MainLayout } from "./pages/MainLayout";

export const App = () => {
  return (
    <StrictMode>
      <html lang="zh-CN">
        <head>
          <meta charSet="utf-8" />
          <meta name="viewport" content="width=device-width, initial-scale=1" />
          <title>React SSR App</title>
          <link rel="stylesheet" href="/src/index.css" />
        </head>
        <body>
          <StaticRouter>
            <Routes>
              <Route path="/" element={<MainLayout />} />
            </Routes>
          </StaticRouter>
        </body>
      </html>
    </StrictMode>
  );
};

在网络上其他的示例中,这个 SSR 渲染入口文件也常常被命名为entry-server.jsentry-server.jsxentry-server.tsx等。但是无论其如何命名,所执行的功能都是声明项目最基础的 HTML 文件和载入 React 应用入口文件。

在定义了App.tsx文件以后,就可以将项目根目录中的index.html文件删除了。

在 React 19 中,HTML 的元标签,比如<html><meta><title>等标签都是可以在组件中使用了,而且会被自动放置到 HTML DOM 中的相应位置。

Tip

HTML的元标签不仅在SSR中可以使用,在传统客户端渲染的React应用中一样可以使用。比如使用<title>来动态设定HTML文档的标题就十分方便了,不再需要引入其他的Hook。

与上一节中部分托管的形式不同,这一份App.tsx文件中虽然也不需要提供通用 React 组件树入口文件的载入,但是还是需要提供所使用的样式文件的引入。这个样式文件的引入直接仿照示例中的<link>标签的书写即可。

React 应用的组件树现在已经无需额外在main.tsx文件中再定义了,可以直接在<body>中完成书写。

Caution

本节示例使用的是React官网上所述的水合整个HTML文档的方式,所以将App组件中定义的内容拆分定义必要性不大。

改造通用 React 组件树入口

在原本的main.tsx文件中,其主要功能是使用 React DOM 提供的 createRoot 函数来将 React 组件树插入到 HTML DOM 中。但是在这个示例中,由于整个组件树已经在App.tsx中完成了向 HTML DOM 中的插入,所以main.tsx的内容就变得十分简单,只需要引入App.tsx中定义的文档根组件,然后进行水合处理即可。

以下是main.tsx的全部内容。

import { hydrateRoot } from "react-dom/client";
import { App } from "./App";

hydrateRoot(document, <App />);

增加server.ts服务器启动文件

这个服务器启动文件可以被命名为任意名称,不必局限于server.ts,这个命名只是习惯上的。

以下是这个server.ts文件的最小内容。

import Elysia from "elysia";
import { connect } from "elysia-connect-middleware";
import { createElement } from "react";
import { renderToReadableStream } from "react-dom/server";
import { createServer } from "vite";
import { App } from "./src/App";

const app = new Elysia();
const vite = await createServer({
  server: { middlewareMode: true },
  appType: "custom",
});
app.use(connect(vite.middlewares));
app.get("/", async () => {
  const app = createElement(App);
  const stream = await renderToReadableStream(app, {
    bootstrapModules: ["/src/main.tsx"],
  });
  return new Response(stream, {
    headers: {
      "Content-Type": "text/html",
    },
  });
});

app.listen(8000);

console.log(
  `Server running at http://${app.server?.hostname}:${app.server?.port}`
);

Elysia 服务框架的使用基本上与 Express 服务框架差不多,在这个server.ts文件中,只有几点需要特别说明的。

  1. 由于 Vite 以中间件模式启动的时候,只兼容 Express 服务,所以需要使用 Elysia 的中间件插件elysia-connect-middleware来适配和转换 Express 的中间件供 Elysia 使用。具体 Vite 的中间件模式都做了哪些事情,可以参考 Vite 的使用文档。
  2. 在生产模式中,不需要以中间件模式加载 Vite。
  3. React Dom 提供的renderToReadableStream所接受的第一个参数是经过渲染的组件树,由于现在这个文件是.ts格式,不能使用 HTML 标签,所以就只能以模块的形式导入App组件,然后使用 React 提供的createElement函数进行渲染。
  4. 用于在客户端引入的应用启动脚本src/main.tsx需要在renderToReadableStream函数的第二个参数中指定,不能直接在App组件中直接写入。React DOM 提供了多个配置项用来加载不同形式的客户端启动脚本,包括直接插入脚本内容bootstrapScriptContent、插入脚本路径bootstrapScripts以及插入模块bootstrapModules的方式插入脚本。在一般情况下,插入脚本模块的方式是更加常用的,它会自动在<script>标签中增加type="module"属性以标识引用的脚本文件是一个模块。

有了这个文件以后,就可以在项目根目录中执行bun run server.ts来启动支持 SSR 的 React 应用了。

Tip

如果需要实现类似于Vite的开发服务器的监控文件变更自动重载功能,可以使用bun --watch run server.ts命令或者bun --hot run server.ts,但是这种运行模式需要使用进程管理器(Windows)或者活动管理器(macOS)来终止。

上面这个示例中的server.ts的内容是启动服务的最小文件,这个文件目前还只能在项目开发过程中使用。如果需要在生产环境中使用,那么就还需要引入不少的编码和配置,也就是说,如果需要server.ts能够同时兼容开发过程使用和生产环境使用,那么server.ts中就必须再增加能够根据当前运行环境,选择编译后文件并加载的功能。