# 服务端渲染

注意

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

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

Low-level API

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

帮助

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

# 示例项目

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

  • Vue 3
  • React

# 源码结构

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

- index.html
- server.js # main application server
- src/
  - main.js          # 导出环境无关的(通用的)应用代码
  - entry-client.js  # 将应用挂载到一个 DOM 元素上
  - entry-server.js  # 使用某框架的 SSR API 渲染该应用

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

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

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

# 情景逻辑

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

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

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

# 设置开发服务器

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

server.js

import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import express from 'express'
import { createServer as createViteServer } from 'vite'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

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

  // 以中间件模式创建 Vite 应用,这将禁用 Vite 自身的 HTML 服务逻辑
  // 并让上级服务器接管控制
  const vite = await createViteServer({
    server: { middlewareMode: true },
    appType: 'custom'
  })

  // 使用 vite 的 Connect 实例作为中间件
  // 如果你使用了自己的 express 路由(express.Router()),你应该使用 router.use
  app.use(vite.middlewares)

  app.use('*', async (req, res) => {
    // 服务 index.html - 下面我们来处理这个问题
  })

  app.listen(5173)
}

createServer()

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

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

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

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

    // 2. 应用 Vite HTML 转换。这将会注入 Vite HMR 客户端,
    //    同时也会从 Vite 插件应用 HTML 转换。
    //    例如:@vitejs/plugin-react 中的 global preambles
    template = await vite.transformIndexHtml(url, template)

    // 3. 加载服务器入口。vite.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脚本也应该相应地改变,使用服务器脚本:

  "scripts": {
-   "dev": "vite"
+   "dev": "node server"
  }

# 生产环境构建

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

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

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条件分支,需要添加一些用于生产环境的特定逻辑:

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

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

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

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

# 生成预加载指令

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

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

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

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

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

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

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

# 预渲染 / SSG

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

# SSR 外部化

当运行 SSR 时依赖会由 Vite 的 SSR 转换模块系统作外部化。这会同时提速开发与构建。

例如,如果依赖项需要通过 Vite 的管道进行转换,因为在这些依赖在管道中使用 Vite 特性时是不转翻译的,则可以将它们添加到 ssr.noExternal 中。

使用别名

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

# SSR 专有插件逻辑

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

  • resolveId
  • load
  • transform

示例:

export function mySSRPlugin() {
  return {
    name: 'my-ssr',
    transform(code, id, options) {
      if (options?.ssr) {
        // 执行 ssr 专有转换...
      }
    },
  }
}

options中的 loadtransform为可选项,rollup 目前并未使用该对象,但将来可能会用额外的元数据来扩展这些钩子函数。

Note

Vite 2.7 之前的版本,会提示你 ssr参数的位置不应该是 options对象。目前所有主要框架和插件都已对应更新,但你可能还是会发现使用过时 API 的旧文章。

# SSR 构建目标

SSR 构建的默认目标为 node 环境,但你也可以让服务运行在 Web Worker 上。每个平台的打包条目解析是不同的。你可以将ssr.target设置为 webworker,以将目标配置为 Web Worker。

# SSR 构建产物

在某些如 webworker运行时等特殊情况中,你可能想要将你的 SSR 打包成单个 JavaScript 文件。你可以通过设置 ssr.noExternaltrue来启用这个行为。这将会做两件事:

  • 将所有依赖视为 noExternal(非外部化)
  • 若任何 Node.js 内置内容被引入,将抛出一个错误

# Vite CLI

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

注意

使用一个后置钩子,使得你的 SSR 中间件在 Vite 的中间件 之后运行。

# SSR 格式

默认情况下,Vite 生成的 SSR 打包产物是 ESM 格式。实验性地支持配置 ssr.format,但不推荐这样做。未来围绕 SSR 的开发工作将基于 ESM 格式,并且为了向下兼容,commonjs 仍然可用。如果你的 SSR 项目不能使用 ESM,你可以通过 Vite v2 外部启发式方法 设置 legacy.buildSsrCjsExternalHeuristics: true生成 CJS 格式的产物。

Last Updated: 6/17/2023, 6:57:19 PM