揭秘 Rollup Tree Shaking
- 前言
- 比照webpack
- 如何应用rollup
- 应用tree shaking 性能
- rollup源码解析
- magic-string
acorn
- AST工作流
- AST解析过程
- Scope
- 实现rollup
- 实现 tree shaking
- 依赖的变量有做批改操作
- 反对块级作用域
- 解决入口处 tree shaking
- 实现变量重命名
- 总结
- 援用
揭秘 Rollup Tree Shaking
文章首发于@careteen/rollup,转载请注明起源即可。
前言
rollup随着ES2015公布Scripts and Modules横空出世。
Next-generation ES module bundler
官网定义此为下一代ES模块捆绑器。
能够发现目前前端最火的几个框架Angular/React/Vue都在应用rollup
作为打包工具。
rollup
依赖于ES6的module
,提供Tree-shaking
性能。对其直译为树木摇摆
,行将树木下面枯死的树叶摇摆下来。对应到编程中则是去除掉无用代码。这对于大型项目能起到肯定优化作用。
比方在做一些中后盾时会应用到[ant-design]()局部组件,然而如果间接import Ant from 'antd'
会将所有组件代码进行打包,而官网也提供了babel-plugin-import插件反对按需加载。从肯定水平减小我的项目体积,使页面更快出现给用户。
比照webpack
webpack
能够进行代码分隔,动态资源解决,热模块替换rollup
反对ES6 module
,tree-shaking
功能强大;但webpack
不反对导出ES6 module
。webpack
打包体积臃肿,rollup
打包后简洁,更靠近源代码。
比照两者各自个性,能够发现webpack
更适宜于利用
,而rollup
更实用于类库
。
我的项目中都在应用webpack
进行开发,抱着知其然并知其所以然的态度也学习并实现了一个简版webpack,并且钻研其热更新原理。感兴趣的同学能够返回浏览。
如何应用rollup
上面示例代码寄存在rollup根底应用,可返回进行调试。
我的项目根目录新建文件rollup.config.dev.js
做如下配置
// rollup.config.dev.jsimport babel from 'rollup-plugin-babel'import resolve from '@rollup/plugin-node-resolve'import commonjs from '@rollup/plugin-commonjs'import typescript from '@rollup/plugin-typescript'import { terser } from 'rollup-plugin-terser'import postcss from 'rollup-plugin-postcss'import serve from 'rollup-plugin-serve'export default { input: './src/index.ts', output: { file: 'dist/index.js', format: 'es', }, plugins: [ babel({ exclude: 'node_modules/**', }), resolve(), commonjs(), typescript(), terser(), postcss(), serve({ open: true, port: 2333, contentBase: './dist', }), ],}
- rollup-plugin-babel反对应用新语法,用babel进行编译输入。
- @rollup/plugin-node-resolve反对解析第三方模块,即
node_modules
目录下。 - @rollup/plugin-commonjs反对
commonjs标准
。(因为默认只反对ES6 module
) - @rollup/plugin-typescript反对解析
typescript
- rollup-plugin-terser反对压缩
js
- rollup-plugin-postcss反对编译
css
- rollup-plugin-serve反对启动本地服务器
再新建src/index.ts
文件
// src/index.tsconsole.log('hello rollup')
在package.json
文件中配置
// package.json"scripts": { "dev": "rollup --config rollup.config.dev.js -w",},
运行脚本npm run dev
应用tree shaking 性能
新建文件src/userinfo.ts
// src/userinfo.tsexport const name = 'careteen'export const age = 25
改变src/index.ts
// src/index.tsimport { name, age } from './userinfo'console.log(name)
运行脚本npm run dev
后查看dist/index.js
const name = 'careteen'console.log(name)
可发现,rollup
将两个文件合并成一个文件输入,去除了无用代码export const age = 25
,而且也去除了import/export
语句。
rollup源码解析
当下最新rollup
功能丰富,架构简单。而此文次要是想钻研tree-shaking
性能,故翻看rollup
提交记录,找到rollup@0.2.0最后可用版本。
文件数量和代码行数都比拟少,也不便咱们能读懂。
.├── Bundle│ └── index.js # 负责打包├── Module│ └── index.js # 负责解析模块├── ast│ ├── Scope.js # 作用域链│ ├── analyse.js # 解析ast语法树│ └── walk.js # 遍历ast语法树├── finalisers # 输入类型│ ├── amd.js│ ├── cjs.js│ ├── es6.js│ ├── index.js│ └── umd.js├── rollup.js # 入口└── utils # 工具函数 ├── map-helpers.js ├── object.js ├── promise.js └── replaceIdentifiers.js
rollup.js
打包入口文件Bundle/index.js
打包工具,打包时生成一个Bundle
实例,收集依赖的所有模块,最初将代码打包在一起输入Module/index.js
每个文件就是一个模块ast/Scope.js
构建作用域和作用域链ast/analyse.js
剖析Ast
作用域和依赖项ast/walk.js
遍历Ast
简略浏览代码依赖的第三方库,能够看到magic-string和acorn,咱们先做个简略理解。
magic-string
作用是能够对源代码进行轻微的批改和替换。
const MagicString = require('magic-string')const s = new MagicString(`export var name = 'careteen'`)// 返回剪切后的字符console.log(s.snip(0, 6).toString(), s.toString()) // export, export var name = 'careteen'// 移除指定地位区间字符console.log(s.remove(0, 7).toString(), s.toString()) // var name = 'careteen', var name = 'careteen'// 用指定分隔符拼接源代码const b = new MagicString.Bundle()b.addSource({ content: `var name = 'careteen'`, separator: '\n',})b.addSource({ content: `var age = 25`, separator: '\n',})console.log(b.toString()) // var name = 'careteen' \n var age = 25
acorn
上面波及代码寄存在@careteen/rollup - prepos,感兴趣可返回调试。
JavaScript
解析器,将源代码解析成形象语法树AST
。
树上定义了代码的构造,通过操作这棵树,能够精准的定位到申明语句、赋值语句、运算语句等等。实现对代码的剖析、优化、变更等操作。
AST工作流
- Parse(解析) 将源代码转换成形象语法树,树上有很多的estree节点
- Transform(转换) 对形象语法树进行转换
- Generate(代码生成) 将上一步通过转换过的形象语法树生成新的代码
AST解析过程
借助astexplorer能够实时预览源代码的解析后果。并且设置Parser Settings
为acorn
。
上面代码解析后果如图
import $ from 'jquery';
那如何去遍历形象语法树,并在适合的机会操作它呢?
采纳深度优先遍历
。
还不理解的同学返回图解深度优先遍历算法
// walk.js// DFSfunction walk (node, { enter, leave }) { visit(node, null, enter, leave)}function visit (node, parent, enter, leave) { if (enter) { enter.call(null, node, parent) } const keys = Object.keys(node).filter(key => typeof node[key] === 'object') keys.forEach(key => { const value = node[key] if (Array.isArray(value)) { value.forEach(val => { visit(val, node, enter, leave) }) } else if (value && value.type) { visit(value, node, enter, leave) } }) if (leave) { leave.call(null, node, parent) }}module.exports = walk
此逻辑在源码rollup/ast/walk.js处
输入形象语法树
// ast.jsconst walk = require('./walk')const acorn = require('acorn')const ast = acorn.parse( `import $ from 'jquery';`, { locations: true, ranges: true, sourceType: 'module', ecmaVersion: 8, })let ident = 0const padding = () => ' '.repeat(ident)// Testast.body.forEach(statement => { walk(statement, { enter(node) { if (node.type) { console.log(padding() + node.type) ident += 2 } }, leave(node) { if (node.type) { ident -= 2 console.log(padding() + node.type) } } })})// 输入后果/**ImportDeclaration ImportDefaultSpecifier Identifier Identifier ImportDefaultSpecifier Literal LiteralImportDeclaration */
冀望的输入后果和上图构造一样。
Scope
源码处还有此文件rollup/ast/Scope.js绝对独立,其实为创立作用域的繁难实现。
// scope.jsclass Scope { constructor(options = {}) { this.name = options.name this.parent = options.parent this.names = options.params || [] } add(name) { this.names.push(name) } findDefiningScope(name) { if (this.names.includes(name)) { return this } if (this.parent) { return this.parent.findDefiningScope(name) } return null }}module.exports = Scope
此逻辑在源码rollup/ast/scope.js处
看看如何应用
// useScope.jsconst Scope = require('./scope')var a = 1function one() { var b = 2 function two() { var c = 3 console.log(a, b, c) } two()}one()// 构建scope chainconst globalScope = new Scope({ name: 'global', parent: null,})const oneScope = new Scope({ name: 'one', parent: globalScope,})const twoScope = new Scope({ name: 'two', parent: oneScope,})globalScope.add('a')oneScope.add('b')twoScope.add('c')console.log(twoScope.findDefiningScope('a'))console.log(oneScope.findDefiningScope('c'))console.log(globalScope.findDefiningScope('d'))// 输入后果// 1 2 3// Scope { name: 'global', parent: null, names: [ 'a' ] }// null// null
此文件次要作用是创立作用域和作用域链,并且将申明的变量挂载到对应的作用域,而且也提供办法findDefiningScope
查找具体变量所在的作用域。
其意义重大,rollup
能够借助他判断变量是否为以后文件定义,否则为import
导入,进而递归直到找到变量定义所在作用域,而后将依赖写入。
ES module
个性是export
的是值的援用,在import
后如果对其批改会扭转源,即只是个浅拷贝。
找到变量所在作用域后,可间接将其源代码剪切过去输入。(前面有具体实现)
实现rollup
新建可调式的配置文件,将src/index.js
作为入口文件,打包后输入到dest/bundle.js
// ./example/myRollup/rollup.config.jsconst path = require('path')const rollup = require('../../src/rollup')const entry = path.resolve(__dirname, 'src/index.js')rollup(entry, 'dest/bundle.js')
入口文件则依赖于bundle
对其真正编译打包。
// .src/rollup.jsconst Bundle = require('./bundle')function rollup(entry, filename) { const bundle = new Bundle({ entry, }) bundle.build(filename)}module.exports = rollup
初步思路则是解析入口文件entryPath
内容,对源代码做解析解决,并输入到指标文件下。
// .src/bundle.jsconst path = require('path')class Bundle { constructor(options) { this.entryPath = path.resolve(options.entry.replace(/\.js$/, '') + '.js') this.modules = {} } build(filename) { console.log(this.entryPath, filename) }}module.exports = Bundle
大抵流程为
- 获取入口文件的内容,包装成
module
,生成形象语法树
- 获取入口文件的内容,包装成
- 对入口文件形象语法树进行依赖解析
- 生成最终代码
写入指标文件
// .src/bundle.jsconst { readFileSync, writeFileSync } = require('fs')const { resolve } = require('path')const Module = require('./module')const MagicString = require('magic-string')class Bundle {constructor(options) { this.entryPath = resolve(options.entry.replace(/\.js$/, '') + '.js') this.modules = {} this.statements = []}build(filename) { // 1. 获取入口文件的内容,包装成`module`,生成形象语法树 const entryModule = this.fetchModule(this.entryPath) // 2. 对入口文件形象语法树进行依赖解析 this.statements = entryModule.expandAllStatements() // 3. 生成最终代码 const { code } = this.generate() // 4. 写入指标文件 writeFileSync(filename, code)}fetchModule(importee) { let route = importee if (route) { const code = readFileSync(route, 'utf-8') const module = new Module({ code, path: importee, bundle: this, }) return module }}generate() { const ms = new MagicString.Bundle() this.statements.forEach(statement => { const source = statement._source.clone() ms.addSource({ content: source, separator: '\n', }) }) return { code: ms.toString() }}}module.exports = Bundle
每一个文件即是一个module
,会将源代码解析成形象语法树,而后将源代码挂载到树的节点上,并提供开展批改办法。
// ./src/module.jsconst { parse } = require('acorn')const MagicString = require('magic-string')const analyse = require('./ast/analyse')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() } analyse() { analyse(this.ast, this.code, this) } expandAllStatements() { const allStatements = [] this.ast.body.forEach(statement => { const statements = this.expandStatement(statement) allStatements.push(...statements) }) return allStatements } expandStatement(statement) { statement._included = true const result = [] result.push(statement) return result }}module.exports = Module
将源代码挂载到树的节点上。
// ./src/ast/analyse.jsfunction analyse(ast, ms) { ast.body.forEach(statement => { Object.defineProperties(statement, { _source: { value: ms.snip(statement.start, statement.end) } }) })}module.exports = analyse
实现 tree shaking
将调试文件内容做如下批改,测试tree-shaking性能
// ./example/myRollup/src/index.jsimport { name, age } from './userinfo'function say() { console.log('hi ', name)}say()
依赖的userinfo文件
// ./example/myRollup/src/userinfo.jsexport var name = 'careteen'export var age = 25
冀望打包后果为
var name = 'careteen'function say() { console.log('hi ', name)}say()
须要做如下// +
处新增和批改
// ./src/bundle.jsconst { readFileSync, writeFileSync } = require('fs')const { resolve, isAbsolute, dirname } = require('path') // +const Module = require('./module')const MagicString = require('magic-string')class Bundle { constructor(options) { this.entryPath = resolve(options.entry.replace(/\.js$/, '') + '.js') this.modules = {} this.statements = [] } build(filename) { const entryModule = this.fetchModule(this.entryPath) this.statements = entryModule.expandAllStatements() const { code } = this.generate() writeFileSync(filename, code) } fetchModule(importee, importer) { // + let route // + if (!importer) { // + route = importee // + } else { // + if (isAbsolute(importee)) { // + route = importee // + } else if (importee[0] === '.') { // + route = resolve(dirname(importer), importee.replace(/\.js$/, '') + '.js') // + } // + } // + if (route) { const code = readFileSync(route, 'utf-8') const module = new Module({ code, path: importee, bundle: this, }) return module } } generate() { const ms = new MagicString.Bundle() this.statements.forEach(statement => { const source = statement._source.clone() if (/^Export/.test(statement.type)) { // + if (statement.type === 'ExportNamedDeclaration') { // + source.remove(statement.start, statement.declaration.start) // + } // + } // + ms.addSource({ content: source, separator: '\n', }) }) return { code: ms.toString() } }}module.exports = Bundle
次要是批改fetchModule
办法,解决相似于import { name } from './userinfo'
的状况时,userinfo
中若有定义变量或者依赖其余文件变量时,做递归解决。
生成代码时generate
办法中过滤掉ExportNamedDeclaration
语句,而将变量定义间接输入。
- export var name = 'careteen'+ var name = 'careteen'
对module模块做解决
// ./src/module.jsconst { parse } = require('acorn')const MagicString = require('magic-string')const analyse = require('./ast/analyse')function hasOwn(obj, prop) { // + return Object.prototype.hasOwnProperty.call(obj, prop) // +} // +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', }) // +++ start +++ this.imports = {} // 导入的变量 this.exports = {} // 导出的变量 this.definitions = {} // 变量定义的语句 // +++ end +++ this.analyse() } analyse() { // +++ start +++ // 收集导入和导出变量 this.ast.body.forEach(node => { if (node.type === 'ImportDeclaration') { const source = node.source.value node.specifiers.forEach(specifier => { const { name: localName} = specifier.local const { name } = specifier.imported this.imports[localName] = { source, name, localName, } }) } else if (node.type === 'ExportNamedDeclaration') { const { declaration } = node if (declaration.type === 'VariableDeclaration') { const { name } = declaration.declarations[0].id this.exports[name] = { node, localName: name, expression: declaration, } } } }) // +++ end +++ analyse(this.ast, this.code, this) // +++ start +++ // 收集所有语句定义的变量,建设变量和申明语句之间的对应关系 this.ast.body.forEach(statement => { Object.keys(statement._defines).forEach(name => { this.definitions[name] = statement }) }) // +++ end +++ } expandAllStatements() { const allStatements = [] this.ast.body.forEach(statement => { // +++ start +++ // 过滤`import`语句 if (statement.type === 'ImportDeclaration') { return } // +++ end +++ const statements = this.expandStatement(statement) allStatements.push(...statements) }) return allStatements } expandStatement(statement) { statement._included = true const result = [] // +++ start +++ const dependencies = Object.keys(statement._dependsOn) dependencies.forEach(name => { const definition = this.define(name) result.push(...definition) }) // +++ end +++ result.push(statement) return result } // +++ start +++ define(name) { if (hasOwn(this.imports, name)) { const importDeclaration = this.imports[name] const mod = this.bundle.fetchModule(importDeclaration.source, this.path) const exportDeclaration = mod.exports[importDeclaration.name] if (!exportDeclaration) { throw new Error(`Module ${mod.path} does not export ${importDeclaration.name} (imported by ${this.path})`) } return mod.define(exportDeclaration.localName) } else { let statement = this.definitions[name] if (statement && !statement._included) { return this.expandStatement(statement) } else { return [] } } } // +++ end +++}module.exports = Module
须要做几件事
收集导入和导出变量
- 建设映射关系,不便后续应用
收集所有语句定义的变量
- 建设变量和申明语句之间的对应关系,不便后续应用
过滤
import
语句- 删除关键词
输入语句时,判断变量是否为
import
- 如是须要递归再次收集依赖文件的变量
- 否则间接输入
构建依赖关系,创立作用域链,交由
./src/ast/analyse.js
文件解决- 在形象语法树的每一条语句上挂载
_source(源代码)、_defines(以后模块定义的变量)、_dependsOn(内部依赖的变量)、_included(是否曾经蕴含在输入语句中)
- 收集每个语句上定义的变量,创立作用域链
- 收集内部依赖的变量
- 在形象语法树的每一条语句上挂载
// ./src/ast/analyse.js// +++ start +++const Scope = require('./scope')const walk = require('./walk')function analyse(ast, ms) { let scope = new Scope() // 创立作用域链、 ast.body.forEach(statement => { function addToScope(declarator) { const { name } = declarator.id scope.add(name) if (!scope.parent) { // 如果没有下层作用域,阐明是模块内的定级作用域 statement._defines[name] = true } } Object.defineProperties(statement, { _source: { // 源代码 value: ms.snip(statement.start, statement.end), }, _defines: { // 以后模块定义的变量 value: {}, }, _dependsOn: { // 以后模块没有定义的变量,即内部依赖的变量 value: {}, }, _included: { // 是否曾经蕴含在输入语句中 value: false, writable: true, }, }) // 收集每个语句上定义的变量,创立作用域链 walk(statement, { enter(node) { let newScope switch (node.type) { case 'FunctionDeclaration': const params = node.params.map(p => p.name) addToScope(node) newScope = new Scope({ parent: scope, params, }) break; case 'VariableDeclaration': node.declarations.forEach(addToScope) break; } if (newScope) { Object.defineProperty(node, '_scope', { value: newScope, }) scope = newScope } }, leave(node) { if (node._scope) { scope = scope.parent } }, }) }) ast._scope = scope // 收集内部依赖的变量 ast.body.forEach(statement => { walk(statement, { enter(node) { if (node.type === 'Identifier') { const { name } = node const definingScope = scope.findDefiningScope(name) // 作用域链中找不到 则阐明为内部依赖 if (!definingScope) { statement._dependsOn[name] = true } } }, }) })}module.exports = analyse// +++ end +++
依赖的遍历和作用域可间接应用上文提到的walk和Scope的实现。
依赖的变量有做批改操作
通过上述解决,曾经根本实现繁难tree-shaking
性能,然而对于如下依赖文件中导出变量有做过批改还需解决
// ./example/myRollup/src/userinfo.jsexport var name = 'careteen'name += 'lan'name ++export var age = 25
须要在./src/module.js
文件下做如下// +
的新增批改
// ./src/module.jsclass Module { constructor() { // ... this.definitions = {} // 变量定义的语句 this.modifications = {} // 批改的变量 // + this.analyse() } analyse() { // ... // 收集所有语句定义的变量,建设变量和申明语句之间的对应关系 this.ast.body.forEach(statement => { Object.keys(statement._defines).forEach(name => { this.definitions[name] = statement }) // +++ start +++ Object.keys(statement._modifies).forEach(name => { if (!hasOwn(this.modifications, name)) { this.modifications[name] = [] } // 可能有多处批改 this.modifications[name].push(statement) }) // +++ end +++ }) } expandStatement(statement) { statement._included = true const result = [] const dependencies = Object.keys(statement._dependsOn) dependencies.forEach(name => { const definition = this.define(name) result.push(...definition) }) result.push(statement) // +++ start +++ // 以后模块下所定义的变量 若有批改 则退出result const defines = Object.keys(statement._defines) defines.forEach(name => { const modifications = hasOwn(this.modifications, name) && this.modifications[name] if (modifications) { modifications.forEach(modif => { if (!modif._included) { const statements = this.expandStatement(modif) result.push(...statements) } }) } }) // +++ end +++ return result } }
须要做几件事
在形象语法树每条语句中定义批改的变量
_modifies
(交由src/ast/analyse.js
解决)- 收集内部依赖的变量(下面曾经实现过)
- 收集变量批改的语句
- 将所有批改语句的变量寄存到
modifications
- 输入语句时,判断定义的变量
_defines
中是否含有批改语句变量,若有则须要输入
// ./src/ast/analyse.jsfunction analyse(ast, ms) { ast.body.forEach(statement => { // ... Object.defineProperties(statement, { // ... _modifies: { // 批改的变量 value: {}, // + }, }) }) ast.body.forEach(statement => { // +++ start +++ // 收集内部依赖的变量 function checkForReads(node) { if (node.type === 'Identifier') { const { name } = node const definingScope = scope.findDefiningScope(name) // 作用域链中找不到 则阐明为内部依赖 if (!definingScope) { statement._dependsOn[name] = true } } } // 收集变量批改的语句 function checkForWrites(node) { function addNode(n) { while (n.type === 'MemberExpression') { // var a = 1; var obj = { c: 3 }; a += obj.c; n = n.object } if (n.type !== 'Identifier') { return } statement._modifies[n.name] = true } if (node.type === 'AssignmentExpression') { addNode(node.left) } else if (node.type === 'UpdateExpression') { // var a = 1; a++ addNode(node.argument) } else if (node.type === 'CallExpression') { node.arguments.forEach(addNode) } } // // +++ end +++ walk(statement, { enter(node) { // +++ start +++ if (node._scope) { scope = node._scope } checkForReads(node) checkForWrites(node) }, leave(node) { if (node._scope) { scope = scope.parent } } // +++ end +++ }) }) }
通过上述解决后,可失去冀望的输入后果
// ./example/myRollup/dest/bundle.jsvar name = 'careteen'name += 'lan'name ++function say() { console.log('hi ', name)}say()
反对块级作用域
对于如下语句,还需提供反对
if(true) { var blockVariable = 25}console.log(blockVariable)
须要做如下解决
// ./src/ast/scope.jsclass Scope { constructor(options = {}) { // ... this.isBlockScope = !!options.block // 是否为块作用域 } // +++ start +++ add(name, isBlockDeclaration) { if (this.isBlockScope && !isBlockDeclaration) { // 以后作用域是块级作用域 && 此语句为var或申明函数 this.parent.add(name, isBlockDeclaration) } else { this.names.push(name) } } // +++ end +++}
- 创立作用域时,辨别块级作用域和一般变量定义
// ./src/ast/analyse.jsfunction analyse(ast, ms) { ast.body.forEach(statement => { function addToScope(declarator, isBlockDeclaration = false) { // + const { name } = declarator.id scope.add(name, isBlockDeclaration) // + // ... } // ... // 收集每个语句上定义的变量,创立作用域链 walk(statement, { enter(node) { let newScope switch (node.type) { // +++ start +++ case 'FunctionExpression': case 'FunctionDeclaration': const params = node.params.map(p => p.name) if (node.type === 'FunctionDeclaration') { addToScope(node) } else if (node.type === 'FunctionExpression' && node.id) { params.push(node.id.name) } newScope = new Scope({ parent: scope, params, block: true, }) break; case 'BlockStatement': newScope = new Scope({ parent: scope, block: true, }) break; case 'VariableDeclaration': node.declarations.forEach(variableDeclarator => { if (node.kind === 'let' || node.kind === 'const') { addToScope(variableDeclarator, true) } else { addToScope(variableDeclarator, false) } }) break; // +++ end +++ } } } }}
输入语句时将零碎变量console.log
做解决,防止输入两次。
// ./src/module.jsconst SYSTEM_VARIABLE = ['console', 'log']class Module { // ... define(name) { if (hasOwn(this.imports, name)) { // ... } else { let statement = this.definitions[name] // +++ start +++ if (statement && !statement._included) { return this.expandStatement(statement) } else if (SYSTEM_VARIABLE.includes(name)) { return [] } else { throw new Error(`variable '${name}' is not exist`) } // +++ end +++ } } }
解决入口处 tree shaking
上述tree-shaking是针对于有import
语句时的解决,对于入口文件有定义但未应用变量时,还需解决
var company = 'sohu focus'var companyAge = 23console.log(company)
- 过滤定义但未应用的变量
- 收集定义变量时,如果变量曾经输入则不再输入
// ./src/module.jsclass Module { // ... expandAllStatements() { if (statement.type === 'ImportDeclaration') { return } // +++ start +++ // 过滤定义但未应用的变量 if (statement.type === 'VariableDeclaration') { return } // +++ end +++ } define(name) { if (hasOwn(this.imports, name)) { // ... } else { let statement = this.definitions[name] // +++ start +++ if (statement) { if (statement._included) { return [] } else { return this.expandStatement(statement) } // +++ end +++ } else if (SYSTEM_VARIABLE.includes(name)) { return [] } else { throw new Error(`variable '${name}' is not exist`) } } } }
实现变量重命名
存在如下状况,多个模块都有某个变量雷同命名company
// ./example/myRollup/src/compay1.tsconst company = 'qunar'export const company1 = company + '1'// ./example/myRollup/src/compay2.tsconst company = 'sohu'export const company2 = company + '2'
// ./example/myRollup/src/index.tsimport { company1 } from './compay1'import { company2 } from './compay2'console.log(company1, company2)
此时须要在打包时对重名变量适当重命名再输入
先抽离和筹备工具函数
// ./src/utils.jsconst walk = require('./ast/walk')function hasOwn(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop)}function replaceIdentifiers(statement, source, replacements) { walk(statement, { enter(node) { if (node.type === 'Identifier') { if (node.name && replacements[node.name]) { source.overwrite(node.start, node.end, replacements[node.name]) } } } })}module.exports = { hasOwn, replaceIdentifiers,}
在形象语法树每条语句上挂载以后模块的实例
// ./src/ast/analyse.jsfunction analyse(ast, ms, module) { // + ast.body.forEach(statement => { Object.defineProperties(statement, { _module: { // module实例 value: module, // + }, // ... } }}
在module上提供重命名的办法
// ./src/module.jsclass Module { constructor() { // ... this.canonicalNames = {} // 不重名的变量 this.analyse() } // +++ start ++ rename(name, replacement) { this.canonicalNames[name] = replacement } getCanonicalName(localName) { if (!hasOwn(this.canonicalNames, localName)) { this.canonicalNames[localName] = localName } return this.canonicalNames[localName] } // +++ end ++}
- 收集反复命名的变量
重命名反复命名的变量
// ./src/bundle.jsconst { hasOwn, replaceIdentifiers } = require('./utils')class Bundle {constructor(options) {build(filename) { const entryModule = this.fetchModule(this.entryPath) this.statements = entryModule.expandAllStatements() this.definesConflict() // + const { code } = this.generate() writeFileSync(filename, code)}// +++ start +++definesConflict() { const defines = {} const conflicts = {} this.statements.forEach(statement => { Object.keys(statement._defines).forEach(name => { if (hasOwn(defines, name)) { conflicts[name] = true } else { defines[name] = [] } defines[name].push(statement._module) }) }) Object.keys(conflicts).forEach(name => { const modules = defines[name] modules.pop() // 最初一个重名的不解决 modules.forEach(module => { const replacement = getSafeName(name) module.rename(name,replacement) }) }) function getSafeName(name) { while (hasOwn(conflicts, name)) { name = `_${name}` } conflicts[name] = true return name }}// +++ end +++generate() { const ms = new MagicString.Bundle() this.statements.forEach(statement => { // +++ start +++ let replacements = {} Object.keys(statement._dependsOn) .concat(Object.keys(statement._defines)) .forEach(name => { const canonicalName = statement._module.getCanonicalName(name) if (name !== canonicalName) { replacements[name] = canonicalName } }) // +++ end +++ const source = statement._source.clone() if (/^Export/.test(statement.type)) { if (statement.type === 'ExportNamedDeclaration') { source.remove(statement.start, statement.declaration.start) } } replaceIdentifiers(statement, source, replacements) // + }}}
进过上述解决,可失去如下输入后果
// ./example/myRollup/dest/bundle.jsconst _company = 'qunar'const company1 = _company + '1'const company = 'sohu'const company2 = company + '2'console.log(company1, company2)
✌ ✌
总结
本文从rollup
应用再到源码揭秘,实现了Tree-shaking
繁难性能,所有代码寄存在@careteen/rollup中。感兴趣的同学能够返回调试。
援用
- Rollup 官网
- ECMA Module
- ES module 工作原理
- webpack繁难实现
- commonjs标准原理
- 在线解析AST