Hvigor
hvigor是一款基于TS实现的 构建任务编排工具,主要提供任务管理机制,包括任务注册编排、工程模型管理、配置管理等关键能力,更符合node生态开发者开发习惯
它不负责实际的打包,实际打包的工作是通过rollup来做的。
rollup
一个用于 JavaScript 的 模块打包工具,它将小的代码片段编译成更大、更复杂的代码
rollup打包工具在前端很早就有了,与它齐名的还有webpack,webpack主要用于打包web应用。
rollup基于ESM模块打包,能同时处理Nodejs和浏览器的JS打包工作,它还会自动对代码进行tree shaking减小包的体积,在对库和模块打包时非常有用。React、Vue等框架的构建就是使用的rollup
rollup 支持的打包文件的格式有 amd, cjs, esm/es, iife, umd。其中amd 为 AMD 标准,cjs 为 CommonJS 标准,esm/es为 ES 模块标准,iife 为立即调用函数, umd 同时支持 amd、cjs 和 iife
rollup.js 默认采用 ES 模块标准,ESM:ECMAScript 模块是未来的官方标准和主流
tree-shaking
tree-shaking 本质上是 消除无用的 JS 代码。 当引入一个模块时,并不引入整个模块的所有代码,而是只引入需要的代码,那些不需要的无用代码就会被"摇"掉
tree-shaking 虽然能够消除无用代码,但仅针对 ES6 模块语法,因为 ES6 模块采用的是静态分析,从字面量对代码进行分析
DCE(dead code elimination)
无用代码有一个专业术语 - dead code elimination(DCE)。编译器可以判断出哪些代码并不影响输出,然后消除这些代码。DCE主要包括以下几个方面
- 代码不会被执行,不可到达
- 代码执行的结果不会被用到
- 代码只会影响死变量,只写不读
基于两个关键实现
-
ES6 的模块引入是静态分析的,可以在编译时正确判断到底加载了什么代码。 ES6 Module一些特性如下
- 只能作为模块顶层的语句出现,不能出现在
function
或是if
等块级作用域中 - import 的模块名只能是字符串常量
- import binding 是 immutable 的,类似
const
- import hoisted,不管
import
的语句出现的位置在哪里,在模块初始化的时候所有的import
都必须已经导入完成
- 只能作为模块顶层的语句出现,不能出现在
-
分析程序流,判断哪些变量被使用、引用,打包这些代码
- 基于作用域,在 AST 过程中对函数或全局对象形成对象记录
- 在整个形成的作用域链对象中进行匹配 import 导入的标识,最后只打包匹配的代码,而删除那些未被匹配使用的代码
下面的截图中 index.js 是入口文件,打包生成的代码在 bundle.js 中,除此之外的 a.js、util.js 等文件均作为被引用的依赖模块
1)消除未使用的变量

a.js中定义的变量 b 和 c 没有使用到,它们不会出现在打包后的bundle.js文件中
2)消除未被调用的函数
仅引入但未使用到的 util3()和 util2()函数没有被打包进来
3)消除未被使用的类

只引用类文件 mixer.js 但实并未用 它的任何方法和变量,该类不会出现在bundle.js文件中
4)未消除的副作用-模块中类的方法未被引用

引用类文件 mixer.js并使用了其中的getName方法,虽然其他方法未被使用,但是整个类是被打包进去的
5)未消除的副作用-模块中定义的变量影响了全局变量

a.js和utils.js模块中都给window.c进行了重新赋值,他们的引入顺序会影响window上c这个属性的最终值
Rollup探索
AST 抽象语法树
树上定义了代码的结构,通过操作这棵树,可以精准的定位到声明语句、赋值语句、运算语句 等等。实现对代码的分析、优化、变更等操作

AST工作流
- Parse(解析) 将源代码转换成抽象语法树,树上有很多的estree节点
- Transform(转换) 对抽象语法树进行转换
- Generate(代码生成) 将上一步经过转换过的抽象语法树生成新的代码

打包流程
rollup的打包流程主要是 通过遍历输入的文件,生成抽象语法树AST并对AST进行剪枝做treeshaking功能,然后把最终用到的代码写入到输出文件中。有以下两个阶段
-
rollup()阶段,解析源码,生成 AST tree,对 AST tree 上的每个节点进行遍历,判断出是否 include(标记避免重复打包),是的话标记,然后生成 chunks,最后导出。
- 通过 resolveId()方法解析文件地址,拿到文件绝对路径
- 通过从入口文件的绝对路径出发找到它的模块定义,并获取这个入口模块所有的依赖语句并返回所有内容
- 每个文件都是一个模块,每个模块都会有一个 Module 实例。在 Module 实例中,模块文件的代码通过 acorn 的 parse 方法遍历解析为 AST 语法树
- 将 source 解析并设置到当前 module 上,完成从文件到模块的转换,并解析出 ES tree node 以及其内部包含的各类型的语法树
-
generate()/write()阶段,根据 rollup()阶段做的标记,进行代码收集,最后生成真正用到的代码
- 将经处理生成后的代码写入文件,handleGenerateWrite()方法内部生成了 bundle 实例进行处理

具体细节可以参考,原理:无用代码去哪了?项目减重之 rollup 的 Tree-shaking。
我看了下大体代码差不多。rollup最新版本为4.9.6 并且使用wasm技术,感兴趣的同学可以查看 github.com/rollup/roll...
为了简单的探索rollup的打包原理,我使用的版本为0.3.1
rollup中两个比较重要的库是 acorn 和 magic-string
acorn
一个JS语法解析器,用于将JS代码组成的字符串解析成抽象语法树AST。rollup 使用它来实现 AST 抽象语法树的遍历解析
比如这个代码
js
export default function add(a, b) { return a + b }
通过 在线查看AST 之后,生成的AST如下
json
{
"type": "Program",
"start": 0,
"end": 50,
"body": [
{
"type": "ExportDefaultDeclaration",
"start": 0,
"end": 50,
"declaration": {
"type": "FunctionDeclaration",
"start": 15,
"end": 50,
"id": {
"type": "Identifier",
"start": 24,
"end": 27,
"name": "add"
},
"expression": false,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 28,
"end": 29,
"name": "a"
},
{
"type": "Identifier",
"start": 31,
"end": 32,
"name": "b"
}
],
"body": {
"type": "BlockStatement",
"start": 34,
"end": 50,
"body": [
{
"type": "ReturnStatement",
"start": 36,
"end": 48,
"argument": {
"type": "BinaryExpression",
"start": 43,
"end": 48,
"left": {
"type": "Identifier",
"start": 43,
"end": 44,
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 47,
"end": 48,
"name": "b"
}
}
}
]
}
}
}
],
"sourceType": "module"
}
AST是一棵树,由一个个的节点组成,每个节点都有一个 type 字段表示类型,例如 Identifier 表示一个标识符;BlockStatement 表示一个块语句;ReturnStatement 表示一个return语句等
树的根节点 type 是 Program 表示一个程序,这个程序内所有语句对应的代码的AST位于 body 字段下
magic-string
一个操作字符串的库,可以方便的替换、移除字符串中内容,并将字符串写入文件。rollup 使用它来操作字符串和生成 source-map 文件
下面是官方的一些示例用法
js
import MagicString from 'magic-string';
import fs from 'fs'
const s = new MagicString('problems = 99');
s.update(0, 8, 'answer');
s.toString(); // 'answer = 99'
s.update(11, 13, '42'); // character indices always refer to the original string
s.toString(); // 'answer = 42'
s.prepend('var ').append(';'); // most methods are chainable
s.toString(); // 'var answer = 42;'
const map = s.generateMap({
source: 'source.js',
file: 'converted.js.map',
includeContent: true
}); // generates a v3 sourcemap
fs.writeFileSync('converted.js', s.toString());
fs.writeFileSync('converted.js.map', map.toString());
目录结构简介


一些前置知识
-
在 rollup 中,一个文件就是一个模块
-
每一个模块都会根据文件中的代码生成一个 AST 抽象语法树,之后会对树上的每一个 AST 节点进行分析
-
分析 AST 节点,就是看看这个节点有没有调用函数或方法。如果有,就查看所调用的函数或方法是否在当前作用域,如果不在就往上找,直到找到模块顶级作用域为止。
-
如果本模块都没找到,说明这个函数、方法依赖于其他模块,需要从其他模块引入
js
import { name, age, MyCls } from './modules/myModule'
// 我们从myModule中引入了name、age、MyCls这几个变量,就需要从myModule文件查找
// 在引入这几个变量的过程中,如果发现变量还依赖其他模块,就会递归读取其他模块,如此循环直到没有所引入的变量不再依赖的模块为止
- 最后将所有引入的代码打包在一起,写入最终的单个文件中。这个文件通过-o指定
举例看过程
- 按照如下的截图创建目录和文件,之后进入工程目录下执行 npm install,之后执行npm run build命令,生成的文件位于dist/bundle.js
- 可以看到,代码中没有用到的变量是不会被打进去的。一般开发库或者框架会选择rollup进行打包,可以减少代码的体积

