跳转到内容

服务端渲染 (SSR)

注意

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

本指南假设你具备所选框架中 SSR 的使用经验,并将重点介绍 Vite 特有的集成细节。

底层 API

这是一个面向库和框架作者的底层 API。如果你的目标是创建一个应用程序,请务必先查看 Awesome Vite SSR 部分中的高级 SSR 插件和工具。即便如此,许多应用程序已经成功直接构建在 Vite 的原生底层 API 之上。

目前,Vite 正在通过 环境 API (Environment API) 开发更完善的 SSR API。查看链接以了解更多详情。

示例项目

Vite 提供了对服务端渲染 (SSR) 的内置支持。create-vite-extra 包含了一些 SSR 示例配置,你可以将其作为本指南的参考:

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

源码结构

一个典型的 SSR 应用程序具有以下源码结构:

- index.html
- server.js # main application server
- src/
  - main.js          # exports env-agnostic (universal) app code
  - entry-client.js  # mounts the app to a DOM element
  - entry-server.js  # renders the app using the framework's SSR API

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

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

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

条件逻辑

如果你需要根据 SSR 或客户端执行条件逻辑,可以使用:

js
if (import.meta.
env
.
SSR
) {
// ... server only logic }

这会在构建过程中进行静态替换,从而允许对未使用的分支进行摇树优化 (tree-shaking)。

配置开发服务器

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

server.js
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
()
// Create Vite server in middleware mode and configure the app type as // 'custom', disabling Vite's own HTML serving logic so parent server // can take control const
vite
= await
createViteServer
({
server
: {
middlewareMode
: true },
appType
: 'custom'
}) // Use vite's connect instance as middleware. If you use your own // express router (express.Router()), you should use router.use // When the server restarts (for example after the user modifies // vite.config.js), `vite.middlewares` is still going to be the same // reference (with a new internal stack of Vite and plugin-injected // middlewares). The following is valid even after restarts.
app
.
use
(
vite
.
middlewares
)
app
.
use
('*all', async (
req
,
res
) => {
// serve index.html - we will tackle this next })
app
.
listen
(5173)
}
createServer
()

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

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

server.js
js
app
.
use
('*all', async (
req
,
res
,
next
) => {
const
url
=
req
.
originalUrl
try { // 1. Read index.html let
template
=
fs
.
readFileSync
(
path
.
resolve
(import.meta.
dirname
, 'index.html'),
'utf-8', ) // 2. Apply Vite HTML transforms. This injects the Vite HMR client, // and also applies HTML transforms from Vite plugins, e.g. global // preambles from @vitejs/plugin-react
template
= await
vite
.
transformIndexHtml
(
url
,
template
)
// 3. Load the server entry. ssrLoadModule automatically transforms // ESM source code to be usable in Node.js! There is no bundling // required, and provides efficient invalidation similar to HMR. const {
render
} = await
vite
.
ssrLoadModule
('/src/entry-server.js')
// 4. render the app HTML. This assumes entry-server.js's exported // `render` function calls appropriate framework SSR APIs, // e.g. ReactDOMServer.renderToString() const
appHtml
= await
render
(
url
)
// 5. Inject the app-rendered HTML into the template. const
html
=
template
.
replace
(`<!--ssr-outlet-->`, () =>
appHtml
)
// 6. Send the rendered HTML back.
res
.
status
(200).
set
({ 'Content-Type': 'text/html' }).
end
(
html
)
} catch (
e
) {
// If an error is caught, let Vite fix the stack trace so it maps back // to your actual source code.
vite
.
ssrFixStacktrace
(
e
)
next
(
e
)
} })

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

package.json
diff
  "scripts": {
-   "dev": "vite"
+   "dev": "node server"
  }

生产构建

要发布 SSR 项目用于生产,我们需要:

  1. 像往常一样构建客户端;
  2. 构建 SSR 代码,使其可以通过 import() 直接加载,这样我们就无需经过 Vite 的 ssrLoadModule

我们在 package.json 中的脚本如下所示:

package.json
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 作为模板,因为它包含了指向客户端构建的正确资源链接。

  • 使用 import('./dist/server/entry-server.js') 来代替 await vite.ssrLoadModule('/src/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 清单是从客户端构建生成的,因为我们需要将模块 ID 映射到客户端文件)。该清单包含模块 ID 到其关联块 (chunks) 和资源文件的映射。

为了利用该清单,框架需要提供一种方法来收集在服务端渲染调用期间使用的组件模块 ID。

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

src/entry-server.js
js
const ctx = {}
const html = await vueServerRenderer.renderToString(app, ctx)
// ctx.modules is now a Set of module IDs that were used during the render

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

预渲染 / SSG

如果路由以及特定路由所需的数据是预先知道的,我们可以使用与生产环境 SSR 相同的逻辑将这些路由预渲染为静态 HTML。这也可以被视为静态站点生成 (SSG) 的一种形式。请参阅演示预渲染脚本以获取可运行示例。

SSR 外部化 (Externals)

在运行 SSR 时,依赖项默认会从 Vite 的 SSR 转换模块系统中“外部化”。这加快了开发和构建速度。

如果某个依赖项需要经过 Vite 的流水线转换(例如,因为 Vite 特性在其中未经转换地使用),则可以将它们添加到 ssr.noExternal 中。

对于链接的依赖项,为了利用 Vite 的 HMR,它们默认不会被外部化。如果不需要这样做(例如,为了测试依赖项是否被链接),可以将其添加到 ssr.external 中。

使用别名 (Aliases)

如果你配置了将一个包重定向到另一个包的别名,你可能希望为 node_modules 中的包设置别名,以便使 SSR 外部化的依赖项正常工作。Yarn 和 pnpm 都支持通过 npm: 前缀进行别名设置。

SSR 特定的插件逻辑

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

  • resolveId
  • load
  • transform

示例

js
export function 
mySSRPlugin
() {
return {
name
: 'my-ssr',
transform
(
code
,
id
,
options
) {
if (
options
?.
ssr
) {
// perform ssr-specific transform... } }, } }

loadtransform 中的 options 对象是可选的,Rollup 目前未使用此对象,但未来可能会使用附加元数据扩展这些钩子。

注意

在 Vite 2.7 之前,插件钩子是通过位置参数 ssr 而不是使用 options 对象来获得此信息的。所有主流框架和插件都已更新,但你可能会发现使用旧 API 的过时文章。

SSR 目标 (Target)

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

SSR 捆绑 (Bundle)

在某些情况下(如 webworker 运行时),你可能希望将 SSR 构建捆绑到一个单独的 JavaScript 文件中。你可以通过设置 ssr.noExternaltrue 来启用此行为。这将执行两件事:

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

SSR 解析条件

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

Vite CLI

CLI 命令 $ vite dev$ vite preview 也可以用于 SSR 应用。你可以通过 configureServer 将你的 SSR 中间件添加到开发服务器,通过 configurePreviewServer 添加到预览服务器。

注意

使用后置钩子 (post hook),以便你的 SSR 中间件在 Vite 的中间件之后运行。