You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
varMagicString=require('magic-string');vars=newMagicString('problems = 99');s.overwrite(0,8,'answer');s.toString();// 'answer = 99's.overwrite(11,13,'42');// character indices always refer to the original strings.toString();// 'answer = 42's.prepend('var ').append(';');// most methods are chainables.toString();// 'var answer = 42;'varmap=s.generateMap({source: 'source.js',file: 'converted.js.map',includeContent: true});// generates a v3 sourcemaprequire('fs').writeFile('converted.js',s.toString());require('fs').writeFile('converted.js.map',map.toString());
// 作用域classScope{constructor(options={}){this.parent=options.parent// 父作用域this.depth=this.parent ? this.parent.depth+1 : 0// 作用域层级this.names=options.params||[]// 作用域内的变量this.isBlockScope=!!options.block// 是否块作用域}add(name,isBlockDeclaration){if(!isBlockDeclaration&&this.isBlockScope){// it's a `var` or function declaration, and this// is a block scope, so we need to go upthis.parent.add(name,isBlockDeclaration)}else{this.names.push(name)}}contains(name){return!!this.findDefiningScope(name)}findDefiningScope(name){if(this.names.includes(name)){returnthis}if(this.parent){returnthis.parent.findDefiningScope(name)}returnnull}}
前言
为了学习 rollup 打包原理,我克隆了最新版(v2.26.5)的源码。然后发现打包器和我想像的不太一样,代码实在太多了,光看 d.ts 文件就看得头疼。为了看看源码到底有多少行,我写了个脚本,结果发现有 19650行,崩溃...
这就能打消我学习 rollup 的决心吗?不可能,退而求其次,我下载了 rollup 初版源码,才 1000 行左右。
我的目的是学习 rollup 怎么打包的,怎么做 tree-shaking 的。而初版源码已经实现了这两个功能(半成品),所以看初版源码已经足够了。
好了,下面开始正文。
正文
rollup 使用了
acorn
和magic-string
两个库。为了更好的阅读 rollup 源码,必须对它们有所了解。下面我将简单的介绍一下这两个库的作用。
acorn
acorn
是一个 JavaScript 语法解析器,它将 JavaScript 字符串解析成语法抽象树 AST。例如以下代码:
将被解析为:
可以看到这个 AST 的类型为
program
,表明这是一个程序。body
则包含了这个程序下面所有语句对应的 AST 子节点。每个节点都有一个
type
类型,例如Identifier
,说明这个节点是一个标识符;BlockStatement
则表明节点是块语句;ReturnStatement
则是 return 语句。如果想了解更多详情 AST 节点的信息可以看一下这篇文章《使用 Acorn 来解析 JavaScript》。
magic-string
magic-string
也是 rollup 作者写的一个关于字符串操作的库。下面是 github 上的示例:从示例中可以看出来,这个库主要是对字符串一些常用方法进行了封装。这里就不多做介绍了。
rollup 源码结构
上面是初版源码的目录结构,在继续深入前,请仔细阅读上面的注释,了解一下每个文件的作用。
rollup 如何打包的?
在 rollup 中,一个文件就是一个模块。每一个模块都会根据文件的代码生成一个 AST 语法抽象树,rollup 需要对每一个 AST 节点进行分析。
分析 AST 节点,就是看看这个节点有没有调用函数或方法。如果有,就查看所调用的函数或方法是否在当前作用域,如果不在就往上找,直到找到模块顶级作用域为止。
如果本模块都没找到,说明这个函数、方法依赖于其他模块,需要从其他模块引入。
例如
import foo from './foo.js'
,其中foo()
就得从./foo.js
文件找。在引入
foo()
函数的过程中,如果发现foo()
函数依赖其他模块,就会递归读取其他模块,如此循环直到没有依赖的模块为止。最后将所有引入的代码打包在一起。
上面例子的示例图:
接下来我们从一个具体的示例开始,一步步分析 rollup 是如何打包的。
以下两个文件是代码文件。
下面是测试代码:
1. rollup 读取
main.js
入口文件。rollup()
首先生成一个Bundle
实例,也就是打包器。然后根据入口文件路径去读取文件,最后根据文件内容生成一个Module
实例。2. new Moudle() 过程
在 new 一个
Module
实例时,会调用acorn
库的parse()
方法将代码解析成 AST。接下来需要对生成的 AST 进行分析。
第一步,分析导入和导出的模块,将引入的模块和导出的模块填入对应的对象。
每个
Module
实例都有一个imports
和exports
对象,作用是将该模块引入和导出的对象填进去,代码生成时要用到。上述例子对应的
imports
和exports
为:第二步,分析每个 AST 节点间的作用域,找出每个 AST 节点定义的变量。
每遍历到一个 AST 节点,都会为它生成一个
Scope
实例。Scope
的作用很简单,它有一个names
属性数组,用于保存这个 AST 节点内的变量。例如下面这段代码:
打断点可以看出来,它生成的作用域对象,
names
属性就会包含a
。并且因为它是模块下的一个函数,所以作用域层级为 1(模块顶级作用域为 0)。第三步,分析标识符,并找出它们的依赖项。
什么是标识符?如变量名,函数名,属性名,都归为标识符。当解析到一个标识符时,rollup 会遍历它当前的作用域,看看有没这个标识符。如果没有找到,就往它的父级作用域找。如果一直找到模块顶级作用域都没找到,就说明这个函数、方法依赖于其它模块,需要从其他模块引入。如果一个函数、方法需要被引入,就将它添加到
Module
的_dependsOn
对象里。例如
test()
函数中的变量a
,能在当前作用域找到,它就不是一个依赖项。foo1()
在当前模块作用域找不到,它就是一个依赖项。打断点也能发现
Module
的_dependsOn
属性里就有foo1
。这就是 rollup 的 tree-shaking 原理。
rollup 不看你引入了什么函数,而是看你调用了什么函数。如果调用的函数不在此模块中,就从其它模块引入。
换句话说,如果你手动在模块顶部引入函数,但又没调用。rollup 是不会引入的。从我们的示例中可以看出,一共引入了
foo1()
foo2()
两个函数,_dependsOn
里却只有foo1()
,因为引入的foo2()
没有调用。_dependsOn
有什么用呢?后面生成代码时会根据_dependsOn
里的值来引入文件。3. 根据依赖项,读取对应的文件。
从
_dependsOn
的值可以发现,我们需要引入foo1()
函数。这时第一步生成的
imports
就起作用了:rollup 将
foo1
当成 key,找到它对应的文件。然后读取这个文件生成一个新的Module
实例。由于foo.js
文件导出了两个函数,所以这个新Module
实例的exports
属性是这样的:这时,就会用
main.js
要导入的foo1
当成 key 去匹配foo.js
的exports
对象。如果匹配成功,就把foo1()
函数对应的 AST 节点提取出来,放到Bundle
中。如果匹配失败,就会报错,提示foo.js
没有导出这个函数。4. 生成代码。
由于已经引入了所有的函数。这时需要调用
Bundle
的generate()
方法生成代码。同时,在打包过程中,还需要对引入的函数做一些额外的操作。
移除额外代码
例如从
foo.js
中引入的foo1()
函数代码是这样的:export function foo1() {}
。rollup 会移除掉export
,变成function foo1() {}
。因为它们就要打包在一起了,所以就不需要export
了。重命名
例如两个模块中都有一个同名函数
foo()
,打包到一起时,会对其中一个函数重命名,变成_foo()
,以避免冲突。好了,回到正文。
还记得文章一开始提到的
magic-string
库吗?在generate()
中,会将每个 AST 节点对应的源代码添加到magic-string
实例中:这个操作本质上相当于拼字符串:
最后将拼在一起的代码返回。
到这就已经结束了,如果你想把代码生成文件,可以调用
write()
方法生成文件:这个方法是写在
rollup()
函数里的。结尾
本文对源码进行了抽象,所以很多实现细节都没说出来。如果对实现细节有兴趣,可以看一下源码。代码放在我的 github 上。
我已经对 rollup 初版源码进行了删减,并添加了大量注释,让代码更加易读。
The text was updated successfully, but these errors were encountered: