rollup打包工具
在学习vite和vue3源码的时候,接触到了rollup,所以过来学习一下
什么是rollup
rollup是一个模块化的打包工具,会将javascript文件进行合并。比起webpack,webpack在打包的时候会进行代码注入(保障兼容性),如果对于一些项目,特别是类库,没有其他的静态资源文件,就可以使用rollup。rollup支持es6模块,支持tree-shaking,不支持code-splitting,模块热更新
- tree shaking优化: tree shaking是一种优化技术,用于剔除未使用的代码,减少最终的文件大小
- ES6模块支持: rollup专注es6模块的打包,有助于避免commonjs模块的一些问题,比如命名空间
- 代码拆分与懒加载:Rollup 支持代码拆分和懒加载,允许将代码拆分成多个文件,只在需要时加载。这有助于减少初始加载时间,并提供更好的性能。
- 可插拔的插件系统:允许使用现有插件和编写自定义插件
- 输出格式多样性:支持多种输出格式,包含ES6模块,commonjs,umd等
rollup的工作流程
acorn: JavaScript的此法解析器,可以将JavaScript字符串解析成为语法抽象树AST。
提供一个入口文件,rollup通过acorn读取解析文件,返回一种ast的抽象语法树。一个文件就是一个模块,每个模块都会根据文件的代码生成一个ast抽象语法树
分析AST节点,就是看这个节点有没有调用函数的方法,有没有读到变量,有,就查看是否在当前的作用域,不在就往上找,直到找到模块顶层作用域为止,如果本模块没有找到,则说明依赖于别的模块,需要从其他模块中去导出。直到没有依赖的模块为止。
│ bundle.js // Bundle 打包器,在打包过程中会生成一个 bundle 实例,用于收集其他模块的代码,最后再将收集的代码打包到一起。
│ external-module.js // ExternalModule 外部模块,例如引入了 'path' 模块,就会生成一个 ExternalModule 实例。
│ module.js // Module 模块,module 实例。
│ rollup.js // rollup 函数,一切的开始,调用它进行打包。
│
├─ast // ast 目录,包含了和 AST 相关的类和函数
│ analyse.js // 主要用于分析 AST 节点的作用域和依赖项。
│ Scope.js // 在分析 AST 节点时为每一个节点生成对应的 Scope 实例,主要是记录每个 AST 节点对应的作用域。
│ walk.js // walk 就是递归调用 AST 节点进行分析。
│
├─finalisers
│ cjs.js
│ index.js
│
└─utils // 一些帮助函数
map-helpers.js
object.js
promise.js
replaceIdentifiers.js
生成一个new Bundle(),然后执行build()打包
```javascript
let Bundle = require('./bundle')
function rollup(entry, outputFileName) {
const bundle = new Bundle({ entry })
bundle.build(outputFileName)
}
module.export = rollup
```
```javascript
class Bundle {
constructor() {}
build(outputFileName) {}
fetchModule(importee, importer) {
...
if(route) {
let code = fs.readFileSync(route, 'utf8'),
let module = new Module({
code, // 模块的源代码
path: route, // 模块的绝对路径
bundle: this // 属于那个bundle
})
return module;
}
}
}
```
new Module()
每个文件都是一个模块,每个模块都会有一个module实例,在module实例中,会调用acorn库的parse()方法将代码解析称为AST
class Module {
constructor({code, path, bundle}) {
this.code = new MagicString(code, {filename: path})
this.path = path
this.bundle = bundle
this.ast = parse(code, { // 把源代码转换为抽象语法树
ecmaVersion: 7,
sourceType: 'module'
})
this.analyse()
}
}
- 词法分析
this.analyse()
- 分析当前模块导入import和导出exports模块,将引入的模块和导出的模块存储起来的this.imports={} // 存放当前模块所有的导入
- this.exports = {} 存放着当前模块所有的导出
this.imports = {};//存放着当前模块所有的导入 this.exports = {};//存放着当前模块所有的导出 this.ast.body.forEach(node => { if (node.type === 'ImportDeclaration') {// 说明这是一个 import 语句 let source = node.source.value; // 从哪个模块导入的 let specifiers = node.specifiers; // 导入标识符 specifiers.forEach(specifier => { const name = specifier.imported.name; //name const localName = specifier.local.name; //name //本地的哪个变量,是从哪个模块的的哪个变量导出的 this.imports[localName] = { name, localName, source } }); //}else if(/^Export/.test(node.type)){ // 导出方法有很多 } else if (node.type === 'ExportNamedDeclaration') { // 说明这是一个 exports 语句 let declaration = node.declaration;//VariableDeclaration if (declaration.type === 'VariableDeclaration') { let name = declaration.declarations[0].id.name; this.exports[name] = { node, localName: name, expression: declaration } } } }); analyse(this.ast, this.code, this);//找到了_defines 和 _dependsOn
- analyse(this.ast, this.code, this)
- _defines: { value: {} },//存放当前模块定义的所有的全局变量
- _dependsOn: { value: {} },//当前模块没有定义但是使用到的变量,也就是依赖的外部变量
- _included: { value: false, writable: true },//此语句是否已经被包含到打包结果中,防止重复打包
- _source: { value: magicString.snip(statement.start, statement.end) } //magicString.snip 返回的还是 magicString 实例 clone
function analyse(ast, magicString, module) { let scope = new Scope();//先创建一个模块内的全局作用域 //遍历当前的所有的语法树的所有的顶级节点 ast.body.forEach(statement => { //给作用域添加变量 var function const let 变量声明 function addToScope(declaration) { var name = declaration.id.name;//获得这个声明的变量 scope.add(name); if (!scope.parent) {//如果当前是全局作用域的话 statement._defines[name] = true; } } Object.defineProperties(statement, { _defines: { value: {} },//存放当前模块定义的所有的全局变量 _dependsOn: { value: {} },//当前模块没有定义但是使用到的变量,也就是依赖的外部变量 _included: { value: false, writable: true },//此语句是否已经 被包含到打包结果中了 //start 指的是此节点在源代码中的起始索引,end 就是结束索引 //magicString.snip 返回的还是 magicString 实例 clone _source: { value: magicString.snip(statement.start, statement.end) } }); //这一步在构建我们的作用域链 walk(statement, { enter(node) { let newScope; if (!node) return switch (node.type) { case 'FunctionDeclaration': const params = node.params.map(x => x.name); if (node.type === 'FunctionDeclaration') { addToScope(node); } //如果遍历到的是一个函数声明,我会创建一个新的作用域对象 newScope = new Scope({ parent: scope,//父作用域就是当前的作用域 params }); break; case 'VariableDeclaration': //并不会生成一个新的作用域 node.declarations.forEach(addToScope); break; } if (newScope) {//当前节点声明一个新的作用域 //如果此节点生成一个新的作用域,那么会在这个节点放一个_scope,指向新的作用域 Object.defineProperty(node, '_scope', { value: newScope }); scope = newScope; } }, leave(node) { if (node._scope) {//如果此节点产出了一个新的作用域,那等离开这个节点,scope 回到父作用法域 scope = scope.parent; } } }); }); ast._scope = scope; //找出外部依赖_dependsOn ast.body.forEach(statement => { walk(statement, { enter(node) { if (node._scope) { scope = node._scope; } //如果这个节点放有一个 scope 属性,说明这个节点产生了一个新的作用域 if (node.type === 'Identifier') { //从当前的作用域向上递归,找这个变量在哪个作用域中定义 const definingScope = scope.findDefiningScope(node.name); if (!definingScope) { statement._dependsOn[node.name] = true;//表示这是一个外部依赖的变量 } } }, leave(node) { if (node._scope) { scope = scope.parent; } } }); }); }
- this.definitions = {} 全局变量的定义语句存放到definitions里
// module.js this.definitions = {};//存放着所有的全局变量的定义语句 this.ast.body.forEach(statement => { Object.keys(statement._defines).forEach(name => { //key 是全局变量名,值是定义这个全局变量的语句 this.definitions[name] = statement; }); });
- 展开语法,展开当前模块的所有语法,把这些语句中定义的变量的语句放到结果里
generate()
- 移除额外代码
- 处理ast节点上的源码,拼接字符串
- 返回合并后的源代码
- 输出到dist/bundle.js中
总结
- 获取入口文件的内容,包装成 module,生成抽象语法树
- 对入口文件抽象语法树进行依赖解析
- 生成最终代码
- 写入目标文件