原文地址:http://www.2ality.com/2015/12/webpack-tree-shaking.html
校对:Kathy

Rich Harris 的模块打包器 Rollup 普及了 JavaScript 圈内一个重要的特性:Tree-shaking,即从模块包中排除未使用的 exports 项。Rollup 基于 ES6 模块的静态结构(引入和导出不会在运行时改变)来检测哪一个模块没有被使用。

Webpack 的 Tree-shaking 特性目前正在 beta 阶段,这篇博文解释了它是如何工作的,相关的项目代码可以在 Github 上查看:Tree-shaking Demo

1. Webpack 2 如何消除未使用的导出

Webpack 2 是一个尚处于 beta 阶段的新版本,它通过下面两个步骤来消除未使用的导出:

  • 首先,所有 ES6 模块文件会被合并为一个打包文件。在这个文件中在任何地方都没有被引入的导出将不再会被打包(译注:export 语句将会直接被删除)。
  • 其次,通过消除不可到达的代码,将打包文件精简。因此,那些既没有被导出,又没有在其模块中被使用的代码将不会出现在精简的打包文件中。如果没有第一个步骤,那些被导出的不可到达代码永远不会被删除(export 关键字会访问他们)。

只有模块系统拥有一个静态的结构时,模块加载器才能在构建阶段对没有使用过的 export 语句进行可靠的检测。因此,Webpack 2 可以解析并且理解所有 ES6 代码,并且仅对检测到的 ES6 模块进行 Tree-shaking。但不管怎样,只有 importexport 语句会被 Webpack 转译为 ES5,如果希望所有代码都转译为 ES5,你仍然需要一个转译器对剩余部分的代码进行转译。在这篇博文中,我们会使用 Babel 6。

2. 输入:ES6 代码

Demo 项目有两个 ES6 模块。

helpers.js 包含辅助函数:

// helpers.js
export function foo() {
  return 'foo';
}
export function bar() {
  return 'bar';
}

main.js,web 应用的入口:

// main.js
import { foo } from './helpers';

let elem = document.getElementById('output');
elem.innerHTML = `Output: ${foo()}`;

需要注意的是在 helpers 模块中导出的 bar 在整个项目的任何地方都没有被使用过。

3. 不使用 Tree-shaking 的输出

Babel 6 的典型选择是使用 es2015 这个 preset:

{
  presets: ['es2015'],
}

不管怎样,这一 preset 包含了 transform-es2015-modules-commonjs 这个插件,这意味着 Babel 会输出 CommonJS 规范的模块,这时 Webpack 无法进行 Tree-shaking:

function(module, exports) {

  'use strict';

  Object.defineProperty(exports, "__esModule", {
    value: true
  });
  exports.foo = foo;
  exports.bar = bar;
  function foo() {
    return 'foo';
  }
  function bar() {
    return 'bar';
  }

}

你可以看到在代码精简的过程中,bar 作为导出的一部分使它不能被识别为不可访问的代码。

4. 使用 Tree-shaking 的输出

我们需要的是 Babel 的 es2015,并不需要其中的 transform-es2015-modules-commonjs 插件。这时,实现以上需求的唯一途径是在配置中声明除 transform-es2015-modules-commonjs 以外的所有的插件。preset 的源代码托管在 Gibhub 上,所以我们这里复制粘贴需要的插件:

{
  plugins: [
    'transform-es2015-template-literals',
    'transform-es2015-literals',
    'transform-es2015-function-name',
    'transform-es2015-arrow-functions',
    'transform-es2015-block-scoped-functions',
    'transform-es2015-classes',
    'transform-es2015-object-super',
    'transform-es2015-shorthand-properties',
    'transform-es2015-computed-properties',
    'transform-es2015-for-of',
    'transform-es2015-sticky-regex',
    'transform-es2015-unicode-regex',
    'check-es2015-constants',
    'transform-es2015-spread',
    'transform-es2015-parameters',
    'transform-es2015-destructuring',
    'transform-es2015-block-scoping',
    'transform-es2015-typeof-symbol',
    ['transform-regenerator', { async: false, asyncGenerators: false }],
  ],
}

如果现在我们构建这个项目,helper 模块在打包文件中看起来就像这样:

function(module, exports, __webpack_require__) {

  /* harmony export */ exports["foo"] = foo;
  /* unused harmony export bar */;

  function foo() {
      return 'foo';
  }
  function bar() {
      return 'bar';
  }
}

现在,只有 foo 被导出了,但 bar 的代码仍然存在。经过代码精简,helpers 看起来就像这样:

function (t, n, r) {
  function e() {
      return "foo"
  }

  n.foo = e
}

Et voilà – 再也没有 bar 方法了!

5. 进一步阅读

标签: none

已有 2 条评论

  1. [...][译文]使用 Webpack 2 和 Babel 6 进行 Tree-shaking | Zhang Visper | 2016-04-29[...]

  2. [...][译文]使用 Webpack 2 和 Babel 6 进行 Tree-shaking | Zhang Visper | 2016-04-29[...]

添加新评论