跳到主要内容

创建 Builder

了解 Nuxt builder 是如何工作的,以及如何编写你自己的 builder。

builder(构建器)是 Nuxt 中负责打包你的应用的部分。Nuxt 自带三个官方 builder:Vite(默认)、webpackRspack,你可以通过 builder 选项选择其中一个,或者提供你自己的。

本指南解释了 builder 如何融入 Nuxt 构建、一个 builder 必须满足的契约,以及如何编写一个 builder。

编写 builder 是一个进阶主题。大多数应用从不需要自定义 builder;如果你只是想影响打包产物,一个注册打包工具插件的模块通常是正确的工具。

Builder 做了什么

Nuxt 将构建什么如何构建分离开来。

Nuxt 核心(在《Nuxt 是如何工作的》中描述的 nuxt 上下文)和你的模块生成虚拟应用:入口点、路由表、插件、组件注册表,以及 #build 下的虚拟文件系统 的其余部分。builder 接收这个虚拟应用,并将其转换为真实的 JavaScript 和 CSS bundle,并在开发模式下运行一个 dev server 来提供并热重载它们。

具体来说,一个 builder 负责:

  • 打包客户端构建(浏览器 bundle),以及在启用 SSR 时打包服务端构建(SSR 应用入口)。
  • 生成服务端运行时渲染和水合所需的产物:客户端 manifest、按组件的样式映射,等等(参见构建产物契约)。
  • 在开发模式下,暴露一个 dev server,并在构建变化时触发重载。

可部署的服务器本身由 Nitro 通过 @nuxt/nitro-server 生成,而不是由 builder 生成。builder 通过一个类型化的契约将其输出移交给 Nitro;Nitro 将它们打包进最终的 .output

Builder 接口

一个 builder 是一个实现了 NuxtBuilder 接口的对象。唯一必需的方法是 bundle

import type { Nuxt } from '@nuxt/schema'

export interface NuxtBuilder {
  bundle: (nuxt: Nuxt) => Promise<void>
  /**
   * 可选。当用户通过 `experimental.watcher: 'builder'` 选择加入时,
   * Nuxt 会调用它,而不是启动它自己的 dev 文件监视器,让
   * builder 复用它自己的监视器。builder 应该注册一个
   * `nuxt.hook('close', ...)` 来进行清理。
   */
  setupWatcher?: (nuxt: Nuxt) => Promise<void> | void
}

Nuxt 从 builder 选项中解析当前活动的 builder。它接受一个默认导出 NuxtBuilder 的模块标识符,或者一个内联对象:

nuxt.config.ts
export default defineNuxtConfig({
  // 一个导出了 `{ bundle }` 的包
  builder: '@nuxt/vite-builder',
})
nuxt.config.ts
import type { NuxtBuilder } from '@nuxt/schema'

const myBuilder: NuxtBuilder = {
  async bundle (nuxt) {
    // ...
  },
}

export default defineNuxtConfig({
  builder: myBuilder,
})

Nuxt 在 nuxt buildnuxt dev 期间调用 bundle(nuxt) 一次,在虚拟应用已生成且 build:before 钩子已触发之后。Nuxt 包裹你的 bundle,这样任何抛出的错误都会自动触发 build:error 钩子。

只有三个官方 builder 标识符(@nuxt/vite-builder@nuxt/webpack-builder@nuxt/rspack-builder)会被 Nuxt 中其它地方按名称识别为 builder 特定行为。自定义 builder 仍然通过上述通用契约工作,但会被当作旧版(非 Vite 环境)路径处理。

构建生命周期

当你运行 nuxt buildnuxt dev 时,Nuxt:

  1. 创建 nuxt 上下文并运行模块,填充 nuxt.options 和构建钩子。
  2. 将虚拟应用(模板、路由表、插件)生成到 #build 虚拟文件系统中。
  3. 触发 build:before
  4. 解析 builder 并调用 builder.bundle(nuxt)这就是你的 builder 运行的地方。
  5. 触发 build:done,并在生产模式下关闭 nuxt 实例。

你的 bundle 实现通常会根据 nuxt.options.dev 分支:

  • 生产模式下,它运行客户端和(如果 nuxt.options.ssr)服务端构建直到完成,将产物写入 nuxt.options.buildDir 并将它们注册为构建输出。
  • 开发模式下,它设置一个 dev server,启动一个监视型构建,赋值 nuxt.server,并保持运行。

一个 builder 几乎完全通过钩子与 Nuxt 的其余部分通信。它从 nuxt.options 读取构建配置,并在适当的时候让模块扩展它的打包工具配置。

让模块扩展打包工具

模块通过 Nuxt Kit 辅助函数影响打包产物。一个 builder 应该遵循相关的那些:

官方 builder 也会发出它们自己的钩子(例如 vite:extendConfigvite:serverCreatedwebpack:config),以便模块和 Nitro 参与构建。自定义 builder 可以发出它自己的钩子,但下面的构建产物契约才是让它与 Nuxt 服务端运行时互操作的关键。

构建产物契约

服务端运行时(@nuxt/nitro-server)不知道是哪个 builder 生成了这个应用。它通过稳定的 nuxt/* 子路径导入每个构建产物,而当前活动的 builder 会用 nuxt.buildOutputs 填充这些子路径。这就是构建产物契约

该契约由 @nuxt/schema 中的 NuxtBuildOutputs 接口声明:

export interface NuxtBuildOutputs {
  /** 重新导出 SSR 应用入口的模块主体。 */
  serverEntry: () => string | Promise<string>
  /** 发出的按组件 SSR 样式映射路径,未产生内联样式时为 `undefined`。 */
  ssrStyles: string | undefined
  /** 给 `vue-bundle-renderer` 用的序列化客户端 manifest。 */
  clientManifest: () => string | Promise<string>
  /** 给 `vue-bundle-renderer` 用的序列化预计算客户端依赖数据。 */
  clientPrecomputed: () => string | Promise<string>
  /** 导出用于 import map 的哈希入口 chunk 文件名的模块主体。 */
  entryChunkName: () => string | Promise<string>
  /** 导出用于内联样式提取的入口模块 ID 的模块主体。 */
  entryIds: () => string | Promise<string>
}

