来自掘金小册 深入浅出 Vite 的学习实践与总结
为什么需要插件?
主要是构建需求越来越复杂,rollup 内置的打包功能并不能满足所有应用场景,为了扩展 rollup 的功能就设计了一套完整的插件机制,通过开发插件来满足打包需求。
这样做的好处有:
- 将 rollup 的核心打包逻辑与场景的处理逻辑进行分离,方便维护
- 通过引入插件的方式来扩展 rollup 的功能,提高了可维护性
- 插件功能非常灵活,能针对不同的需求进行对应的开发
插件构建流程
执行 rollup 命令进行打包,核心逻辑大概是这样:
// Build 阶段
const bundle = await rollup.rollup(inputOptions);
// Output 阶段
await Promise.all(outputOptions.map(bundle.write));
// 构建结束
await bundle.close();
插件 Hook 类型
Hook 的整个构建流程的钩子分为 Build Hook 和 Output Hook。
- Build Hook 就是指在 Build 阶段执行的钩子函数,主要针对模块代码的转换、AST 解析、模块依赖解析等。对于代码的操作粒度为模块级别
- Output Hook 是在 Output 阶段执行的钩子函数,主要针对代码进行打包。对于代码的操作粒度为
chunck
(将多个模块的代码汇总成一个文件)级别
根据 Hook的执行方式不同可以分为以下几种类型:
- Async:异步钩子
- Sync:同步钩子,里面不能有异步逻辑
- Parallel:并行钩子。假设有多个插件实现了这个钩子的逻辑,一旦有钩子函数是异步逻辑,则并发执行所有插件的钩子函数(底层使用
Promise.all
) - Sequential: 串行钩子。前一个钩子函数的结果作为下一个钩子函数的入参,存在依赖关系,所以需要按照顺序进行执行。例如钩子函数
transform
- First:多个插件实现了这个钩子函数,依次执行,当某个钩子返回的结果不为
null
与undefined
,则后面的钩子都不会再执行了。例如钩子函数resolveId
。
钩子函数的类型可以由 Async ****或 ****Sync 与其它 3 种类型任意一个进行组合。
Hook 执行流程图如下,使用边框与填充颜色代表不同的 Hook 类型。
Build Hooks 执行流程:
- 通过 options(Async + Sequential)进行配置转换,得到处理后的配置对象
- 调用 buildStart (Async + Parallel),开始构建
- 调用 resolveId(Async + First)解析文件路径,从入口文件开始
- 调用 load(Async + First)加载模块内容
- 调用 transform(Async + Sequential)自定义转换模块内容,例如 babel 转译
- 进行 AST 分析,得到所有 import 内容,调用 moduleParsed(Async + Parallel):
-
- 普通 import,则执行 resolveId 钩子
- 动态 import,则执行 resolveDynamicImport 解析路径,解析成功调用 load 加载模块,否则调用 resolveId
- 所有 import 解析完毕,调用 buildEnd,Build 阶段结束
Output Hooks 执行流程:
- 调用 outputOptions(Sync + Sequential)转换 output 配置
- 调用 renderStart(Async + Parallel),开始打包
- 调用 banner、footer、intro、outro(都是 Async + Parallel ,底层使用
Promise.all
),主要功能就是往产物的固定位置(头或尾)添加自定义内容 - 从入口模块开始解析,针对动态 import 语句调用 renderDynamicImport(Sync + First)自定义内容
- 对即将生成的 chunk 调用 augmentChunkHash(Sync + Sequential)更改 hash 值。针对 watch 模式下的多次打包情况,该钩子比较适用
- 如果遇到
import.meta
语句
-
- 调用 resolveFileUrl 自定义
import.meta.url
(Sync + First)的解析逻辑 - 调用 resolveImportMeta 自定义其它
import.meta.*
(Sync + First)的解析逻辑
- 调用 resolveFileUrl 自定义
- 调用 renderChunk(Async + Sequential)可以自定义 chunk 的内容
- 调用 generateBundle(Async + Sequential)可以删除 chunk 或 asset(静态文件)
- 当最终产物写入磁盘后,调用 writeBundle(Async + Parallel)钩子
- 当调用
bundle.close()
时,触发 closeBundle(Async + Parallel)钩子,Output 阶段结束
总结
rollup 插件就是一系列的 Hook 的组合。
rollup 插件的特性总结如下:
- 插件集中管理。 各个阶段的 Hook 都可以写同一个插件里面,而不用像 webpack 那样需要分开开发 loader 和 plugin
- 插件 API 简洁优雅。 开发插件的时候,插件函数只要返回一个带有
name
和各种 Hook 函数组成的对象即可 - 插件间的互相调用。在 Hook 中,可以通过 this(上下文对象)调用其它插件的 Hook,然后再做处理,大大提高了灵活性