Vite 服务端渲染(SSR)

Vite 服务端渲染(SSR)

示例项目

Vite 内置服务端渲染(SSR)支持。create-vite-extra 提供了以下 SSR 示例配置,可以结合本指南参考:

也可以运行 create-vite,在框架选项中选择 Others > create-vite-extra,将这些项目的基础结构生成到本地。 (running create-vite)

源文件结构

典型的 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()

这里的 vite 是 ViteDevServer 实例。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 中的 scripts 配置如下:

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,添加生产环境专用逻辑:

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

  • 用 import('./dist/server/entry-server.js') 替代 await vite.ssrLoadModule('/src/entry-server.js'),其中导入的文件就是 SSR 构建产物。

  • 将 vite 开发服务器的创建及所有使用位置移入仅在开发环境执行的条件分支,并添加静态文件中间件,提供 dist/client 中的文件。

可工作的完整配置请参考示例项目。 (example projects)

生成预加载指令

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 与对应 chunk 和资源文件之间的映射。

要使用这个清单,框架需要提供一种方法,收集一次服务端渲染调用过程中使用的组件模块 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。 (demo source)

预渲染与静态站点生成(SSG)

如果提前知道某些路由以及它们所需的数据,可以使用与生产 SSR 相同的逻辑,将这些路由预渲染为静态 HTML。这也可以视为静态站点生成(SSG)的一种形式。可运行的示例见演示项目的预渲染脚本。 (demo pre-render script)

SSR 外部依赖

运行 SSR 时,默认会把依赖从 Vite 的 SSR 转换模块系统中外部化,从而加快开发和构建。

如果某个依赖需要经过 Vite 转换流水线处理,例如它未经转译便使用了 Vite 特性,可以把它加入 ssr.noExternal。

对于通过链接引入的依赖,默认不会将其外部化,以便使用 Vite 的 HMR。如果不希望这样,例如想测试依赖未被链接时的行为,可以把它加入 ssr.external。

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...
      }
    },
  }
}

load 和 transform 中的 options 对象是可选的。Rollup 目前没有使用这个对象,但将来可能用额外的元数据扩展这些钩子。

SSR 目标环境

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

SSR 打包

在 Web Worker 等运行环境中,你可能希望把 SSR 构建打包成单个 JavaScript 文件。将 ssr.noExternal 设为 true 可启用这种行为,它会产生两个效果:

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

SSR 解析条件

默认情况下,SSR 构建的包入口解析使用 resolve.conditions 中设置的条件。可以通过 ssr.resolve.conditions 和 ssr.resolve.externalConditions 自定义这一行为。

Vite 命令行

命令行 $ vite dev 和 $ vite preview 也可以用于 SSR 应用。可以通过 configureServer 把 SSR 中间件加入开发服务器,通过 configurePreviewServer 加入预览服务器。

原文:服务端渲染(SSR)。Vite 文档团队(原页面为英文)。中文翻译与排版改编。原文版权归 Vite 项目及其贡献者;许可见 Vite LICENSE。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容