- 原文地址:How to do proper tree-shaking in Webpack 2
- 原文做者:Gábor Soós
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:薛定谔的猫
- 校对者:lsvih、lampui
tree-shaking 这个术语首先源自 Rollup -- Rich Harris 写的模块打包工具。它是指在打包时只包含用到的 Javascript 代码。它依赖于 ES6 静态模块(exports 和 imports 不能在运行时修改),这使咱们在打包时能够检测到未使用的代码。Webpack 2 也引入了这一特性,Webpack 2 已经内置支持 ES6 模块和 tree-shaking。本文将会介绍如何在 webpack 中使用这一特性,如何克服使用中的难点。前端
若是想跳过,直接看例子请访问 Babel、Typescript。react
理解在 Webpack 中使用 tree-shaking 的最佳的方式是经过一个微型应用例子。我将它比做一个汽车有特定的引擎,该应用由 2 个文件组成。第 1 个文件有:一些 class,表明不一样种类的引擎;一个函数返回其版本号 -- 都经过 export 关键字导出。android
export class V6Engine {
toString() {
return 'V6';
}
}
export class V8Engine {
toString() {
return 'V8';
}
}
export function getVersion() {
return '1.0';
}复制代码
第 2 个文件表示一个汽车拥有它本身的引擎,将这个文件做为应用打包的入口(entry)。webpack
import { V8Engine } from './engine';
class SportsCar {
constructor(engine) {
this.engine = engine;
}
toString() {
return this.engine.toString() + ' Sports Car';
}
}
console.log(new SportsCar(new V8Engine()).toString());复制代码
经过定义类 SportsCar,咱们只使用了 V8Engine,而没有用到 V6Engine。运行这个应用会输出:‘V8 Sports Car’。ios
应用了 tree-shaking 后,咱们指望打包结果只包含用到的类和函数。在这个例子中,意味着它只有 V8Engine 和 SportsCar 类。让咱们来看看它是如何工做的。git
咱们打包时不使用编译器(Babel 等)和压缩工具(UglifyJS 等),能够获得以下输出:github
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
/* unused harmony export getVersion */
class V6Engine {
toString() {
return 'V6';
}
}
/* unused harmony export V6Engine */
class V8Engine {
toString() {
return 'V8';
}
}
/* harmony export (immutable) */ __webpack_exports__["a"] = V8Engine;
function getVersion() {
return '1.0';
}
/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__engine__ = __webpack_require__(0);
class SportsCar {
constructor(engine) {
this.engine = engine;
}
toString() {
return this.engine.toString() + ' Sports Car';
}
}
console.log(new SportsCar(new __WEBPACK_IMPORTED_MODULE_0__engine__["a" /* V8Engine */]()).toString());
/***/ })复制代码
Webpack 用注释 /\unused harmony export V6Engine*/ 将未使用的类和函数标记下来,用 /*harmony export (immutable)*/ webpack_exports[“a”] = V8Engine;* 来标记用到的。你应该会问未使用的代码怎么还在?tree-shaking 没有生效吗?web
背后的缘由是:Webpack 仅仅标记未使用的代码(而不移除),而且不将其导出到模块外。它拉取全部用到的代码,将剩余的(未使用的)代码留给像 UglifyJS 这类压缩代码的工具来移除。UglifyJS 读取打包结果,在压缩以前移除未使用的代码。经过这一机制,就能够移除未使用的函数 getVersion 和类 V6Engine。typescript
而 Rollup 不一样,它(的打包结果)只包含运行应用程序所必需的代码。打包完成后的输出并无未使用的类和函数,压缩仅涉及实际使用的代码。json
UglifyJS 不支持 ES6(又名 ES2015)及以上。咱们须要用 Babel 将代码编译为 ES5,而后再用 UglifyJS 来清除无用代码。
最重要的是让 ES6 模块不受 Babel 预设(preset)的影响。Webpack 认识 ES6 模块,只有当保留 ES6 模块语法时才可以应用 tree-shaking。若是将其转换为 CommonJS 语法,Webpack 不知道哪些代码是使用过的,哪些不是(就不能应用 tree-shaking了)。最后,Webpack将把它们转换为 CommonJS 语法。
咱们须要告诉 Babel 预设(在这个例子中是babel-preset-env)不要转换 module。
{
"presets": [
["env", {
"loose": true,
"modules": false
}]
]
}复制代码
对应 Webpack 配置:
module: {
rules: [
{ test: /\.js$/, loader: 'babel-loader' }
]
},
plugins: [
new webpack.LoaderOptionsPlugin({
minimize: true,
debug: false
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: true
},
output: {
comments: false
},
sourceMap: false
})
]复制代码
来看一下 tree-shaking 以后的输出: link to minified code.
能够看到函数 getVersion 被移除了,这是咱们所预期的,然而类 V6Engine 并无被移除。这是什么缘由呢?
首先 Babel 检测到 ES6 模块将其转换为 ES5,而后 Webpack 将全部的模块汇集起来,最后 UglifyJS 会移除未使用的代码。咱们来看一下 UglifyJS 的输出,就能够找到问题出在哪里。
WARNING in car.prod.bundle.js from UglifyJs
Dropping unused function getVersion [car.prod.bundle.js:103,9]
Side effects in initialization of unused variable V6Engine [car.prod.bundle.js:79,4]
它告诉咱们类 V6Engine 转换为 ES5 的代码在初始化时有反作用。
var V6Engine = function () {
function V6Engine() {
_classCallCheck(this, V6Engine);
}
V6Engine.prototype.toString = function toString() {
return 'V6';
};
return V6Engine;
}();复制代码
在使用 ES5 语法定义类时,类的成员函数会被添加到属性 prototype,没有什么方法能彻底避免此次赋值。UglifyJS 不可以分辨它仅仅是类声明,仍是其它有反作用的操做 -- UglifyJS 不能作控制流分析。
编译过程阻止了对类进行 tree-shaking。它仅对函数起做用。
在 Github 上,有一些相关的 bug report:Webpack repository、UglifyJS repository。一个解决方案是 UglifyJS 彻底支持 ES6,但愿下个主版本可以支持。另外一个解决方案是将其标记为 pure(无反作用),以便 UglifyJS 可以处理。这种方法已经实现,但要想生效,还需编译器支持将类编译后的赋值标记为 @__PURE__。实现进度:Babel、Typescript。
Babel 的开发者们认为:为何不开发一个基于 Babel 的代码压缩工具,这样就可以识别 ES6+ 的语法了。因此他们开发了Babili,全部 Babel 能够解析的语言特性它都支持。Babili 能将 ES6 代码编译为 ES5,移除未使用的类和函数,这就像 UglifyJS 已经支持 ES6 同样。
Babili 会在编译前删除未使用的代码。在编译为 ES5 以前,很容易找到未使用的类,所以 tree-shaking 也能够用于类声明,而再也不仅仅是函数。
咱们只需用 Babili 替换 UglifyJS,而后删除 babel-loader 便可。另外一种方式是将 Babili 做为 Babel 的预设,仅使用 babel-loader(移除 UglifyJS 插件)。推荐使用第一种(插件的方式),由于当编译器不是 Babel(好比 Typescript)时,它也能生效。
module: {
rules: []
},
plugins: [
new BabiliPlugin()
]复制代码
咱们须要将 ES6+ 代码传给 BabiliPlugin,不然它不用移除(未使用的)类。
使用 Typescript 等编译器时,也应当使用 ES6+。Typescript 应当输出 ES6+ 代码,以便 tree-shaking 可以生效。
如今的输出再也不包含类 V6Engine:压缩后代码。
对第三方包来讲也是,应当使用 ES6 模块。幸运的是,愈来愈多的包做者同时发布 CommonJS 格式 和 ES6 格式的模块。ES6 模块的入口由 package.json 的字段 module 指定。
对 ES6 模块,未使用的函数会被移除,但 class 并不必定会。只有当包内的 class 定义也为 ES6 格式时,Babili 才能将其移除。不多有包可以以这种格式发布,但有的作到了(好比说 lodash 的 lodash-es)。
罪魁祸首是当包的单独文件经过扩展它们来修改其余模块时,导入文件有反作用。RxJs就是一个例子。经过导入一个运算符来修改其中一个类,这些被认为是反作用,它们阻止代码进行 tree-shaking。
经过 tree-shaking 你能够至关程度上减小应用的体积。Webpack 2 内置支持它,但其机制并不一样于 Rollup。它会包含全部的代码,标记未使用的函数和函数,以便压缩工具可以移除。这就是对全部代码都进行 tree-shake 的困难之处。使用默认的压缩工具 UglifyJS,它仅移除未使用的函数和变量;Babili 支持 ES6,能够用它来移除(未使用的)类。咱们还必须特别注意第三方模块发布的方式是否支持 tree-shaking。
但愿这篇文章为您清楚阐述了 Webpack tree-shaking 背后的原理,并为您提供了克服困难的思路。
实际例子请访问 Babel、Typescript。
感谢阅读!喜欢本文请点击原文中的 ❤,而后分享到社交媒体上。欢迎关注 Medium,Twitter 阅读更多有关 Javascript 的内容!
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、React、前端、后端、产品、设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。