认识
一、认识
Rollup
从 AST
解析的功能开始,完成代码的词法分析(tokenize
)和语义分析(parse
),实现模块依赖图和作用域链的搭建,并完成 Tree Shaking
、循环依赖检测及 Bundle
代码生成,最终实现一个类似 Rollup
的 Bundler
。
在执行 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
以及模块之间的依赖关系, 返回一个Bundle
对象, 这个对象的作用在于存储各个模块的内容及依赖关系,同时暴露generate
和write
方法,以进入到后续的Output
阶段(write
和generate
方法唯一的区别在于前者打包完产物会写入磁盘,而后者不会)。// src/index.js
import { a } from './module-a';
console.log(a);
// src/module-a.js
export const a = 1;const rollup = require('rollup');
const util = require('util');
async function build() {
const bundle = await rollup.rollup({
input: ['./src/index.js'],
});
console.log(util.inspect(bundle));
}
build(); -
Output
: 完成打包及输出的过程const rollup = require('rollup');
async function build() {
const bundle = await rollup.rollup({
input: ['./src/index.js'],
});
const result = await bundle.generate({
format: 'es',
});
console.log('result:', result);
}
build();
对于一次完整的构建过程而言, Rollup
会先进入到 Build
阶段,解析各模块的内容及依赖关系,然后进入 Output
阶段,完成打包及输出的过程。对于不同的阶段,Rollup
插件会有不同的插件工作流程
二、Build 工作流
对于 Build
阶段,插件 Hook
的调用流程如下图所示。流程图的最上面声明了不同 Hook
的类型,也就是我们在上面总结的 5
种 Hook
分类,每个方块代表了一个 Hook
,边框的颜色可以表示Async
和Sync
类型,方块的填充颜色可以表示Parallel
、Sequential
和 First
类型。
-
首先经历
options
钩子进行配置的转换,得到处理后的配置对象。 -
随之
Rollup
会调用buildStart
钩子,正式开始构建流程。 -
Rollup
先进入到resolveId
钩子中解析文件路径。(从input
配置指定的入口文件开始)。 -
Rollup
通过调用load
钩子加载模块内容。 -
紧接着
Rollup
执行所有的transform
钩子来对模块内容进行进行自定义的转换,比如babel
转译。 -
现在
Rollup
拿到最后的模块内容,进行AST
分析,得到所有的import
内容,调用moduleParsed
钩子:-
6.1 如果是普通的
import
,则执行resolveId
钩子,继续回到步骤3。 -
6.2 如果是动态
import
,则执行resolveDynamicImport
钩子解析路径,如果解析成功,则回到步骤4
加载模块,否则回到步骤3
通过resolveId
解析路径。
-
-
直到所有的
import
都解析完毕,Rollup
执行buildEnd
钩子,Build
阶段结束。
当然,在 Rollup
解析路径的时候,即执行 resolveId
或者 resolveDynamicImport
的时候,有些路径可能会被标记为 external
(翻译为排除),也就是说不参加 Rollup
打包过程,这个时候就不会进行load
、transform
等等后续的处理了。
在流程图最上面,不知道大家有没有注意到watchChange
和closeWatcher
这两个 Hook
,这里其实是对应了 rollup
的 watch
模式。当你使用 rollup --watch
指令或者在配置文件配有 watch: true
的属性时,代表开启了 Rollup
的 watch
打包模式,这个时候 Rollup
内部会初始化一个 watcher
对象,当文件内容发生变化时,watcher
对象会自动触发 watchChange
钩子执行并对项目进行重新构建。在当前打包过程结束时,Rollup
会自动清除 watcher
对象调用 closeWacher
钩子。
三、Output 工作流
-
执行所有插件的
outputOptions
钩子函数,对output
配置进行转换。 -
执行
renderStart
,并发执行renderStart
钩子,正式开始打包。 -
并发执行所有插件的
banner
、footer
、intro
、outro
钩子(底层用Promise.all
包裹所有的这四种钩子函数),这四个钩子功能很简单,就是往打包产物的固定位置(比如头部和尾部)插入一些自定义的内容,比如协议声明内容、项目介绍等等。 -
从入口模块开始扫描,针对动态
import
语句执行renderDynamicImport
钩子,来自定义动态import
的内容。 -
对每个即将生成的
chunk
,执行augmentChunkHash
钩子,来决定是否更改chunk
的哈希值,在watch
模式下即可能会多次打包的场景下,这个钩子会比较适用。 -
如果没有遇到
import.meta
语句,则进入下一步,否则:-
6.1 对于
import.meta.url
语句调用resolveFileUrl
来自定义url
解析逻辑 -
6.2 对于其他
import.meta
属性,则调用resolveImportMeta
来进行自定义的解析。
-
-
接着
Rollup
会生成所有chunk
的内容,针对每个chunk
会依次调用插件的renderChunk
方法进行自定义操作,也就是说,在这里时候你可以直接操作打包产物了。 -
随后会调用
generateBundle
钩子,这个钩子的入参里面会包含所有的打包产物信息,包括chunk
(打包后的代码)、asset
(最终的静态资源文件)。你可以在这里删除一些chunk
或者asset
,最终这些内容将不会作为产物输出。 -
前面提到了
rollup.rollup
方法会返回一个bundle
对象,这个对象是包含generate
和write
两个方法,两个方法唯一的区别在于后者会将代码写入到磁盘中,同时会触发writeBundle
钩子,传入所有的打包产物信息,包括chunk
和asset
,和generateBundle
钩子非常相似。不过值得注意的是,这个钩子执行的时候,产物已经输出了,而generateBundle
执行的时候产物还并没有输出。顺序如下图所示:Preview -
当上述的
bundle
的close
方法被调用时,会触发closeBundle
钩子,到这里Output
阶段正式结束。
注意: 当打包过程中任何阶段出现错误,会触发 renderError
钩子,然后执行 ``closeBundle` 钩子结束打包。