一、开始
本次Rollup打包原理分析是v0.3.1版本,因其代码精简,易读性高,所以用它来初探打包器的工作原理。
在Rollup中,一个文件就是一个Module(模块),每一个模块都会生成AST语法抽象树,Rollup会对每个AST节点进行分析。
二、AST
Rollup打包的核心是对AST的解析和处理,要想深入理解打包原理,必须深入理解AST。
Rollup是通过acorn这个库进行AST的生成的,acorn的生成结果遵循estree规范。
下面是一个通过acorn生成的AST例子,来直观感受下。
const { parse } = require('acorn');
const code = `import { a } from './a.js'`
console.log(parse(code, {
ecmaVersion: 7,
sourceType: 'module',
}))生成的AST结果:
{
type: 'Program',
start: 0,
end: 26,
body: [
{
type: 'ImportDeclaration',
start: 1,
end: 26,
specifiers: [
{
type: 'ImportSpecifier',
start: 9,
end: 10,
imported: {
type: 'Identifier',
start: 9,
end: 10,
name: 'a',
},
local: {
type: 'Identifier',
start: 9,
end: 10,
name: 'a',
},
},
],
source: {
type: 'Literal',
start: 18,
end: 26,
value: './a.js',
raw: '\'./a.js\'',
},
},
],
sourceType: 'module',
};AST的基本结构是一个个Node节点,其必包含type和loc(start/end)信息。type为Program的Node节点表明这是段程序,通常是脚本的最外层,body是其属性。
这里有几个网站可以在线查看AST结构:AST Explorer、esprima。
三、V0.3.1的打包原理
入口文件是src/rollup.js,其实例化了一个Bundle,调用其build方法后,返回了一个对象,包含generate和write方法。
export function rollup(entry, options = {}) {
const bundle = new Bundle({
entry,
esolvePath: options.resolvePath
})
return bundle.build().then(() => {
return {
generate: options => bundle.generate(options),
write: (dest, options = {}) => {
let { code, map } = bundle.generate({
dest,
format: options.format,
globalName: options.globalName
});
return Promise.all([
writeFile( dest, code ),
writeFile( dest + '.map', map.toString() )
]);
}
}
})
}注意这个版本的Rollup的使用方式举例如下:
rollup('entry.js').then((res) => {
res.wirte('bundle.js')
})可以看出其主要分为两步,第一步是通过build构建,第二步是generate或者write进行输出。genenrate是把code返回,write是把code写入文件中。
1. build
在bundle.build方法中,调用了this.fetchModule,其主要是读取文件(模块)内容,然后实例化一个Module。
class Bundle {
build() {
return this.fetchModule(this.entryPath)
.then(entryPath => {
this.entryModule = entryModule;
return entryModule.expandAllStatements(true);
})
.then((statements) => {
this.statements = statements;
this.deconflict();
});
}
fetchModule() {
// ...
const module = new Module({
code,
path: route,
bundle: this,
});
// ...
}
}在Module的构造函数中,对传入的code(文件内容)通过acorn.parse进行了解析,获取了AST,然后调用了this.analyse。
class Module {
constructor({ code, path, bundle }) {
this.code = new MagicString(code, {
filename: path
})
this.ast = acorn.parse(code, {
ecmaVersion: 6,
sourceType: 'module',
})
}
this.analyse();
}在module的analyse方法中对ast的body进行了遍历,判断如果存在import或者export语句,将其放到this.imports和this.exports中。
然后调用了ast目录下的analyse方法,为每个node节点增加了_scope(作用域)、_defines(定义变量)、_modifies(修改的)、_dependsOn(依赖)等变量。
回到bundle.build方法中,在调用fetchModule获取到entryModule(入口模块)后,然后调用了entryModule.expandAllStatements方法。其遍历了ast.body,调用expandStatement去收集所有的语句。
expandStatement根据节点的_dependsOn属性,找到其依赖,然后依次调用define方法。define方法中根据导入模块的路径去调用fetchModule,实现了递归引入依赖,构建了一个大的模块树。
这样所有的声明都被收集到了bundle.statements中,然后调用this.deconflict解决命名冲突。
bundle.build的整体逻辑就是这样,在其过程中会有许多对AST的判断,以区分不同的类型的节点,来进行不同的处理。
2. generate
bundle.generate的方法是相对独立的,其核心思想是对this.statements的拼接。因为所有的语句都已经收集到了statement中,那么拼接起来返回就行了。
在拼接过程中过滤了export语句,并处理了一些import语句,然后返回了code。
四、流程图
画了一张流程图,来加深理解:

五、总结
上面就是Rollup基本的打包原理,简单的总结下就是从入口文件开始构建AST、收集依赖,最后拼接所有语句,输出bundle。
现在的最新版本V2.59.0比其多出的功能包括:watch、plugin、命令行参数等。
Rollup的插件系统其实就是在build和generate的不同生命周期阶段额外做的一些事情,其英文文档非常详细,建议直接阅读文档。