Rollup 插件开发
本篇系统整理 Rollup 官方「插件开发」文档中的技术内容,并结合前两篇 《Rollup》与《Rollup 核心插件系统》做更偏“实践视角”的串联,帮助你从零编写、 调试并发布一个高质量的 Rollup 插件。
1. 插件概述
Rollup 插件本质上是一个普通的 JavaScript 对象,它:
- 拥有若干属性(如
name、version) - 实现一个或多个构建阶段钩子
- 实现一个或多个输出生成阶段钩子
一个典型的插件通常以“工厂函数”的形式对外导出:
- 包作为一个 npm 包发布
- 默认导出一个函数
- 调用该函数可以传入插件特定的选项
- 函数返回插件对象
插件可以用来完成:
- 在打包前对源码进行转译或注入辅助代码
- 从
node_modules中解析第三方依赖 - 在构建结束后生成额外的产物(清单、报告等)
- 对 chunk 进行二次处理、重写
import.meta或文件 URL 等
在上一篇《Rollup 核心插件系统》里,我们从宏观上讲了“插件在生命周期的什么阶段能做什么事”; 本篇则对官方文档里每个钩子、上下文和惯例做更细粒度的技术说明。
2. 一个简单的插件示例
下面是官方文档中的一个经典示例:拦截对虚拟模块 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返回该模块的源码字符串- 对使用方来说,这和从真实文件系统加载一个模块没有区别
这类“虚拟模块”技巧在很多插件中都很常见,例如:
- 插件内部抽出公共 runtime 代码
- 从配置生成一段虚拟的入口模块
- 注入 polyfill 或调试工具等
3. 插件编写约定
为了让插件更易于在社区中被发现、理解与维护,官方文档给出了一系列约定:
-
命名约定
- 插件名称建议以
rollup-plugin-作为前缀 - 在
package.json中添加rollup-plugin关键词,便于搜索
- 插件名称建议以
-
测试与异步 IO
- 插件应该有测试,官方推荐使用支持 Promise 的测试框架(如 Mocha、AVA)
- 在可能的情况下优先使用异步 API(例如
fs.promises.readFile),避免阻塞构建
-
文档与说明
- 使用英文撰写插件的 README,以便更多开发者理解和使用
- 明确说明插件的配置项、支持的 Rollup 版本以及典型使用场景
-
Source Map
- 如果插件会修改源码(通过
transform钩子等),应当生成并返回正确的 Source Map - 这对调试、错误栈定位、性能分析等都非常重要
- 如果插件会修改源码(通过
-
虚拟模块 ID 前缀
- 若插件内部使用“虚拟模块”(如注入辅助函数、工具模块),推荐使用
\0前缀 - 如:
\0my-plugin-runtime - 作用:
- 防止其他插件尝试去解析这个 ID
- 避免与真实文件路径冲突
- 若插件内部使用“虚拟模块”(如注入辅助函数、工具模块),推荐使用
4. 插件对象的核心属性
官方文档在“属性”章节重点强调了两个字段。
4.1 name
- 类型:
string - 作用:
- 在日志、警告和错误信息中标识插件
- 便于用户快速定位是哪一个插件报错
这是插件对象中唯一必填的字段。没有 name 的插件会导致调试体验非常糟糕。
4.2 version
- 类型:
string - 作用:
- 主要用于插件之间的通信场景
- 某些插件可能会根据另外一个插件的版本号决定行为
该字段通常直接与 package.json 中的版本保持一致。
5. 构建阶段钩子(Build Hooks)
为了与 Rollup 的构建流程交互,插件可以实现一组构建阶段钩子。 官方文档按“钩子类型”说明了它们的执行方式:
async:钩子可以返回 Promise;否则视为同步first:若多个插件实现该钩子,按顺序执行,直到某个钩子返回非null/undefinedsequential:所有插件的该钩子按顺序一个接一个执行parallel:多个插件可以并行执行该钩子
除此之外,很多钩子既可以写成函数,也可以写成“带配置对象”的形式,详见后文。
下面按调用时机梳理每个构建阶段钩子的职责。
5.1 options
-
类型:
sequential -
调用时机:
- 在每次构建开始前
- 在 Rollup 解析完用户传入的基础配置后
-
函数签名(简化):
tsoptions(inputOptions) -
返回值:
- 可以返回新的
inputOptions对象,用于覆盖或扩展配置 - 若返回
null/undefined,则沿用原有配置
- 可以返回新的
典型用途:
- 动态修改入口文件、外部依赖或插件列表
- 根据自定义环境变量,注入额外配置
5.2 buildStart
-
类型:
sequential -
调用时机:
- 构建正式开始时
- 所有配置已经生效
-
签名(简化):
tsbuildStart(inputOptions)
典型用途:
- 初始化插件内部状态或缓存
- 校验外部依赖、环境变量是否存在
- 使用
this.addWatchFile注册额外的监听文件
5.3 resolveId
-
类型:
first -
调用时机:
- 遇到每一个
import或require(通过 CommonJS 插件转换后)时 - 也用于解析入口文件 ID
- 遇到每一个
-
签名(简化):
tsresolveId(source, importer, options)其中:
source: 源码中出现的导入路径importer: 当前模块的绝对路径(入口模块为undefined)options.custom: 供插件间通信使用的自定义选项
-
返回值:
null/undefined:交给下一个插件处理,最终由 Rollup 默认解析逻辑兜底- 字符串:解析后的模块 ID
- 对象:
{ id, external, moduleSideEffects, ... }
典型用途:
- 自定义别名解析(路径映射)
- 支持虚拟模块、内联模块等
- 控制模块是否视为 external
5.4 resolveDynamicImport
-
类型:
first -
调用时机:
- 遇到动态导入表达式
import(expr)时
- 遇到动态导入表达式
-
签名(简化):
tsresolveDynamicImport(specifier, importer, options) -
返回值:
- 类似
resolveId,允许返回字符串/对象或null
- 类似
典型用途:
- 对动态导入路径进行特殊处理
- 限制或重写某些动态导入的目标
5.5 load
-
类型:
first -
调用时机:
- 当 Rollup 需要获取某个已解析模块 ID 的源码时
-
签名(简化):
tsload(id) -
返回值:
null/undefined:交给下一个插件或默认文件读取逻辑- 字符串:模块源码
- 对象:
{ code, map }
典型用途:
- 为虚拟模块返回源码
- 从自定义存储(内存、远程接口等)读取模块内容
5.6 transform
-
类型:
sequential -
调用时机:
- 每个模块被成功
load后
- 每个模块被成功
-
签名(简化):
tstransform(code, id) -
返回值:
null/undefined:表示未修改代码- 字符串:新的代码
- 对象:
{ code, map, ast, moduleSideEffects, syntheticNamedExports }
典型用途:
- 代码转译(如调用 Babel、TypeScript 编译器)
- 注入辅助函数或运行时代码
- 删除调试语句、替换常量、宏展开等
5.7 shouldTransformCachedModule
-
类型:
first -
调用时机:
- 在使用缓存模块前,Rollup 会询问插件该模块是否需要重新执行
transform
- 在使用缓存模块前,Rollup 会询问插件该模块是否需要重新执行
-
签名(简化):
tsshouldTransformCachedModule(options) -
作用:
- 针对 watch 模式或持久化缓存,插件可以精细控制缓存复用策略
5.8 moduleParsed
-
类型:
sequential -
调用时机:
- 某个模块被解析为 AST 之后
-
签名(简化):
tsmoduleParsed(moduleInfo) -
moduleInfo中包含:- 导入导出列表
- 该模块的依赖关系
meta字段可存放插件自定义元数据
典型用途:
- 基于 AST 做静态分析
- 为后续阶段准备统计信息(代码大小、依赖图、命名导出等)
5.9 onLog
- 类型:
sequential - 调用时机:
- 当 Rollup 打算输出一条日志(包括错误、警告、信息)时
插件可以通过该钩子:
- 过滤、重写或收集日志
- 将日志转发到自定义的日志系统
5.10 buildEnd
-
类型:
sequential -
调用时机:
- 构建结束时,无论成功与否都会调用
-
签名(简化):
tsbuildEnd(error?)
典型用途:
- 释放资源,关闭数据库连接、网络连接
- 根据
error参数输出构建失败的统计信息
5.11 watchChange
-
类型:
sequential -
调用时机:
--watch模式下,当某个文件发生变更时
-
签名(简化):
tswatchChange(id, change) -
change一般包含:event: "create" | "update" | "delete"
典型用途:
- 清理插件内部缓存
- 调整增量构建策略
5.12 closeWatcher
- 类型:
sequential - 调用时机:
watch监听器关闭时
- 典型用途:
- 清理和
watch相关的持久连接、临时文件等
- 清理和
6. 钩子对象形式与执行控制
官方文档特别说明:除直接提供函数外,很多钩子也可以以对象形式声明:
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钩子需要串行执行且有依赖关系 - 中间某个插件必须在其他插件前后精确插入一个步骤
7. 输出生成阶段钩子(Output Generation Hooks)
当 Rollup 开始从模块图生成最终 bundle 时,会进入“输出阶段”。插件可以在这一阶段:
- 动态修改输出配置
- 重写 chunk 内容或导入导出
- 生成额外文件(如 manifest、报告)
- 控制 bundle 写入磁盘前后的行为
下面按典型调用顺序介绍这些钩子。
7.1 outputOptions
-
类型:
sequential -
调用时机:
- 对每一份输出配置执行一次
-
签名(简化):
tsoutputOptions(outputOptions) -
返回值:
- 可以返回新的
outputOptions,类似构建阶段的options钩子
- 可以返回新的
典型用途:
- 动态更改输出目录、文件名模式、格式等
- 为多产物构建注入额外的输出设置
7.2 renderStart
-
类型:
sequential -
调用时机:
- 某次输出流程开始之前
-
签名(简化):
tsrenderStart(outputOptions, inputOptions)
典型用途:
- 输出构建信息、版本号等
- 初始化本次输出阶段需要的临时数据
7.3 banner / footer / intro / outro
这些钩子可以是字符串或返回字符串的函数,也可以是带 handler 的对象。
banner:插入在 bundle 代码的最开头footer:插入在 bundle 代码的结尾intro:插入在自执行函数或模块包装内部的开头outro:插入在自执行函数或模块包装内部的结尾
常见用途:
- 注入版权声明、构建信息
- 插入包裹代码、运行时初始化逻辑等
7.4 augmentChunkHash
-
类型:
sequential -
调用时机:
- Rollup 计算 chunk hash 的过程中
-
签名(简化):
tsaugmentChunkHash(chunkInfo) -
返回值:
- 可以返回一个字符串,将其混入 hash 计算
典型用途:
- 当插件对 chunk 内容以外的因素敏感时,强制 hash 变化
- 控制缓存失效策略(如插件配置变化时强制替换浏览器缓存)
7.5 renderChunk
-
类型:
sequential -
调用时机:
- 对每一个生成的 chunk 执行
-
签名(简化):
tsrenderChunk(code, chunk, options) -
返回值:
- 与
transform类似,可以返回新的{ code, map }
- 与
典型用途:
- 按 chunk 维度注入 runtime
- 做 bundle 级别的压缩或重写(非模块级)
7.6 renderDynamicImport
- 调用时机:
- 生成最终代码时,决定
import()在不同格式下如何呈现
- 生成最终代码时,决定
通过该钩子可以:
- 自定义不同输出格式下动态导入的实现方式
- 与特定运行环境的模块加载机制集成
7.7 resolveFileUrl
-
调用时机:
- 生成
import.meta.ROLLUP_FILE_URL_*相关代码时
- 生成
-
签名(简化):
tsresolveFileUrl(options) -
其中
options包含:fileNamereferenceIdchunkId等信息
典型用途:
- 针对不同部署环境(CDN、相对路径等)自定义资源 URL
7.8 resolveImportMeta
- 调用时机:
- 解析
import.meta.*表达式时
- 解析
典型用途:
- 为
import.meta.env、import.meta.url等注入特定实现 - 与运行时环境集成(如 SSR、定制运行时)
7.9 generateBundle
-
类型:
sequential -
调用时机:
- Rollup 生成 bundle 元信息时
-
签名(简化):
tsgenerateBundle(outputOptions, bundle, isWrite) -
bundle是一个对象:- key 为文件名
- value 为每个 chunk 或 asset 的描述信息
典型用途:
- 遍历所有产物,生成清单文件、统计报告、HTML 模板等
- 根据
isWrite决定是“只读分析”还是“实际写文件”
7.10 writeBundle
-
类型:
sequential -
调用时机:
- 所有文件已经写入磁盘之后
-
签名与
generateBundle类似:tswriteBundle(outputOptions, bundle)
典型用途:
- 在文件真实存在于磁盘后执行额外操作:
- 上传到远程服务器
- 调用外部命令行工具
- 清理中间产物
7.11 renderError
- 调用时机:
- 输出阶段发生错误且构建被中断时
典型用途:
- 为输出阶段错误增加额外上下文
- 将错误上报到监控系统
7.12 closeBundle
- 调用时机:
- 整个构建流程完全结束后(包括所有输出)
典型用途:
- 关闭所有仍然打开的资源(网络连接、句柄等)
- 做一次性清理或收尾日志
8. 插件上下文(this)常用方法
在每个钩子内部,this 指向插件上下文对象。官方文档列出了大量实用方法,
其中常用的包括:
this.addWatchFile(id)- 在
--watch模式下监听额外文件
- 在
this.getWatchFiles()- 获取当前被监听的所有文件列表
this.emitFile(descriptor)- 声明一个额外的文件或 chunk,例如
{ type: "asset", name, source }{ type: "chunk", id, name }
- 声明一个额外的文件或 chunk,例如
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?)- 将源码解析为 AST,使用与 Rollup 内部相同的解析器
this.resolve(source, importer, options?)- 在插件内部复用 Rollup 的模块解析逻辑
this.getModuleIds()- 返回一个遍历器,用于遍历所有已知模块 ID
this.getModuleInfo(id)- 获取单个模块的详细信息(包含
meta字段)
- 获取单个模块的详细信息(包含
this.getCombinedSourcemap()- 获取某个模块在所有
transform链之后的合并 Source Map
- 获取某个模块在所有
this.meta- 包含插件运行时环境信息(如当前 Rollup 版本)
这些 API 是实现复杂插件(如分析工具、可视化工具、资源管理器)的基础。
9. 文件 URL 与 import.meta
官方文档针对“文件 URL”专门有一节说明,核心机制是:
- 插件可以通过
this.emitFile声明 asset 或 chunk - 在源码中使用
import.meta.ROLLUP_FILE_URL_referenceId - Rollup 在生成代码时会调用
resolveFileUrl钩子,最终替换成实际 URL 字符串
典型流程示意:
// 在插件中
const imageId = this.emitFile({
type: "asset",
name: "logo.png",
source: imageBuffer
})
// 在用户源码中(经由插件注入)
const logoUrl = import.meta.ROLLUP_FILE_URL_imageId在 resolveFileUrl 钩子中,可以根据:
- 当前输出目录
- CDN 前缀
- 是否 hash 命名
来生成不同形式的 URL,从而适配多种部署环境。
同时,resolveImportMeta 钩子允许插件自定义处理 import.meta.*:
- 将
import.meta.env.*映射到构建时环境变量 - 替换或增强
import.meta.url的行为
10. 转换器与代码转换(Transformers)
官方文档中把很多“只做源码转换”的插件统称为“转换器(transformers)”,
它们通常只实现 transform 钩子。
10.1 转换器的基本模式
一个典型的转换器插件大致结构如下:
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 正确串联
10.2 源代码转换示例
比如实现一个“移除所有 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 }
}
}
}这类插件不依赖模块图或输出阶段,只基于源码字符串工作。
10.3 合成命名导出(Synthetic Named Exports)
对于那些只提供默认导出、没有显式命名导出的模块,Rollup 提供
syntheticNamedExports 概念;插件也可以在 transform 返回值中设置:
return {
code,
map,
syntheticNamedExports: true
}这样 Rollup 会尝试基于返回的代码为该模块创建“合成的命名导出”,以改善互操作性。
11. 插件间通信
在大型工程中,多种插件之间往往需要协作。官方文档总结了几种推荐的通信方式。
11.1 自定义解析器选项
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 的需求执行特殊逻辑
}
}这种方式适用于多个解析插件协同工作时传递额外上下文。
11.2 自定义模块元数据
每个模块的 moduleInfo.meta 字段可以挂载任意插件自定义数据:
// 在某个插件中
moduleParsed(info) {
info.meta.myPlugin = {
hasSpecialExport: checkSomething(info.ast)
}
}其他插件可以在后续钩子中通过 this.getModuleInfo(id) 获取这些信息,从而实现跨插件的数据共享。
11.3 插件对象上的自定义 API
插件对象本身也可以暴露一个 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 访问其中的方法。
这种模式适合插件之间存在明确依赖关系、且需要共享较复杂逻辑时使用。
12. 小结与实践建议
本篇将官方「插件开发」文档中的技术要点按“插件对象 → 钩子 → 上下文 → 文件 URL → 转换器 → 插件间通信”的顺序系统梳理,结合本系列前两篇,你可以用以下步骤 练习并掌握插件开发:
- 先实现一个只用
resolveId与load的“虚拟模块”插件 - 再实现一个只用
transform的简单转换器(如删除调试代码) - 在此基础上尝试:
- 使用
moduleParsed做简单的 AST 分析 - 在
generateBundle中基于bundle生成清单文件 - 使用
import.meta.ROLLUP_FILE_URL_*和resolveFileUrl处理静态资源
- 使用
- 最终,将这些能力组合成一个具有实际生产价值的插件,并按本节的约定 补全文档、测试与发布流程
掌握这些内容后,你就可以在 Rollup 之上搭建高度定制的构建流程,甚至为其他打包工具 (如 Vite)贡献插件生态。