本文深入解析 Webpack 中 Tree-Shaking 的工作原理,指出其依赖于 ES6 模块的静态解析特性。通过对比 CommonJS 与 ES6 模块的差异,演示如何在 JavaScript 项目中启用并验证摇树优化。重点解决 TypeScript 编译后未能触发 Tree-Shaking 的问题,通过调整 tsconfig.json 目标为 ES6 实现完整的代码剔除。
Tree-Shaking 原理与 ES6 模块基础
1.什么是tree-shaking
Webpack 2 引入的最重要特性之一是 Tree-Shaking。该技术最初源自 Rollup.js,如今已被 Webpack 2 采纳并实现类似功能。
Webpack 中 Tree-Shaking 的实现得益于 ES6 规范模块。前端模块化规范众多,包括 CommonJS、AMD 和 ES6 等。这里简要分析 CommonJS 与 ES6 模块的核心区别。
CommonJS 模块
CommonJS 模块规范在 Node.js 环境中广泛应用,其主要特性包括以下几点:
1.动态加载模块
CommonJS 与 ES6 的最大区别在于此。CommonJS 模块支持动态加载,从而轻松实现懒加载,优化用户体验。
2.加载整个模块
在 CommonJS 模块中,导出的是整个模块对象。
3.每个模块皆为对象
CommonJS 将每个模块视为一个对象。
4.值拷贝
CommonJS 模块的输出类似于函数的值传递,即进行值的拷贝。
ES6 模块
1.静态解析
即在解析阶段就确定输出的模块,因此 ES6 模块的 import 语句通常位于被引入文件的开头。
2.模块不是对象
在 ES6 中,每个模块不被视为一个对象。
3.加载的不是整个模块
在 ES6 模块中,经常可以看到一个模块包含多个 export 导出。
4.模块的引用
ES6 模块导出的并非模块的值拷贝,而是对模块的引用。
结合 ES6 与 CommonJS 模块的区别可知,ES6 的特点是静态解析,而 CommonJS 模块的特点是动态解析。因此,借助 ES6 模块的静态解析能力,Tree-Shaking 的实现才成为可能。 在 Webpack 中,Tree-Shaking 指的是按需加载,即未被引用的模块不会被打包进来,从而减少包体积,缩短应用加载时间,为用户提供更佳的体验。
JavaScript 项目中的 Tree-Shaking 配置
2.怎么使用tree-shaking
那么,具体如何启用 Tree-Shaking 呢? Webpack 默认对使用 ES6 规范编写的模块启用 Tree-Shaking。具体含义如下例所示。
首先展示我的 demo 目录结构如下:
├─dist └─index.html ├─node_modules └─... ├─src ├─scripts ├─assets ├─webpack.config.js └─package.json
dist 目录用于存放打包后的代码。
src 目录相反,用于存放源文件。
src 中的 scripts 目录用于存放 JS 脚本文件,assets 用于存放静态资源文件。
执行以下几条命令后,开始我们的 Tree-Shaking 之旅。
npm install --save-dev webpack webpack-dev-server
webpack.config.js
const webpack = require('webpack')
const path = require('path')
module.exports = {
entry:'./src/scripts/main.js',
output:{
path:path.resolve(__dirname,'dist/'),
filename:'main.bundle.js'
},
plugins:[
new webpack.HotModuleReplacementPlugin()
],
devServer:{
port:4200,
contentBase:path.resolve(__dirname,'dist/'),
historyApiFallback:true,
hot:true
}
}
接下来是 main.js,直接引入了 sayHello。
import { sayHello } from './greeter.ts';
sayHello();
相应的 main.js 依赖 greeter.js。
export function sayHello(){
alert('hello')
}
export function sayWorld(){
alert('world')
}
在 dist 目录下有一个 index.html,用于引入打包后的 bundle。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<script type="text/javascript" src="./main.bundle.js"></script>
</body>
</html>
以上为整个 demo 的代码,接下来直接执行 Webpack 打包试试看。
去掉打包后冗长的代码,只看 chunk 传参的部分:
[ /* 0 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__person__ = __webpack_require__(1); Object(__WEBPACK_IMPORTED_MODULE_0__person__["a /* sayHello */])(); /***/ }), /* 1 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return sayHello; }); /* unused harmony export sayWorld */ function sayHello(){ alert('hello'); } function sayWorld(){ alert('world'); } /***/ }) /******/ ]
我们关注这一行。
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return sayHello; });
实际上只 return 了一个 sayHello。
因此,我们现在只需要压缩整个 JS 代码,就能把未引用的 sayWorld 剔除。
键入以下命令进行压缩。
webpack --optimize-minimize
由于压缩后的代码只有一行了,我们移步尾部:
function(e,n,r){"use strict";function t(){alert("hello")}r.d(n,"a",function(){return t})}]);
可以看到 sayWorld 函数已经被成功剔除。
我们启动 webpack-dev-server。
webpack-dev-server
在浏览器中输入。
http://localhost:4200

