Webpack 4 教程 - 7. 通过“tree shaking”减少打包的尺寸
在这一部分的webpack 4 教程中,我们继续进一步考虑优化。我们要学习“tree shaking”是啥以及如何使用。你会学到在webpack 4中使用“tree shaking”技术有什么要求,它能带来什么好处。开始吧!
Webpack 4 Tree Shaking
首先,我们回答“tree shaking”是啥技术以及它能带来什么好处。我们常常碰到这样的案例,需要从某文件中命名导出(某一个或几个变量、函数、对象等),然而这个文件还有许多其它(我们这次并不需要)的导出,webpack会不管三七二十一简单粗暴的将整个模块包含进来,使得我们最终打包的文件里有了许多不需要的垃圾。这就到了tree shaking出手的地方了,因为它能帮助我们干掉那些死代码,大大减少打包的尺寸。
如果你想对imports和exports了解更多,请查阅本教程1。要想让tree shaking能“摇起来”,有几个要求,首先,必须使用ES6模块,不能使用其它类型的模块如CommonJS之流。如果使用Babel的话,这里有一个小问题,因为Babel的预案(preset)默认会将任何模块类型都转译成CommonJS类型。修正这个问题也很简单,不是在.babelrc文件中就是在webpack.config.js文件中设置modules: false就好了。
// .babelrc { "presets": [ ["env", { "modules": false } ] ] }
// webpack.config.js module: { rules: [ { test: /\.js$/, exclude: /(node_modules)/, use: { loader: 'babel-loader', options: { presets: ['env', { modules: false }] } } } ] },如果你想阅读更多有关babel-loader或loaders的一般用法,请看本教程之2
第二个要求,需要使用UglifyJsPlugin插件。如果在mode:"production"模式,这个插件已经默认添加了,如果在其它模式下,可以手工添加它。
不熟悉UglifyJsPlugin插件的话,请参阅本教程5另外要记住的是打开optimization.usedExports。在mode: "production"模式下,它也是默认打开了的。它告诉webpack每个模块明确使用exports。这样之后,webpack会在打包文件中添加诸如/* unused harmony export */
这样的注释,其后UglifyJsPlugin插件会对这些注释作出理解。
我们来看看下面的情形:
// utilities.js export function add(a, b) { return a + b; } export function subtract(a, b) { return a - b; }
// index.js import { add } from './utilities'; console.log(add(1,2)); console.log(add(3,4));
配置不恰当的运行就会有如下的输出:
/*(...)*/ /* 1 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "add", function() { return add; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "substract", function() { return substract; }); function add(a, b) { return a + b; } function subtract(a, b) { return a - b; } /***/ }) /******/ ]);
正如你所见,webpack没有“摇”我们的包!这里既有add函数又有subtract函数。再使用如下的配置我们来体验一点点不同:
// webpack.config.js const webpack = require('webpack'); const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); const UglifyJS = require('uglify-es'); const DefaultUglifyJsOptions = UglifyJS.default_options(); const compress = DefaultUglifyJsOptions.compress; for(let compressOption in compress) { compress[compressOption] = false; } compress.unused = true; module.exports = { mode: 'none', optimization: { minimize: true, minimizer: [ new UglifyJsPlugin({ uglifyOptions: { compress, mangle: false, output: { beautify: true } }, }) ], } }
我关掉了UglifyJsPlugin插件的大部分选项配置以便更清楚的观察代码中发生了什么。运行之后有下面的输出:
/* (...) */ /* 0 */ /***/ function() { "use strict"; // CONCATENATED MODULE: ./src/utilities.js function add(a, b) { return a + b; } // CONCATENATED MODULE: ./src/index.js console.log(add(1, 2)); console.log(add(3, 4)); /***/} /******/ ]);
正因为optimization.usedExports和UglifyJsPlugin插件关掉的配置项,垃圾代码清除了。请注意,这是UglifyJsPlugin插件的默认行为,所以默认配置下使用UglifyJsPlugin也将清除死代码(除非运行了其它的压缩进程)。
库的Tree Shaking
如果你想要对库进行tree shake,首先要记住的注意点还是前面所说的:使用ES6模块。然而许多库并不一定使用ES6模块,典型的例子比如lodash就是这样,看它的源代码,很明显它没有使用ES6模块。
假设要使用lodash库中的debounce函数。
// index.js import _ from 'lodash'; console.log(_.debounce);
现在你的输出里把整个lodash都打进来了。当使用import _ from 'lodash'时没有办法避免这一点。还好,有人创建了一个叫lodash-es的库,它改写lodash按ES6模块的形式导出。
import { debounce } from 'lodash';// (译注:原文如此,应该为'lodash-es') console.log(debounce);
很不幸,webpack依旧没有对其tree shake。根据ECMAScript规范,所有的子模块都需要去评估,因为它们可能有副作用。我推荐读一下Sean Larking(他是webpack核心团队成员之一)在Stack Overflow上写的这篇很好的解释。库的作者可以在package.json文件里注明它的库没有副作用。打开lodash库的package.json文件,可以看到有"sideEffects": false的标注。那么这儿的问题是什么?
Webpack默认忽略了sideEffect标注,改变此行为需要设置optimization.sideEffects为true。你能手工设置它或通过设置mode:"production"模式也行。
// webpack.config.js const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { mode: 'none', optimization: { minimize: true, minimizer: [ new UglifyJsPlugin() ], usedExports: true, sideEffects: true }, plugins: [ new HtmlWebpackPlugin() ] }
现在webpack终于对lodash库“摇”起来了。
小结
要让tree shaking好好工作,有一定条件。这么很有用的功能,当然值得学习。希望通过这篇文章,你会知道怎么去使用它,因为它能大大减少你打包的体积。记住需要使用ES6模块和UglifyJsPlugin插件,以及配置optimization选项,设置usedExports和sideEffects为true。