上一节我们学习了 Rollup 构建工具的使用,相信你已经对 Rollup 的基础概念和使用有了基本的掌握。同时你也可以明白,仅仅使用 Rollup 内置的打包能力很难满足项目日益复杂的构建需求。对于一个真实的项目构建场景来说,我们还需要考虑到模块打包之外的问题,比如路径别名(alias) 、全局变量注入、代码压缩等等。
可要是把这些场景的处理逻辑与核心的打包逻辑都写到一起,一来打包器本身的代码会变得十分臃肿,二来对原有的核心代码也会产生一定的侵入性,混入很多与核心流程无关的代码,不易于后期的维护。因此 ,Rollup 中设计出了一套完整的插件机制,将自身的核心逻辑与插件逻辑相分离,让你能按需引入插件功能,提高了 Rollup 自身的可扩展性。
我个人也是非常喜欢 Rollup 的插件机制的,既功能完备又上手简单,体现了 Rollup 本身小而美的风格。那在接下来的内容中,我会从宏观到微观的角度带你分析 Rollup 的插件机制,首先带你熟悉 Rollup 插件所经历的构建阶段以及整体的工作流程,然后通过具体的插件案例和你深入插件的开发细节。相信学完本节,你一定会对 Rollup 的插件机制有更加全面和深入的理解。
整体而言,在 Rollup 的打包过程中,会定义一套完整的构建生命周期,从开始打包到产物输出,中途会经历一些标志性的阶段,并且在不同阶段会自动执行对应的插件钩子函数(Hook)。从中你可以看到,对于 Rollup 插件来讲,最重要的部分是钩子函数,一方面它定义了插件的执行逻辑,也就是"做什么";另一方面也声明插件的作用阶段,即"什么时候做",这是与 Rollup 本身的构建生命周期是息息相关的。
因此,要真正理解插件的作用范围和阶段,首先需要了解 Rollup 整体的构建过程中到底做了些什么。
# Rollup 整体构建阶段
在执行 rollup 命令之后,在 cli 内部的主要逻辑简化如下:
// Build 阶段
const bundle = await rollup.rollup(inputOptions);
// Output 阶段
await Promise.all(outputOptions.map(bundle.write));
// 构建结束
await bundle.close();
Rollup 内部主要经历了 Build 和 Output 两大阶段:

首先,Build 阶段主要负责创建模块依赖图,初始化各个模块的 AST 以及模块之间的依赖关系。下面我们用一个简单的例子来感受一下:
// src/index.js
import { a } from './module-a';
console.log(a);
// src/module-a.js
export const a = 1;
然后执行如下的构建脚本:
