服务端渲染

    :::tip 注意 SSR 特别指支持在 Node.js 中运行相同应用程序的前端框架(例如 React、Preact、Vue 和 Svelte),将其预渲染成 HTML,最后在客户端进行注水化处理。如果你正在寻找与传统服务器端框架的集成,请查看 后端集成指南

    下面的指南还假定你在选择的框架中有使用 SSR 的经验,并且只关注特定于 Vite 的集成细节。 :::

    :::warning Low-level API 这是一个底层 API,是为库和框架作者准备的。如果你的目标是构建一个应用程序,请确保优先查看 中更上层的 SSR 插件和工具。也就是说,大部分应用都是基于 Vite 的底层 API 之上构建的。 :::

    :::tip 帮助 如果你有疑问,可以到社区 Discord 的 Vite #ssr 频道,这里会帮到你。 :::

    Vite 为服务端渲染(SSR)提供了内建支持。这里的 Vite 范例包含了 Vue 3 和 React 的 SSR 设置示例,可以作为本指南的参考:

    源码结构

    一个典型的 SSR 应用应该有如下的源文件结构:

    将需要引用 entry-client.js 并包含一个占位标记供给服务端渲染时注入:

    1. <div id="app"><!--ssr-outlet--></div>
    2. <script type="module" src="/src/entry-client.js"></script>

    你可以使用任何你喜欢的占位标记来替代 <!--ssr-outlet-->,只要它能够被正确替换。

    情景逻辑

    如果需要执行 SSR 和客户端间情景逻辑,可以使用:

    1. if (import.meta.env.SSR) {
    2. // ... 仅在服务端执行的逻辑
    3. }

    这是在构建过程中被静态替换的,因此它将允许对未使用的条件分支进行摇树优化。

    在构建 SSR 应用程序时,你可能希望完全控制主服务器,并将 Vite 与生产环境脱钩。因此,建议以中间件模式使用 Vite。下面是一个关于 的例子:

    server.js

    下一步是实现 * 处理程序供给服务端渲染的 HTML:

    1. app.use('*', async (req, res) => {
    2. const url = req.originalUrl
    3. try {
    4. // 1. 读取 index.html
    5. let template = fs.readFileSync(
    6. path.resolve(__dirname, 'index.html'),
    7. )
    8. // 2. 应用 vite HTML 转换。这将会注入 vite HMR 客户端,
    9. // 同时也会从 Vite 插件应用 HTML 转换。
    10. // 例如:@vitejs/plugin-react-refresh 中的 global preambles
    11. template = await vite.transformIndexHtml(url, template)
    12. // 3. 加载服务器入口。vite.ssrLoadModule 将自动转换
    13. // 你的 ESM 源码使之可以在 Node.js 中运行!无需打包
    14. // 并提供类似 HMR 的根据情况随时失效。
    15. const { render } = await vite.ssrLoadModule('/src/entry-server.js')
    16. // 4. 渲染应用的 HTML。这假设 entry-server.js 导出的 `render`
    17. // 例如 ReactDOMServer.renderToString()
    18. const appHtml = await render(url)
    19. // 5. 注入渲染后的应用程序 HTML 到模板中。
    20. const html = template.replace(`<!--ssr-outlet-->`, appHtml)
    21. // 6. 返回渲染后的 HTML。
    22. res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
    23. } catch (e) {
    24. // 如果捕获到了一个错误,让 vite 来修复该堆栈,这样它就可以映射回
    25. vite.ssrFixStacktrace(e)
    26. console.error(e)
    27. res.status(500).end(e.message)
    28. }
    29. })

    package.json 中的 dev 脚本也应该相应地改变,使用服务器脚本:

    1. "scripts": {
    2. - "dev": "vite"
    3. + "dev": "node server"
    4. }

    生产环境构建

    为了将 SSR 项目交付生产,我们需要:

    1. 正常生成一个客户端构建;
    2. 再生成一个 SSR 构建,使其通过 require() 直接加载,这样便无需再使用 Vite 的 ssrLoadModule

    package.json 中的脚本应该看起来像这样:

    注意使用 --ssr 标志表明这将会是一个 SSR 构建。同时需要指定 SSR 的入口。

    接着,在 server.js 中,通过 process.env.NODE_ENV 条件分支,需要添加一些用于生产环境的特定逻辑:

    • 使用 dist/client/index.html 作为模板,而不是根目录的 index.html,因为前者包含了到客户端构建的正确资源链接。

    • 使用 require('./dist/server/entry-server.js') ,而不是 (前者是 SSR 构建后的最终结果)。

    • vite 开发服务器的创建和所有使用都移到 dev-only 条件分支后面,然后添加静态文件服务中间件来服务 dist/client 中的文件。

    可以在此参考 和 React 的设置范例。

    生成预加载指令

    vite build 支持使用 --ssrManifest 标志,这将会在构建输出目录中生成一份 ssr-manifest.json

    1. - "build:client": "vite build --outDir dist/client",
    2. + "build:client": "vite build --outDir dist/client --ssrManifest",

    上面的脚本将会为客户端构建生成 dist/client/ssr-manifest.json(是的,该 SSR 清单是从客户端构建生成而来,因为我们想要将模块 ID 映射到客户端文件上)。清单包含模块 ID 到它们关联的 chunk 和资源文件的映射。

    @vitejs/plugin-vue 支持该功能,开箱即用,并会自动注册使用的组件模块 ID 到相关的 Vue SSR 上下文:

    1. // src/entry-server.js
    2. const ctx = {}
    3. const html = await vueServerRenderer.renderToString(app, ctx)
    4. // ctx.modules 现在是一个渲染期间使用的模块 ID 的 Set

    我们现在需要在 server.js 的生产环境分支下读取该清单,并将其传递到 src/entry-server.js 导出的 render 函数中。这将为我们提供足够的信息,来为异步路由相应的文件渲染预加载指令!查看 示例代码 获取完整示例。

    如果预先知道某些路由所需的路由和数据,我们可以使用与生产环境 SSR 相同的逻辑将这些路由预先渲染到静态 HTML 中。这也被视为一种静态站点生成(SSG)的形式。查看 获取有效示例。

    SSR 外部化

    许多依赖都同时提供 ESM 和 CommonJS 文件。当运行 SSR 时,提供 CommonJS 构建的依赖关系可以从 Vite 的 SSR 转换/模块系统进行 “外部化”,从而加速开发和构建。例如,并非去拉取 React 的预构建的 ESM 版本然后将其转换回 Node.js 兼容版本,用 require('react') 代替会更有效。它还大大提高了 SSR 包构建的速度。

    Vite 基于以下策略执行自动化的 SSR 外部化:

    • 如果一个依赖的解析 ESM 入口点和它的默认 Node 入口点不同,它的默认 Node 入口可能是一个可以外部化的 CommonJS 构建。例如,vue 将被自动外部化,因为它同时提供 ESM 和 CommonJS 构建。

    • 否则,Vite 将检查包的入口点是否包含有效的 ESM 语法 - 如果不包含,这个包可能是 CommonJS,将被外部化。例如,react-dom 将被自动外部化,因为它只指定了唯一的一个 CommonJS 格式的入口。

    如果这个策略导致了错误,你可以通过 ssr.externalssr.noExternal 配置项手动调整。

    在未来,这个策略将可能得到改进,将去探测该项目是否有启用 type: "module",这样 Vite 便可以在 SSR 期间通过动态 import() 导入兼容 Node 的 ESM 构建依赖来实现外部化依赖项。

    :::warning 使用别名 如果你为某个包配置了一个别名,为了能使 SSR 外部化依赖功能正常工作,你可能想要使用的别名应该指的是实际的 node_modules 中的包。 和 pnpm 都支持通过 npm: 前缀来设置别名。 :::

    SSR 专有插件逻辑

    一些框架,如 Vue 或 Svelte,会根据客户端渲染和服务端渲染的区别,将组件编译成不同的格式。可以向以下的插件钩子中,给 Vite 传递额外的 ssr 参数来支持根据情景转换:

    • resolveId
    • load

    示例: