创建一个构建器

了解 Nuxt 构建器如何工作,以及如何编写你自己的构建器。

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

本指南解释构建器如何融入 Nuxt 构建过程、构建器必须满足什么契约,以及如何编写一个构建器。

编写构建器属于高级主题。大多数应用程序都不需要自定义构建器;如果你只是想影响打包结果,通常更适合使用一个注册打包器插件的 模块

构建器的作用

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

Nuxt 核心(Nuxt 的工作方式中描述的 nuxt 上下文)以及你的模块会生成虚拟应用:入口点、路由表、插件、组件注册表,以及 #build 下的其余虚拟文件系统。构建器接收这个虚拟应用,并将其转换为真正的 JavaScript 和 CSS 包;在开发环境中,它还会运行一个提供这些包并支持热重载的开发服务器。

具体来说,构建器负责:

  • 打包 客户端 构建(浏览器包),以及在启用 SSR 时打包 服务端 构建(SSR 应用入口)。
  • 生成服务器运行时用于渲染和水合所需的产物:客户端清单、按组件划分的样式映射等(参见构建输出契约)。
  • 在开发环境中,提供开发服务器,并在构建发生变化时触发重载。

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

构建器接口

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

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

export interface NuxtBuilder {
  bundle: (nuxt: Nuxt) => Promise<void>
  /**
   * 可选。当用户通过 `experimental.watcher: 'builder'` 选择启用时,
   * Nuxt 会调用这个方法,而不是启动自己的开发环境文件监听器,从而让
   * 构建器复用其自己的监听器。构建器应注册一个
   * `nuxt.hook('close', ...)` 来进行清理。
   */
  setupWatcher?: (nuxt: Nuxt) => Promise<void> | void
}

Nuxt 会根据 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 钩子。

只有三个官方构建器标识符(@nuxt/vite-builder@nuxt/webpack-builder@nuxt/rspack-builder)会在 Nuxt 的其他地方被按名称识别为构建器特定行为。自定义构建器仍然可以通过下面描述的通用契约正常工作。

构建生命周期

当你运行 nuxt buildnuxt dev 时,Nuxt 会:

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

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

  • 生产环境中,它会执行客户端构建以及(如果 nuxt.options.ssr)服务端构建直到完成,将产物写入 nuxt.options.buildDir,并将它们注册为构建输出。
  • 开发环境中,它会设置开发服务器,启动监听式构建,分配 nuxt.server,并持续运行。

构建器几乎完全通过 钩子 与 Nuxt 的其余部分通信。它从 nuxt.options 读取构建配置,并在合适的时候允许模块扩展其打包器配置。

让模块扩展打包器

模块通过 Nuxt Kit 辅助函数影响打包过程。构建器应当遵循相关的辅助函数:

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

构建输出契约

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

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

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

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

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

每个 nuxt/* 子路径都会在 nuxt 包中附带一个默认占位实现,因此即使在构建器运行之前,服务器运行时也始终能够通过类型检查并完成构建。构建器通过设置对应的构建输出来覆盖该占位实现;它提供的值会在构建时替换占位内容。

两种构建输出

这些键分为两种形态:

  • 值提供器serverEntryclientManifestclientPrecomputedentryChunkNameentryIds)是返回字符串形式模块主体的函数。该字符串会按原样内联到服务器包中,因此不能依赖文件在磁盘上的位置。比如,serverEntry 返回的模块主体会通过绝对 specifier 重新导出已构建的 SSR 入口:
    setBuildOutput('serverEntry', () => `export { default } from ${JSON.stringify(serverEntryURL)}`)
    
  • 已发出文件路径ssrStyles)是一个绝对路径(不是代码),指向构建器发出的真实模块。运行时对 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/* 导入时按需读取。这使得构建器可以提前注册提供器(例如在客户端构建完成之前),并在数据可用后返回最终结果。

一个最小示例

下面是一个满足生产构建契约的构建器骨架:

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')

  // ...在这里运行你的客户端和服务器打包,将产物写入磁盘...
  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)}`)

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

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

开发服务器

在开发环境中,builder 还负责提供服务和热重载应用。有两件事很重要:

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

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

进一步了解 Nuxt 接口以及构建与运行时之间的分离。
浏览 Nuxt Kit builder 工具,它们用于注册打包器插件和构建输出。