• 揭秘 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 moduletree-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 Settingsacorn

上面代码解析后果如图

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

大抵流程为

    1. 获取入口文件的内容,包装成module,生成形象语法树
    1. 对入口文件形象语法树进行依赖解析
    1. 生成最终代码
    1. 写入指标文件

      // .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