# 一、开始
本次Rollup
打包原理分析是v0.3.1 (opens new window)版本,因其代码精简,易读性高,所以用它来初探打包器的工作原理。
在Rollup
中,一个文件就是一个Module
(模块),每一个模块都会生成AST
语法抽象树,Rollup
会对每个AST
节点进行分析。
# 二、AST
Rollup
打包的核心是对AST
的解析和处理,要想深入理解打包原理,必须深入理解AST
。
Rollup
是通过acorn (opens new window)这个库进行AST
的生成的,acorn
的生成结果遵循estree (opens new window)规范。
下面是一个通过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 (opens new window)、esprima (opens new window)。
# 三、V0.3.1的打包原理
入口文件是src/rollup.js (opens new window),其实例化了一个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 (opens new window)方法中,调用了this.fetchModule (opens new window),其主要是读取文件(模块)内容,然后实例化一个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 (opens new window)的构造函数中,对传入的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 (opens new window)方法中对ast
的body
进行了遍历,判断如果存在import
或者export
语句,将其放到this.imports
和this.exports
中。
然后调用了ast
目录下的analyse (opens new window)方法,为每个node
节点增加了_scope
(作用域)、_defines
(定义变量)、_modifies
(修改的)、_dependsOn
(依赖)等变量。
回到bundle.build
方法中,在调用fetchModule
获取到entryModule
(入口模块)后,然后调用了entryModule.expandAllStatements (opens new window)方法。其遍历了ast.body
,调用expandStatement
去收集所有的语句。
expandStatement (opens new window)根据节点的_dependsOn
属性,找到其依赖,然后依次调用define
方法。define (opens new window)方法中根据导入模块的路径去调用fetchModule
,实现了递归引入依赖,构建了一个大的模块树。
这样所有的声明都被收集到了bundle.statements
中,然后调用this.deconflict (opens new window)解决命名冲突。
bundle.build
的整体逻辑就是这样,在其过程中会有许多对AST
的判断,以区分不同的类型的节点,来进行不同的处理。
# 2. generate
bundle.generate (opens new window)的方法是相对独立的,其核心思想是对this.statements
的拼接。因为所有的语句都已经收集到了statement
中,那么拼接起来返回就行了。
在拼接过程中过滤了export
语句,并处理了一些import
语句,然后返回了code
。
# 四、流程图
画了一张流程图,来加深理解:
# 五、总结
上面就是Rollup
基本的打包原理,简单的总结下就是从入口文件开始构建AST
、收集依赖,最后拼接所有语句,输出bundle
。
现在的最新版本V2.59.0
比其多出的功能包括:watch
、plugin
、命令行参数等。
Rollup
的插件系统其实就是在build
和generate
的不同生命周期阶段额外做的一些事情,其英文文档非常详细,建议直接阅读文档。