知海

服务端渲染(SSR)

vite-main使用指南

服务端渲染(SSR)

:::tip 提示
SSR 特指前端框架(例如 React、Preact、Vue 和 Svelte)支持在 Node.js 中运行同一套应用代码、将其预渲染为 HTML,并最终在客户端进行水合(hydrating)的能力。如果你正在寻找与传统服务端框架的集成方式,请参阅后端集成指南

以下指南还假定你已具备所选框架的 SSR 使用经验,并且只会聚焦于 Vite 特有的集成细节。

:::warning 底层 API
这是一个面向库和框架作者的底层 API。如果你的目标是创建一个应用,请务必先查看 Awesome Vite SSR 部分中更高级的 SSR 插件和工具。尽管如此,许多应用也直接基于 Vite 的原生底层 API 成功构建。

目前,Vite 正借助 Environment API 改进 SSR API。点击链接查看更多详情。

示例项目

Vite 内置了对服务端渲染(SSR)的支持。create-vite-extra 包含了一些可用于参考的 SSR 示例配置:

你也可以在本地通过运行 create-vite 并选择框架选项下的 Others > create-vite-extra 来生成这些项目。

源码结构

一个典型的 SSR 应用将具有以下源文件结构:

复制代码
- index.html
- server.js # 主应用服务器
- src/
  - main.js          # 导出与环境无关(通用)的应用代码
  - entry-client.js  # 将应用挂载到 DOM 元素上
  - entry-server.js  # 使用框架的 SSR API 渲染应用

index.html 需要引用 entry-client.js,并包含一个占位符,用于注入服务端渲染后的标记:

html [index.html] 复制代码
<div id="app"><!--ssr-outlet--></div>
<script type="module" src="/src/entry-client.js"></script>

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

条件逻辑

如果你需要根据 SSR 还是客户端(client)来执行条件逻辑,可以使用:

js twoslash 复制代码
import 'vite/client'
// ---cut---
if (import.meta.env.SSR) {
  // ... 仅服务端逻辑
}

这在构建时会被静态替换,因此可以摇树(tree-shaking)移除未使用的分支。

设置开发服务器

在构建 SSR 应用时,你可能希望完全控制主服务器,并将 Vite 与生产环境解耦。因此,推荐使用 Vite 的中间件模式。以下是一个使用 express 的示例:

js{12-15} twoslash [server.js] 复制代码
import fs from 'node:fs'
import path from 'node:path'
import express from 'express'
import { createServer as createViteServer } from 'vite'

async function createServer() {
  const app = express()

  // 以中间件模式创建 Vite 服务器,并将应用类型配置为 'custom',
  // 禁用 Vite 自身的 HTML 服务逻辑,以便父服务器接管控制权
  const vite = await createViteServer({
    server: { middlewareMode: true },
    appType: 'custom'
  })

  // 使用 vite 的 connect 实例作为中间件。如果你使用自己的
  // express 路由(express.Router()),则应使用 router.use
  // 当服务器重启时(例如用户修改了 vite.config.js),
  // `vite.middlewares` 仍然是同一个引用
  // (但内部是新的 Vite 和插件注入的中间件栈)。
  // 以下代码在重启后依然有效。
  app.use(vite.middlewares)

  app.use('*all', async (req, res) => {
    // 服务 index.html - 我们将在下一步处理
  })

  app.listen(5173)
}

createServer()

这里的 viteViteDevServer 的实例。vite.middlewares 是一个 Connect 实例,可以用作任何兼容 Connect 的 Node.js 框架的中间件。

下一步是实现 * 处理器,用于提供服务端渲染的 HTML:

js twoslash [server.js] 复制代码
// @noErrors
import fs from 'node:fs'
import path from 'node:path'

/** @type {import('express').Express} */
var app
/** @type {import('vite').ViteDevServer}  */
var vite

// ---cut---
app.use('*all', async (req, res, next) => {
  const url = req.originalUrl

  try {
    // 1. 读取 index.html
    let template = fs.readFileSync(
      path.resolve(import.meta.dirname, 'index.html'),
      'utf-8',
    )

    // 2. 应用 Vite 的 HTML 转换。这会注入 Vite HMR 客户端,
    //    并应用 Vite 插件的 HTML 转换,例如 @vitejs/plugin-react 的全局注入。
    template = await vite.transformIndexHtml(url, template)

    // 3. 加载服务端入口。ssrLoadModule 会自动将 ESM 源代码转换
    //    为可在 Node.js 中使用的版本!无需打包,
    //    并提供类似 HMR 的高效失效机制。
    const { render } = await vite.ssrLoadModule('/src/entry-server.js')

    // 4. 渲染应用的 HTML。这假设 entry-server.js 导出的
    //    `render` 函数调用了适当的框架 SSR API,
    //    例如 ReactDOMServer.renderToString()
    const appHtml = await render(url)

    // 5. 将应用渲染后的 HTML 注入到模板中。
    const html = template.replace(`<!--ssr-outlet-->`, () => appHtml)

    // 6. 将渲染后的 HTML 返回给客户端。
    res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
  } catch (e) {
    // 如果捕获到错误,让 Vite 修复堆栈跟踪,以便映射回你的实际源码。
    vite.ssrFixStacktrace(e)
    next(e)
  }
})

package.json 中的 dev 脚本也应该改为使用服务器脚本:

diff [package.json] 复制代码
  "scripts": {
-   "dev": "vite"
+   "dev": "node server"
  }

生产环境构建

要将 SSR 项目发布到生产环境,我们需要:

  1. 照常生成客户端构建产物;
  2. 生成 SSR 构建产物,使其可以通过 import() 直接加载,从而无需经过 Vite 的 ssrLoadModule

