阅读本文之前希望你具备一定 Rollup 或 Vie 基础使用经验
作为一名前端开发者,几乎绕不开 Webpack、Rollup、Vite 等打包工具,在遇到复杂场景时我们通常需要编写插件来满足需求。笔者近期刚好遇到一个编写 Rollup 插件的需求,本文是从这次插件开发过程中的要点记录简单整理而来。
初探插件
基础结构
实现一个 Rollup 插件并非难事,只需要返回一个符合协议的 JavaScript 对象即可,通常长这样:
// rollup-plugin-my-example.js
export default function myExample () {
return {
name: 'my-example', // this name will show up in warnings and errors
resolveId ( source ) {
if (source === 'virtual-module') {
// this signals that rollup should not ask other plugins or check
// the file system to find this id
return source;
}
return null; // other ids should be handled as usually
},
load ( id ) {
if (id === 'virtual-module') {
// the source code for "virtual-module"
return 'export default "This is virtual!"';
}
return null; // other ids should be handled as usually
}
};
}
// rollup.config.js
import myExample from './rollup-plugin-my-example.js';
export default ({
input: 'virtual-module', // resolved by our plugin
plugins: [myExample()],
output: [{
file: 'bundle.js',
format: 'es'
}]
});
官方建议
Rollup 官方给插件开发者的几点建议是:
- 插件应该有一个带有
rollup-plugin-
前缀的清晰名称。 - 在 package.json 中包含
rollup-plugin
关键字。 - 应该测试插件。
- 尽可能使用异步方法,例如
fs.readFile
而不是fs.readFileSync
。 - 使用英文编写插件文档。
- 如果合适,请确保您的插件输出正确的 SourceMap。
- 如果插件使用“虚拟模块”(例如辅助函数),请在模块 ID 前加上
\0
,这可以防止其他插件尝试处理它。
钩子分类
从上面的例子可以看到编写插件的主要难度在于选择合适的 钩子
,在 Rollup 中这些钩子被分为 5 类。
async
钩子也可能返回一个 Promise 解析为相同类型的值。
sync
钩子只能同步返回结果。
first
如果有多个插件实现了这个钩子,那么钩子会按顺序运行,直到一个钩子返回一个非 null 或 undefined 的值。
sequential
如果有多个插件实现了这个钩子,所有插件都会按照指定的插件顺序运行。如果一个挂钩是异步的,则此类后续挂钩将等待当前挂钩被解析。
parallel
如果有多个插件实现了这个钩子,所有插件都会按照指定的插件顺序运行。如果一个钩子是异步的,那么后续的这种钩子将并行运行,而不是等待当前的钩子。
需要注意的是这 5 大类型不是互斥的,换句话说一个钩子可以是 async + parallel 或 async + sequential 的
生命周期
Rollup 的生命周期可以分为两大类,分别是 Build
和 Output
,你可以这么简单理解:
Build
阶段是 Rollup 打包的主要阶段,包括了解析、转化、打包等,对应钩子是file
级别Output
阶段是 Rollup 打包的最后阶段,包括了输出、写入文件等,对应钩子是chunk
和bundle
级别
Build 阶段

这张图里不同颜色代表了不同类型的钩子,整体流程比较清晰,主要步骤如下:
options
转化配置buildStart
开始构建resolveId
解析文件地址- 如果配置了
external
,说明不需要打包,那么直接跳到buildEnd
- 如果配置了
load
加载文件内容shouldTransformCachedModule
检查结构是否被缓存过transform
转化文件内容moduleParsed
解析模块依赖- 如果是同步 import,递归调用
resolveId
,进到下一个文件 build - 如果是异步 import,执行
resolveDynamicImport
- 如果是同步 import,递归调用
buildEnd
结束构建
Output 阶段

输出阶段主要步骤如下:
outputOptions
转化配置renderStart
开始打包banner
、footer
、intro
、outro
注入自定义内容,类似协议声明等renderChunk
打包一个 chunkaugmentChunkHash
修改 chunk hash 值generateBundle
生成整个 bundlewriteBundle
写入整个 bundlechoseBundle
结束前的清理工作
插件上下文
在编写插件时候,你可以通过 this
访问到插件的上下文,类似比较常用的
this.emitFile
输出一个新文件,并返回一个 referenceId
// rollup.config.js
function generateHtmlPlugin() {
let ref1;
return {
name: 'generate-html',
buildStart() {
ref1 = this.emitFile({
type: 'chunk',
id: 'src/entry1'
});
},
generateBundle() {
this.emitFile({
type: 'asset',
fileName: 'index.html',
source: `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script src="${this.getFileName(ref1)}" type="module"></script>
</body>
</html>`
});
}
};
}
this.parse
调用 Rollup
内置的 acorn 来将代码转化成 ast
this.warn、this.error
用于打印日志
所有方法可以参考 https://rollupjs.org/plugin-development/#plugin-context
插件间通信
插件之间直接通信(推荐)
简单粗暴,保存插件引用并直接调用。
function parentPlugin() {
return {
name: 'parent',
api: {
//...methods and properties exposed for other plugins
doSomething(...args) {
// do something interesting
}
}
// ...plugin hooks
};
}
function dependentPlugin() {
let parentApi;
return {
name: 'dependent',
buildStart({ plugins }) {
const parentName = 'parent';
const parentPlugin = plugins.find(
plugin => plugin.name === parentName
);
if (!parentPlugin) {
// or handle this silently if it is optional
throw new Error(
`This plugin depends on the "${parentName}" plugin.`
);
}
// now you can access the API methods in subsequent hooks
parentApi = parentPlugin.api;
},
transform(code, id) {
if (thereIsAReasonToDoSomething(id)) {
parentApi.doSomething(id);
}
}
};
}
自定义模块元数据
可以通过 meta
字段来实现,这个字段是一个对象,可以用来存储任何数据,但是不要存储大量数据,因为这个字段会被序列化到 bundle 中。
function annotatingPlugin() {
return {
name: 'annotating',
transform(code, id) {
if (thisModuleIsSpecial(code, id)) {
return { meta: { annotating: { special: true } } };
}
}
};
}
function readingPlugin() {
let parentApi;
return {
name: 'reading',
buildEnd() {
const specialModules = Array.from(this.getModuleIds()).filter(
id => this.getModuleInfo(id).meta.annotating?.special
);
// do something with this list
}
};
}
自定义解析器选项
比较难理解,不如上面两个简单直接
function requestingPlugin() {
return {
name: 'requesting',
async buildStart() {
const resolution = await this.resolve('foo', undefined, {
custom: { resolving: { specialResolution: true } }
});
console.log(resolution.id); // "special"
}
};
}
function resolvingPlugin() {
return {
name: 'resolving',
resolveId(id, importer, { custom }) {
if (custom.resolving?.specialResolution) {
return 'special';
}
return null;
}
};
}