每个键都映射到一个服务端运行时导入的 nuxt/* 子路径:

构建产物子路径作为
serverEntrynuxt/entry传给 createRenderer 的 SSR 应用工厂
clientManifestnuxt/manifestvue-bundle-renderer 客户端 manifest
clientPrecomputednuxt/precomputed预计算依赖数据
ssrStylesnuxt/styles按组件内联样式映射
entryChunkNamenuxt/entry-chunk哈希入口 chunk 文件名(import map)
entryIdsnuxt/entry-ids用于样式提取的入口模块 ID

每个 nuxt/* 子路径在 nuxt 包中都附带一个默认的 stub,因此即使在 builder 运行之前,服务端运行时总是能通过类型检查并构建。builder 通过设置匹配的构建输出来覆盖 stub;它提供的值会在构建时替换 stub。

两类构建产物

这些键有两种形态:

  • 值提供者serverEntryclientManifestclientPrecomputedentryChunkNameentryIds)是返回模块主体字符串的函数。该字符串会被逐字内联进服务端 bundle,因此它不能依赖于磁盘上文件的位置。例如,serverEntry 返回一个通过绝对标识符重新导出已构建 SSR 入口的主体:
    setBuildOutput('serverEntry', () => `export { default } from ${JSON.stringify(serverEntryURL)}`)
    
  • 发出的文件路径ssrStyles)是 builder 发出的一个真实模块的绝对路径(不是代码)。运行时的 nuxt/styles 导入解析到该文件,因此可部署包的打包工具会相对于该文件自身的位置解析样式映射中的相对同级导入。将其建模为代码字符串会剥离那个目录上下文并破坏相对导入,所以这是一个路径:
    setBuildOutput('ssrStyles', resolve(serverOutDir, 'styles.mjs'))
    

    当构建不产生内联样式时,将 ssrStyles 保留为 undefined(其默认值);运行时会回退到一个空的样式映射。

设置构建产物

使用 @nuxt/kit 中的 setBuildOutput 辅助函数:

import { setBuildOutput } from '@nuxt/kit'

setBuildOutput('clientManifest', () => 'export default ' + serializedManifest)

setBuildOutput 写入 nuxt.buildOutputs[key]。在已经持有 nuxt 实例的打包工具插件内部,你可以直接赋值 nuxt.buildOutputs[key]setBuildOutput 是为通过 useNuxt() 解析 nuxt 的代码提供的便利。

一个提供者可以是异步的,并在服务端构建解析相应的 nuxt/* 导入时被惰性读取。这让 builder 可以尽早注册提供者(例如在客户端构建完成之前),并在数据存在时返回最终值。

一个最小示例

一个在生产构建中满足契约的 builder 的骨架:

import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { setBuildOutput } from '@nuxt/kit'
import type { NuxtBuilder } from '@nuxt/schema'

export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
  const serverDir = resolve(nuxt.options.buildDir, 'dist/server')

  // ... 在这里运行你的客户端和服务端 bundle,将产物写入磁盘 ...
  const { serializedClientManifest } = await runBundles(nuxt, serverDir)

  if (nuxt.options.ssr) {
    // 将 `nuxt/entry` 指向已构建的 SSR 应用入口。
    const serverEntryURL = pathToFileURL(resolve(serverDir, 'server.mjs')).href
    setBuildOutput('serverEntry', () => `export { default } from ${JSON.stringify(serverEntryURL)}`)

    // 提供客户端构建产生的客户端 manifest。
    setBuildOutput('clientManifest', () => `export default ${serializedClientManifest}`)

    // 如果你在 CSS chunk 旁边发出了一个按组件的样式映射:
    setBuildOutput('ssrStyles', resolve(serverDir, 'styles.mjs'))
  }
}
你很少需要提供每一个键。默认值都很合理(空的 manifest、没有内联样式、未定义的入口 chunk),所以只提供你的构建所产生的内容。当 SSR 被禁用时,serverEntry 的默认值是一个空操作的应用,大多数其它输出都不会被使用。

开发服务器

在开发模式下,builder 还负责提供和热重载应用。有两件事很重要:

  • nuxt.server 持有正在运行的 dev server。Nuxt 的 CLI 消费它,官方 builder 在它上面暴露一个 handler(一个 Node 请求监听器)、一个 fetch(一个 web fetch 处理器)以及 reload / close 方法。你是构建自己的 dev server 还是委托给 Nitro 的(nitro/builder 中的 createDevServer)由 builder 决定。
  • 重载是在编译完成时通过调用 nuxt.server.reload() 来发出信号。官方 builder 发出一个 compiled 钩子(例如 vite:compiledwebpack:compiled),服务端集成会监听它来进行重载。

在 dev 模式下,构建产物通常被连接到活的、内存中的源,而不是磁盘上的文件。例如,SSR 入口可能从打包工具的内存中输出提供,客户端 manifest 可能从 dev 模块图计算得出,而不是从 nuxt.options.buildDir 读取。

Builders 与 Vite Environment API

上面的契约特意做到了与 builder 无关,并且对旧版路径(每个 builder 运行它自己的 bundle,Nitro 通过它自己的 Rollup 过程单独打包可部署产物)和较新的 Vite Environment API 路径(Nitro 作为一个 Vite environment 运行)都适用。

对于一个自定义 builder,你只需要满足通用契约。Vite Environment API 集成(experimental.nitroViteEnvironment)是 @nuxt/vite-builder 特有的;其它 builder,包括自定义 builder,使用旧的 Nitro Rollup 路径。

了解更多关于 Nuxt 接口以及构建与运行时的分离。

浏览用于注册打包工具插件和构建产物的 Nuxt Kit builder 工具函数。