创建一个构建器
构建器 是 Nuxt 中负责打包你的应用程序的部分。Nuxt 默认附带三个官方构建器,Vite、webpack 和 Rspack,你可以通过 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 的模块标识符,也接受一个内联对象:
export default defineNuxtConfig({
// 导出 `{ bundle }` 的包
builder: '@nuxt/vite-builder',
})
import type { NuxtBuilder } from '@nuxt/schema'
const myBuilder: NuxtBuilder = {
async bundle (nuxt) {
// ...
},
}
export default defineNuxtConfig({
builder: myBuilder,
})
Nuxt 会在 nuxt build 和 nuxt dev 期间,在虚拟应用生成完成并且 build:before 钩子已触发之后,调用一次 bundle(nuxt)。Nuxt 会对你的 bundle 进行包装,因此任何抛出的错误都会自动触发 build:error 钩子。
@nuxt/vite-builder、@nuxt/webpack-builder、@nuxt/rspack-builder)会在 Nuxt 的其他地方被按名称识别为构建器专属行为。自定义构建器仍然可以通过下面描述的通用契约正常工作,但会被视为旧版(非 Vite 环境)路径。构建生命周期
当你运行 nuxt build 或 nuxt dev 时,Nuxt:
- 创建
nuxt上下文并运行模块,填充nuxt.options和构建钩子。 - 将虚拟应用(模板、路由表、插件)生成到
#build虚拟文件系统中。 - 触发
build:before。 - 解析构建器并调用
builder.bundle(nuxt)。这就是你的构建器运行的地方。 - 触发
build:done,并在生产环境中关闭nuxt实例。
你的 bundle 实现通常会根据 nuxt.options.dev 分支处理:
- 在生产环境中,它会完成客户端构建以及(如果
nuxt.options.ssr)服务端构建,将产物写入nuxt.options.buildDir并将其注册为构建输出。 - 在开发环境中,它会设置开发服务器,启动监视构建,分配
nuxt.server,并持续运行。
构建器几乎完全通过钩子与 Nuxt 的其他部分通信。它从 nuxt.options 读取构建配置,并在适当的时候允许模块扩展其打包器配置。
让模块扩展打包器
模块通过 Nuxt Kit 辅助函数影响构建。构建器应遵守相关的辅助函数:
addVitePlugin/addWebpackPlugin注册打包器特定插件。addBuildPlugin注册一个 unplugin 工厂,因此一个插件可以跨所有构建器工作。extendViteConfig/extendWebpackConfig修改已解析的打包器配置。
官方构建器也会发出它们自己的钩子(例如 vite:extendConfig、vite:serverCreated、webpack: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 map 导出哈希后的入口 chunk 文件名的模块主体。 */
entryChunkName: () => string | Promise<string>
/** 导出用于内联样式提取的入口模块 ID 的模块主体。 */
entryIds: () => string | Promise<string>
}
每个键都映射到服务器运行时导入的一个 nuxt/* 子路径:
| 构建输出 | 子路径 | 以何种形式导入 |
|---|---|---|
serverEntry | nuxt/entry | 传递给 createRenderer 的 SSR 应用工厂 |
clientManifest | nuxt/manifest | vue-bundle-renderer 客户端清单 |
clientPrecomputed | nuxt/precomputed | 预计算的依赖数据 |
ssrStyles | nuxt/styles | 按组件划分的内联样式映射 |
entryChunkName | nuxt/entry-chunk | 哈希后的入口 chunk 文件名(import map) |
entryIds | nuxt/entry-ids | 用于样式提取的入口模块 ID |
每个 nuxt/* 子路径都在 nuxt 包中提供了默认的占位实现,因此即使在构建器运行之前,服务器运行时也始终能够通过类型检查并完成构建。构建器通过设置对应的构建输出来覆盖这些占位实现;它提供的值会在构建时替换掉占位实现。
两种构建输出
这些键分为两种形式:
- 值提供器(
serverEntry、clientManifest、clientPrecomputed、entryChunkName、entryIds)是返回字符串形式模块主体的函数。该字符串会原样内联到服务器 bundle 中,因此它不能依赖文件在磁盘上的位置。例如,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'))
}
}
serverEntry 的默认值是一个无操作应用,并且大多数其他输出都不会被使用。开发服务器
在开发环境中,builder 还负责提供应用服务并进行热重载。有两件事很重要:
nuxt.server保存正在运行的开发服务器。Nuxt 的 CLI 会使用它,官方 builder 还会在其上暴露handler(一个 Node 请求监听器)、fetch(一个 Webfetch处理器)以及reload/close方法。是自行构建开发服务器,还是委托给 Nitro(来自nitro/builder的createDevServer),取决于 builder。- 重新加载 会在编译完成时通过调用
nuxt.server.reload()来触发。官方 builder 会发出compiled钩子(例如vite:compiled、webpack:compiled),服务器集成会监听这些钩子以便重新加载。
在开发模式下,构建输出通常会连接到实时的、内存中的源,而不是磁盘上的文件。例如,SSR 入口可能从打包器的内存输出中提供,而客户端清单则可能根据开发模块图计算得出,而不是从 nuxt.options.buildDir 读取。
构建器与 Vite Environment API
上面的契约是刻意与构建器无关的,它同时适用于旧路径(每个构建器运行自己的 bundle,而 Nitro 通过其自己的 Rollup 过程单独打包可部署产物)以及更新的 Vite Environment API 路径,在该路径中 Nitro 作为一个 Vite 环境运行。
对于自定义构建器,你只需要满足通用契约。Vite Environment API 集成(experimental.nitroViteEnvironment)是 @nuxt/vite-builder 特有的;其他构建器,包括自定义构建器,都会使用旧的 Nitro Rollup 路径。