服务端渲染(SSR)
本章节介绍如何使用 Rsbuild 实现 SSR 功能。
值得注意的是,Rsbuild 自身不提供开箱即用的 SSR 能力,而是提供 low-level 的 API 和配置来允许框架开发者实现 SSR。如果你需要使用开箱即用的 SSR 支持,可以考虑使用基于 Rsbuild 的框架,例如 Modern.js。
什么是 SSR
SSR 是 "Server-Side Rendering"(服务端渲染)的缩写。它表示由服务器生成网页的 HTML,并将其发送给客户端,而不是只发送一个空的 HTML 外壳,并依赖 JavaScript 来生成页面内容。
在传统的客户端渲染中,服务器会向客户端发送一个空的 HTML 外壳和一些 JavaScript 脚本,然后从服务器的 API 中获取数据,并用动态内容填充页面。这会导致页面的初始加载时间较慢,不利于用户体验和 SEO。
使用 SSR 后,服务器会生成已经包含动态内容的 HTML,并将其发送给客户端。这使得首屏加载速度更快,并对 SEO 更加友好,因为搜索引擎可以爬取到渲染后的页面。
文件结构
一个典型的 SSR 应用会包含以下文件:
- index.html
- server.js          # 自定义服务器脚本
- src/
  - App.js           # 导出 App 代码
  - index.client.js  # 客户端入口,挂载 App 组件到 Dom 元素
  - index.server.js  # 服务端入口,通过 SSR API 渲染 App 组件
 
index.html 中需要定义 SSR 渲染占位符:
<div id="root"><!--app-content--></div>
 
创建 SSR 配置
SSR 场景下,需要同时产出 web 和 node 两种类型的产物,分别用于客户端渲染(CSR)和服务器端渲染(SSR)。
此时可以使用 Rsbuild 的多环境构建能力,定义如下配置:
rsbuild.config.ts
export default {
  environments: {
    // 配置 web 环境,用于浏览器端
    web: {
      source: {
        entry: {
          index: './src/index.client.js',
        },
      },
      output: {
        // 浏览器产物的 target 类型为 'web'
        target: 'web',
      },
      html: {
        // 自定义 HTML 模版
        template: './index.html',
      },
    },
    // 配置 node 环境,用于 SSR
    node: {
      source: {
        entry: {
          index: './src/index.server.js',
        },
      },
      output: {
        // Node.js 产物的 target 类型为 'node'
        target: 'node',
      },
    },
  },
};
 
自定义 Server
Rsbuild 并未内置 SSR 渲染能力,你可以通过 Rsbuild 的自定义 Server 和 Environment API 实现 SSR 渲染:
server.mjs
import express from 'express';
import { createRsbuild, loadConfig } from '@rsbuild/core';
// 实现 SSR 渲染功能
const serverRender = (serverAPI) => async (_req, res) => {
  // 加载 SSR bundle
  const indexModule = await serverAPI.environments.node.loadBundle('index');
  const markup = indexModule.render();
  const template = await serverAPI.environments.web.getTransformedHtml('index');
  // 将 SSR 渲染内容插入到 HTML 模版中
  const html = template.replace('<!--app-content-->', markup);
  res.writeHead(200, {
    'Content-Type': 'text/html',
  });
  res.end(html);
};
// 自定义 Server
async function startDevServer() {
  const { content } = await loadConfig({});
  const rsbuild = await createRsbuild({
    rsbuildConfig: content,
  });
  const app = express();
  const rsbuildServer = await rsbuild.createDevServer();
  const serverRenderMiddleware = serverRender(rsbuildServer);
  // 访问 /index.html 时进行 SSR 渲染
  app.get('/', async (req, res, next) => {
    try {
      await serverRenderMiddleware(req, res, next);
    } catch (err) {
      logger.error('SSR render error, downgrade to CSR...\n', err);
      next();
    }
  });
  app.use(rsbuildServer.middlewares);
  const httpServer = app.listen(rsbuildServer.port, async () => {
    await rsbuildServer.afterListen();
  });
  rsbuildServer.connectWebSocket({ server: httpServer });
}
startDevServer(process.cwd());
 
修改启动脚本
使用自定义 Server 后,需要将启动命令由 rsbuild dev 改为 node ./server.mjs。
如果需要预览 SSR 渲染的线上效果,同样需要修改预览命令。SSR Prod Server 示例参考:Example。
package.json
{
  "scripts": {
    "build": "rsbuild build",
    "dev": "node ./server.mjs",
    "preview": "node ./prod-server.mjs"
  }
}
 
现在,执行 npm run dev 命令即可启动带有 SSR 渲染功能的开发服务器,访问 http://localhost:3000/ 即可看到 SSR 内容已经渲染到了 HTML 页面上。
获取资源清单
默认情况下,和当前页面关联的 scripts 和 links 会自动插入到 HTML 模版中,此时通过 getTransformedHtml 即可获取编译后的 HTML 模版内容。
当需要在服务器端动态生成 HTML 时,你需要将 JavaScript 和 CSS 资源的 URL 注入到 HTML 中。通过配置 output.manifest,你可以方便地获取这些资源的清单信息。示例如下:
rsbuild.config.ts
export default {
  output: {
    manifest: true,
  },
};
 
server.ts
async function renderHtmlPage(): Promise<string> {
  const manifest = await fs.promises.readFile('./dist/manifest.json', 'utf-8');
  const { entries } = JSON.parse(manifest);
  const { js, css } = entries['index'].initial;
  const scriptTags = js
    .map((file) => `<script src="${file}" defer></script>`)
    .join('\n');
  const styleTags = css
    .map((file) => `<link rel="stylesheet" href="${file}">`)
    .join('\n');
  return `
    <!DOCTYPE html>
    <html>
      <head>
        ${scriptTags}
        ${styleTags}
      </head>
      <body>
        <div id="root"></div>
      </body>
    </html>`;
}
 
示例项目