rollup打包工具
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.jsobject.jspromise.jsreplaceIdentifiers.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 = bundlethis.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; //nameconst 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;//VariableDeclarationif (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) returnswitch (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;//找出外部依赖_dependsOnast.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,生成抽象语法树
- 对入口文件抽象语法树进行依赖解析
- 生成最终代码
- 写入目标文件