package.json 中的脚本将如下所示:

json [package.json] 复制代码
{
  "scripts": {
    "dev": "node server",
    "build:client": "vite build --outDir dist/client",
    "build:server": "vite build --outDir dist/server --ssr src/entry-server.js"
  }
}

注意 --ssr 标志,它表示这是一个 SSR 构建。它还应指定 SSR 入口。

然后,在 server.js 中,我们需要通过检查 process.env.NODE_ENV 来添加一些生产环境特有的逻辑:

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

  • 不再使用 await vite.ssrLoadModule('/src/entry-server.js'),而是使用 import('./dist/server/entry-server.js')(该文件是 SSR 构建的产物)。

  • vite 开发服务器的创建和所有使用都放到仅开发环境的条件分支后面,并添加静态文件服务中间件来从 dist/client 提供文件。

请参阅示例项目以获取可工作的配置。

生成预加载指令

vite build 支持 --ssrManifest 标志,它会在构建输出目录中生成 .vite/ssr-manifest.json

diff 复制代码
- "build:client": "vite build --outDir dist/client",
+ "build:client": "vite build --outDir dist/client --ssrManifest",

上述脚本现在将为客户端构建生成 dist/client/.vite/ssr-manifest.json(是的,SSR manifest 是从客户端构建生成的,因为我们需要将模块 ID 映射到客户端文件)。该 manifest 包含模块 ID 到其关联 chunk 和资源文件的映射。

要利用该 manifest,框架需要提供一种方式来收集服务端渲染调用期间所使用的组件模块 ID。

@vitejs/plugin-vue 开箱即用地支持这一点,并会自动将已使用的组件模块 ID 注册到关联的 Vue SSR 上下文中:

js [src/entry-server.js] 复制代码
const ctx = {}
const html = await vueServerRenderer.renderToString(app, ctx)
// ctx.modules 现在是一个包含渲染期间所用模块 ID 的 Set

server.js 的生产分支中,我们需要读取 manifest 并将其传递给 src/entry-server.js 导出的 render 函数。这将为我们提供足够的信息来为异步路由所使用的文件渲染预加载指令!参见演示源码以获得完整示例。你还可以将这些信息用于 103 Early Hints

预渲染 / SSG

如果某些路由及其所需的数据可以提前得知,我们可以使用与生产环境 SSR 相同的逻辑将这些路由预渲染为静态 HTML。这也可以视为一种静态站点生成(SSG)。参见演示预渲染脚本以获取可工作的示例。

SSR 外部化

默认情况下,运行 SSR 时依赖项会从 Vite 的 SSR 转换模块系统中被“外部化”(externalized)。这可以加速开发和生产构建。

如果某个依赖项需要通过 Vite 的管道进行转换(例如因为其中使用了未编译的 Vite 特性),可以将其添加到 ssr.noExternal

对于链接的依赖项(linked dependencies),默认不会外部化,以利用 Vite 的 HMR。如果这不是你想要的(例如为了测试依赖项如同未被链接时的表现),可以将其添加到 ssr.external

:::warning 关于别名的注意事项
如果你配置了将一个包重定向到另一个包的别名,你可能需要改为将实际的 node_modules 包作为别名,以便使 SSR 外部化的依赖项正常工作。Yarnpnpm 都支持通过 npm: 前缀进行别名操作。

针对 SSR 的插件逻辑

一些框架(如 Vue 或 Svelte)会根据客户端还是 SSR 将组件编译为不同的格式。为了支持条件转换,Vite 会在以下插件钩子的 options 对象中传递一个额外的 ssr 属性:

  • resolveId
  • load
  • transform

示例:

js twoslash 复制代码
/** @type {() => import('vite').Plugin} */
// ---cut---
export function mySSRPlugin() {
  return {
    name: 'my-ssr',
    transform(code, id, options) {
      if (options?.ssr) {
        // 执行针对 SSR 的转换...
      }
    },
  }
}

loadtransform 中的 options 对象是可选的,Rollup 目前并未使用该对象,但未来可能会为这些钩子扩展额外的元数据。tip 提示
在 Vite 2.7 之前,这是通过位置参数 ssr 传递给插件钩子的,而不是使用 options 对象。所有主要框架和插件都已更新,但你可能会发现一些使用旧 API 的过时文章。

SSR 目标

SSR 构建的默认目标是 Node 环境,但你也可以将服务器运行在 Web Worker 中。不同平台的包入口解析方式不同。你可以通过将 ssr.target 设置为 'webworker' 来将目标配置为 Web Worker。

SSR 打包

在某些情况下(例如 webworker 运行时),你可能希望将 SSR 构建打包成一个单独的 JavaScript 文件。你可以通过将 ssr.noExternal 设置为 true 来启用此行为。这将带来两个效果:

  • 将所有依赖项视为 noExternal
  • 如果导入了任何 Node.js 内置模块,则抛出错误

SSR 解析条件

默认情况下,SSR 构建的包入口解析将使用 resolve.conditions 中设置的条件。你可以使用 ssr.resolve.conditionsssr.resolve.externalConditions 来自定义此行为。

Vite CLI

CLI 命令 $ vite dev$ vite preview 也可以用于 SSR 应用。你可以通过 configureServer 将 SSR 中间件添加到开发服务器,并通过 configurePreviewServer 将其添加到预览服务器。tip 提示
请使用后置钩子(post hook),以便你的 SSR 中间件在 Vite 的中间件之后运行。
:::

帮助我们改进文档

发现翻译问题或内容错误?请告诉我们。