大框架流程

读取入口文件
rollup()
首先生成一个Bundle
实例,也就是打包器。- 然后根据入口文件路径去读取文件,最后根据文件内容生成一个
Module
实例
rollup->bundle.build->fetchModule

new Module()过程
在 new 一个 Module
实例时,会调用 acorn
库的 parse()
方法将代码解析成 AST

分析导入和导出的模块,将引入的模块和导出的模块填入对应的对象
- 每个
Module
实例都有一个imports
和exports
对象,作用是将该模块引入和导出的对象填进去 - 填入的时候可以看到key是导入的名称,value是具有source、name、localName这些key的对象


分析每个 AST 节点的作用域,找出节点中定义的变量
每遍历到一个 AST 节点,都会为它生成一个 Scope
实例

Scope
的作用很简单,它有一个 names
属性数组,用于保存这个 AST 节点内的变量

分析标识符,并找出它们的依赖项
标识符:变量名,函数名,属性名等。
- 当解析到一个标识符时,rollup 会遍历它当前的作用域,看看有没这个标识符。
- 如果没有找到,就往它的父级作用域找。
- 如果一直找到模块顶级作用域都没找到,就说明这个函数、方法依赖于其它模块,需要从其他模块引入。
- 如果一个函数、方法需要被引入,就将它添加到
statement
的_dependsOn
对象里。生成代码时会根据_dependsOn
里的值来引入文件

根据依赖项,读取对应的文件
rollup根据语句_dependsOn里面依赖的标识符名称,在模块的imports里面查找它对应的文件。然后读取这个文件生成一个新的 Module
实例


生成代码
到了这一步之后我们就已经引入了所有的函数,这是调用 Bundle
的 generate()
方法生成代码。这一步还会做一些额外的操作
移除额外代码
例如从 foo.js
中引入的 foo1()
函数代码是这样的:export function foo1() {}
。
rollup 会移除掉 export,
变成 function foo1() {}
。因为最终会把所有的代码都写入到一个文件中,所以也就不存在export,所有的代码都在一个文件里
重命名
例如两个模块中都有一个同名函数 foo()
,打包到一起时,会对其中一个函数重命名,变成 _foo()
,以避免冲突
参考资料
- acorn
- magic-string
- rollup git仓库
- rollup npm package
- rollup官网
- rollup 在线体验 repl
- 在线查看JS的抽象语法树AST
- 工具:在线 ES6转ES5
- 工具:Google traceur 将ES转码成JS(适用于浏览器端)
- 工具:在线查看AST astexplorer
- 工具:在线查看AST语法树 esprima
-
工具: [揭秘 Rollup Tree Shaking](https://link.juejin.cn?target=https%3A%2F%2Fsegmentfault.com%2Fa%2F1190000040009496 "https://segmentfault.com/a/1190000040009496")\]([segmentfault.com/a/119000004...](https://link.juejin.cn?target=https%3A%2F%2Fsegmentfault.com%2Fa%2F1190000040009496 "https://segmentfault.com/a/1190000040009496"))
- 原理:从 rollup 初版源码学习打包原理 2.26.5版本
- 原理:浅析Rollup打包原理 0.3.1版本
- 原理:rollup打包原理 0.3.0版本
- 原理:rollup打包产物解析及原理(对比webpack)
- rollup - 构建原理及简易实现
- 原理:Rollup概念与运行原理
- 使用:rollup从入门到打包一个按需加载的组件库
- 使用:【实战篇】最详细的Rollup打包项目教程
- 使用:Rollup打包工具的使用(超详细,超基础,附代码截图超简单)
- 使用:一文带你快速上手Rollup
- 简单:关于Rollup那些事
- 使用Acorn解析JavaScript](juejin.cn/post/684490...)
- Roll打包系列文章