每次都需要在命令行里输入参数,岂不是很麻烦,还有没有其他更好的办法呢?
(1)我们可以把这串命令放入 package.json 的 scripts 字段,然后通过 npm start 来自动执行。
(2)其实 --optimize-minimize 的底层实现是一个插件 UglifyJsPlugin,因此,我们可以直接在 webpack.config.js 里配置它。
在 webpack.config.js 里配置插件。
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry:'./src/scripts/main.js',
output:{
filename:'main.bundle.js',
path:path.join(__dirname,'dist')
},
plugins:[
new webpack.optimize.UglifyJsPlugin(), // <----------- 压缩js
new webpack.HotModuleReplacementPlugin()
],
devServer:{
port:4200,
historyApiFallback:true,
hot:true,
contentBase:path.join(__dirname,"dist/")
}
}
然后我们 webpack 打包。
即看到同样的效果。
function(e,n,r){"use strict";function t(){alert("hello")}r.d(n,"a",function(){return t})}]);
在 Tree-Shaking 触发打包后,仅仅是撇开了模块的引用,但还是要结合压缩工具来进行,这才是完整的一次 Tree-Shaking。
那如果是 TypeScript 该怎么使用 Tree-Shaking 呢?
TypeScript 环境下的 Tree-Shaking 解决方案
3.如何在typescript里使用tree-shaking
要在 Webpack 里使用 TS,首先我们必须安装 tsc。
npm install --save-dev typescript
之后我们需要解析 TS 文件的 loader。
npm install --save-dev ts-loader
然后在 webpack.config.js 进行配置。
const webpack = require('webpack')
const path = require('path')
module.exports = {
entry:'./src/scripts/main.ts',
output:{
path:path.resolve(__dirname,'dist/'),
filename:'main.bundle.js'
},
module:{
rules:[
{
test:/\.ts$/,
use:['ts-loader']
}
]
},
plugins:[
new webpack.optimize.UglifyJsPlugin(),
new webpack.HotModuleReplacementPlugin()
],
devServer:{
port:4200,
contentBase:path.resolve(__dirname,'dist/'),
historyApiFallback:true,
hot:true
}
}
献上我的两份文件 main.ts , greeter.ts (这两份文件除了后缀名基本没有改动)。
main.ts
import { sayHello } from './greeter.ts';
sayHello();
greeter.ts
export var sayHello = function(){
alert('hello')
}
export var sayWorld = function(){
alert('world')
}
之后我们需要做的是,创建一个 tsconfig.json 的配置文件供 tsc 解析,这时,坑来了。
下面是我的 tsconfig.json 文件。
{
"compilerOptions":{
"target":"es5",
"sourceMap":true
},
"exclude":[
"./node_modules"
]
}
好像没有什么不对。
接着我们 webpack。
看下打包压缩后的代码的最后一部分:
"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.sayHello=function(){alert("hello")},n.sayWorld=function(){alert("world")}}]);
sayWorld 居然还是存在!!!怎么回事,为什么没有被触发 Tree-Shaking 优化?
这是因为 tsc 编译后的代码为 ES5 ,而正因如此,tsc 默认使用了 CommonJS 的规范来加载模块,因此并没有触发 Tree-Shaking,那我们要怎么做?
修改一下 tsconfig.json,把 target 改为 ES6 即可!
{
"compilerOptions":{
"target":"es6",
"sourceMap":true
},
"exclude":[
"./node_modules"
]
}
再次打包。
看一下打包后的 bundle。
function(e,n,r){"use strict";r.d(n,"a",function(){return t});var t=function({alert("hello")}}]);
果然是触发了 Tree-Shaking。
开启 webpack-dev-server。
webpack-dev-server
可以看到成功打印 hello。

以上就是我对 Webpack Tree-Shaking 的总结,希望对大家的学习有所帮助