本篇系统整理 Rollup 官方「插件开发」文档中的技术内容,并结合前两篇 《Rollup》与《Rollup 核心插件系统》做更偏“实践视角”的串联,帮助你从零编写、 调试并发布一个高质量的 Rollup 插件。
Rollup 插件本质上是一个普通的 JavaScript 对象,它:
name、version)一个典型的插件通常以“工厂函数”的形式对外导出:
插件可以用来完成:
node_modules 中解析第三方依赖import.meta 或文件 URL 等在上一篇《Rollup 核心插件系统》里,我们从宏观上讲了“插件在生命周期的什么阶段能做什么事”; 本篇则对官方文档里每个钩子、上下文和惯例做更细粒度的技术说明。
下面是官方文档中的一个经典示例:拦截对虚拟模块 virtual-module 的导入,
并为它返回一段“虚拟源码”,整个过程完全不访问文件系统。
// rollup-plugin-my-example.js
export default function myExample() {
return {
name: "my-example",
resolveId(source) {
if (source === "virtual-module") {
return source
}
return null
},
load(id) {
if (id === "virtual-module") {
return 'export default "This is virtual!"'
}
return null
}
}
}在 Rollup 配置中启用该插件,并将入口改为虚拟模块:
// rollup.config.js
import myExample from "./rollup-plugin-my-example.js"
export default {
input: "virtual-module",
plugins: [myExample()],
output: [
{
file: "bundle.js",
format: "es"
}
]
}关键点:
resolveId 负责“声明”:virtual-module 这个模块 ID 由本插件处理load 返回该模块的源码字符串这类“虚拟模块”技巧在很多插件中都很常见,例如:
为了让插件更易于在社区中被发现、理解与维护,官方文档给出了一系列约定:
命名约定
rollup-plugin- 作为前缀package.json 中添加 rollup-plugin 关键词,便于搜索测试与异步 IO
fs.promises.readFile),避免阻塞构建文档与说明
Source Map
transform 钩子等),应当生成并返回正确的 Source Map虚拟模块 ID 前缀
\0 前缀\0my-plugin-runtime官方文档在“属性”章节重点强调了两个字段。
namestring这是插件对象中唯一必填的字段。没有 name 的插件会导致调试体验非常糟糕。
versionstring该字段通常直接与 package.json 中的版本保持一致。
为了与 Rollup 的构建流程交互,插件可以实现一组构建阶段钩子。 官方文档按“钩子类型”说明了它们的执行方式:
async:钩子可以返回 Promise;否则视为同步first:若多个插件实现该钩子,按顺序执行,直到某个钩子返回非 null/undefinedsequential:所有插件的该钩子按顺序一个接一个执行parallel:多个插件可以并行执行该钩子除此之外,很多钩子既可以写成函数,也可以写成“带配置对象”的形式,详见后文。
下面按调用时机梳理每个构建阶段钩子的职责。
options类型:sequential
调用时机:
函数签名(简化):
options(inputOptions)返回值:
inputOptions 对象,用于覆盖或扩展配置null/undefined,则沿用原有配置典型用途:
buildStart类型:sequential
调用时机:
签名(简化):
buildStart(inputOptions)典型用途:
this.addWatchFile 注册额外的监听文件resolveId类型:first
调用时机:
import 或 require(通过 CommonJS 插件转换后)时签名(简化):
resolveId(source, importer, options)其中:
source: 源码中出现的导入路径importer: 当前模块的绝对路径(入口模块为 undefined)options.custom: 供插件间通信使用的自定义选项返回值:
null/undefined:交给下一个插件处理,最终由 Rollup 默认解析逻辑兜底{ id, external, moduleSideEffects, ... }典型用途:
resolveDynamicImport类型:first
调用时机:
import(expr) 时签名(简化):
resolveDynamicImport(specifier, importer, options)返回值:
resolveId,允许返回字符串/对象或 null典型用途:
load类型:first
调用时机:
签名(简化):
load(id)返回值:
null/undefined:交给下一个插件或默认文件读取逻辑{ code, map }典型用途:
transform类型:sequential
调用时机:
load 后签名(简化):
transform(code, id)返回值:
null/undefined:表示未修改代码{ code, map, ast, moduleSideEffects, syntheticNamedExports }典型用途:
shouldTransformCachedModule类型:first
调用时机:
transform签名(简化):
shouldTransformCachedModule(options)作用:
moduleParsed类型:sequential
调用时机:
签名(简化):
moduleParsed(moduleInfo)moduleInfo 中包含:
meta 字段可存放插件自定义元数据典型用途:
onLogsequential插件可以通过该钩子:
buildEnd类型:sequential
调用时机:
签名(简化):
buildEnd(error?)典型用途:
error 参数输出构建失败的统计信息watchChange类型:sequential
调用时机:
--watch 模式下,当某个文件发生变更时签名(简化):
watchChange(id, change)change 一般包含:
event: "create" | "update" | "delete"典型用途:
closeWatchersequentialwatch 监听器关闭时watch 相关的持久连接、临时文件等官方文档特别说明:除直接提供函数外,很多钩子也可以以对象形式声明:
export default function resolveFirst() {
return {
name: "resolve-first",
resolveId: {
order: "pre",
handler(source) {
if (source === "external") {
return { id: source, external: true }
}
return null
}
}
}
}当钩子是对象时,真正的处理逻辑写在 handler 字段里,额外支持:
order: "pre" | "post" | null
"pre":在实现同一钩子的所有插件之前执行"post":在实现同一钩子的所有插件之后执行null 或未设置:遵循 plugins 数组的默认顺序sequential: boolean
parallel 类型的钩子生效true,则该插件的该钩子不会与其他插件并行,而是等待前序钩子完成后单独执行这在需要严格顺序时非常有用,例如:
writeBundle 钩子需要串行执行且有依赖关系当 Rollup 开始从模块图生成最终 bundle 时,会进入“输出阶段”。插件可以在这一阶段:
下面按典型调用顺序介绍这些钩子。
outputOptions类型:sequential
调用时机:
签名(简化):
outputOptions(outputOptions)返回值:
outputOptions,类似构建阶段的 options 钩子典型用途:
renderStart类型:sequential
调用时机:
签名(简化):
renderStart(outputOptions, inputOptions)典型用途:
banner / footer / intro / outro这些钩子可以是字符串或返回字符串的函数,也可以是带 handler 的对象。
banner:插入在 bundle 代码的最开头footer:插入在 bundle 代码的结尾intro:插入在自执行函数或模块包装内部的开头outro:插入在自执行函数或模块包装内部的结尾常见用途:
augmentChunkHash类型:sequential
调用时机:
签名(简化):
augmentChunkHash(chunkInfo)返回值:
典型用途:
renderChunk类型:sequential
调用时机:
签名(简化):
renderChunk(code, chunk, options)返回值:
transform 类似,可以返回新的 { code, map }典型用途:
renderDynamicImportimport() 在不同格式下如何呈现通过该钩子可以:
resolveFileUrl调用时机:
import.meta.ROLLUP_FILE_URL_* 相关代码时签名(简化):
resolveFileUrl(options)其中 options 包含:
fileNamereferenceIdchunkId 等信息典型用途:
resolveImportMetaimport.meta.* 表达式时典型用途:
import.meta.env、import.meta.url 等注入特定实现generateBundle类型:sequential
调用时机:
签名(简化):
generateBundle(outputOptions, bundle, isWrite)bundle 是一个对象:
典型用途:
isWrite 决定是“只读分析”还是“实际写文件”writeBundle类型:sequential
调用时机:
签名与 generateBundle 类似:
writeBundle(outputOptions, bundle)典型用途:
renderError典型用途:
closeBundle典型用途:
this)常用方法在每个钩子内部,this 指向插件上下文对象。官方文档列出了大量实用方法,
其中常用的包括:
this.addWatchFile(id)
--watch 模式下监听额外文件this.getWatchFiles()
this.emitFile(descriptor)
{ type: "asset", name, source }{ type: "chunk", id, name }this.setAssetSource(assetId, source)
emitFile 声明的 asset 设置或更新内容this.getFileName(fileId)
emitFile 返回的 fileId 获取最终生成的文件名this.warn(message | RollupLog)
this.error(message | RollupError)
this.info(message | RollupLog)
this.debug(message | RollupLog)
this.load(options)
this.parse(code, options?)
this.resolve(source, importer, options?)
this.getModuleIds()
this.getModuleInfo(id)
meta 字段)this.getCombinedSourcemap()
transform 链之后的合并 Source Mapthis.meta
这些 API 是实现复杂插件(如分析工具、可视化工具、资源管理器)的基础。
import.meta官方文档针对“文件 URL”专门有一节说明,核心机制是:
this.emitFile 声明 asset 或 chunkimport.meta.ROLLUP_FILE_URL_referenceIdresolveFileUrl 钩子,最终替换成实际 URL 字符串典型流程示意:
// 在插件中
const imageId = this.emitFile({
type: "asset",
name: "logo.png",
source: imageBuffer
})
// 在用户源码中(经由插件注入)
const logoUrl = import.meta.ROLLUP_FILE_URL_imageId在 resolveFileUrl 钩子中,可以根据:
来生成不同形式的 URL,从而适配多种部署环境。
同时,resolveImportMeta 钩子允许插件自定义处理 import.meta.*:
import.meta.env.* 映射到构建时环境变量import.meta.url 的行为官方文档中把很多“只做源码转换”的插件统称为“转换器(transformers)”,
它们通常只实现 transform 钩子。
一个典型的转换器插件大致结构如下:
export default function myTransformPlugin() {
return {
name: "my-transform",
transform(code, id) {
if (!id.endsWith(".js")) return null
const transformed = someTransform(code)
return {
code: transformed.code,
map: transformed.map
}
}
}
}关键点:
id 决定是否处理某个模块{ code, map } 保证 Source Map 正确串联比如实现一个“移除所有 console.* 调用”的简单插件:
export default function stripConsole() {
return {
name: "strip-console",
transform(code, id) {
if (!id.endsWith(".js")) return null
const next = code.replace(/\bconsole\.[a-zA-Z]+\([^;]*\);?\s*/g, "")
if (next === code) return null
return { code: next, map: null }
}
}
}这类插件不依赖模块图或输出阶段,只基于源码字符串工作。
对于那些只提供默认导出、没有显式命名导出的模块,Rollup 提供
syntheticNamedExports 概念;插件也可以在 transform 返回值中设置:
return {
code,
map,
syntheticNamedExports: true
}这样 Rollup 会尝试基于返回的代码为该模块创建“合成的命名导出”,以改善互操作性。
在大型工程中,多种插件之间往往需要协作。官方文档总结了几种推荐的通信方式。
this.resolve 和 resolveId 钩子都支持一个 custom 字段,用于在解析时传递额外信息:
// 在插件 A 中
const resolved = await this.resolve(source, importer, {
custom: {
"my-plugin": { needRawId: true }
}
})在插件 B 的 resolveId 中可以读取:
resolveId(source, importer, options) {
const custom = options.custom && options.custom["my-plugin"]
if (custom && custom.needRawId) {
// 按插件 A 的需求执行特殊逻辑
}
}这种方式适用于多个解析插件协同工作时传递额外上下文。
每个模块的 moduleInfo.meta 字段可以挂载任意插件自定义数据:
// 在某个插件中
moduleParsed(info) {
info.meta.myPlugin = {
hasSpecialExport: checkSomething(info.ast)
}
}其他插件可以在后续钩子中通过 this.getModuleInfo(id) 获取这些信息,从而实现跨插件的数据共享。
插件对象本身也可以暴露一个 api 字段,供其他插件直接调用:
export default function corePlugin() {
const state = new Map()
return {
name: "core-plugin",
api: {
register(id, info) {
state.set(id, info)
},
get(id) {
return state.get(id)
}
}
}
}其他插件可以在 Rollup 配置中拿到该插件实例后,通过 api 访问其中的方法。
这种模式适合插件之间存在明确依赖关系、且需要共享较复杂逻辑时使用。
本篇将官方「插件开发」文档中的技术要点按“插件对象 → 钩子 → 上下文 → 文件 URL → 转换器 → 插件间通信”的顺序系统梳理,结合本系列前两篇,你可以用以下步骤 练习并掌握插件开发:
resolveId 与 load 的“虚拟模块”插件transform 的简单转换器(如删除调试代码)moduleParsed 做简单的 AST 分析generateBundle 中基于 bundle 生成清单文件import.meta.ROLLUP_FILE_URL_* 和 resolveFileUrl 处理静态资源掌握这些内容后,你就可以在 Rollup 之上搭建高度定制的构建流程,甚至为其他打包工具 (如 Vite)贡献插件生态。