webpack模块化原理解析(四)—— webpack重要特性的原理解析

在这篇文章,你会得到关于 webpack历代版本重要特性的剖析,以及关于这些特性的最佳实践,包括以下内容

  • Scope Hoisting
  • treeShaking
  • babel 在 webpack 模块化起到的作用

Scope Hoisting

通过前三篇文章,我们可以发现模块数组是由一个一个闭包函数组成,闭包函数形成独立的作用域,分别被__webpack_require__调用,保证模块化而不会互相污染作用域。

但这样会降低浏览器中JS执行效率,这主要是闭包函数降低了JS引擎解析速度。于是webpack团队参考Closure Compiler 和 RollupJS,将所有闭包放至一个闭包中,通过减少闭包函数数量从而加快JS的执行速度,且代码体积有所减少。

通过设置 ModuleConcatenationPlugin 使用这个特性:

1
2
3
plugins: [
new webpack.optimize.ModuleConcatenationPlugin(),
]

再举一个例子:

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// index.js
import es, { esTwo, esFour, esThree} from './es.js';

console.log('hello world');
esThree();
console.log(es ,esTwo, esFour)

// es.js
import varA from "./varA";
console.log(varA)
export default 'ES Modules';

let esTwo = 'ES Modules Two';
export {esTwo};

export function esThree() {
console.log('ES Modules Three');
}

export const esFour = 'ES Modules Four';

// varA.js
export default 'a';

打包结果(节选模块数组部分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });

// CONCATENATED MODULE: ./varA.js
/* harmony default export */ var varA = ('a');
// CONCATENATED MODULE: ./es.js
// src/es.js

console.log(varA)
/* harmony default export */ var es = ('ES Modules');

let esTwo = 'ES Modules Two';

function esThree() {
console.log('ES Modules Three');
}

const esFour = 'ES Modules Four';

console.log('hello world');
esThree();
console.log(es ,esTwo, esFour)

/***/ })
/******/ ]

说明:

  • Scope Hoisting 只能在 ES Modules 起作用,因为Commonjs 是运行时加载,不到运行是没法知道依赖关系的,所以webpack 没法分析依赖关系,自然就不能实现功能。
  • 而且在大部分情况下,Scope Hoisting是没法起到作用的,因为我们所依赖的第三方依赖npm包都是以 commonjs 规范输出,所以大部分的 webpack 打包代码依旧保持原样。

Tree Shaking

Tree shaking is a term commonly used in the JavaScript context for dead-code elimination. It relies on the static structureof ES2015 module syntax, i.e. import and export. The name and concept have been popularized by the ES2015 module bundler rollup.

上文是摘抄自webpack 文档,大致意思是Tree Shaking 移除 JavaScript 上下文中的未引用代码(dead-code),该特性依赖于 ES Modules,是源于打包工具 rollup。

Tree-shaking versus dead code elimination

这是 rollup 作者解释Tree Shaking的来由,其中我觉得比较关键的点在于:Tree Shaking 有别于 DCE (dead code elimination),前者消灭没有用到的代码,而后者是把不执行的代码从 AST 树消灭。因为ES Modules,Rollup 得以静态化分析,去除顶层 AST 没用到的代码。不过由于 Javascript 是动态语言,静态化分析难以消灭不会执行的代码,比如对象中不使用的方法等。

例子

项目结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//index.js
import { es, esTwo, esFour, fun} from './es.js';

console.log('hello world');
es();
esTwo();
fun.esThree()

//es.js
export function es() {
console.log('ES Modules');
}
export function esTwo() {
console.log('ES Modules Two');
}
export function esThree() {
console.log('ES Modules Three');
}
export function esFour() {
console.log('ES Modules Four');
}
export const fun = {
esThree,
esFour
}

// webpack.config.js
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
entry:'./index.js',
output:{
filename:'bundle.js'
},
plugins: [
new UglifyJsPlugin({
exclude: /\/excludes/
})
]
};

打包结果(格式化效果):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
!function (e) {
var o = {};

function n(t) {
if (o[t]) return o[t].exports;
var r = o[t] = {i: t, l: !1, exports: {}};
return e[t].call(r.exports, r, r.exports, n), r.l = !0, r.exports
}

n.m = e, n.c = o, n.d = function (e, o, t) {
n.o(e, o) || Object.defineProperty(e, o, {configurable: !1, enumerable: !0, get: t})
}, n.n = function (e) {
var o = e && e.__esModule ? function () {
return e.default
} : function () {
return e
};
return n.d(o, "a", o), o
}, n.o = function (e, o) {
return Object.prototype.hasOwnProperty.call(e, o)
}, n.p = "", n(n.s = 0)
}([function (e, o, n) {
"use strict";
Object.defineProperty(o, "__esModule", {value: !0});
var t = n(1);
console.log("hello world"), Object(t.a)(), Object(t.b)(), t.c.esThree()
}, function (e, o, n) {
"use strict";
o.a = function () {
console.log("ES Modules")
}, o.b = function () {
console.log("ES Modules Two")
};
const t = {
esThree: function () {
console.log("ES Modules Three")
}, esFour: function () {
console.log("ES Modules Four")
}
};
o.c = t
}]);

从打包代码可以看出,Tree Shaking 能消灭没用到的 esThreeesFour函数,但是美中不足的是, Tree shaking 难以消灭不会执行的代码,表现在fun对象的esFour并没有消灭。

在这里说句题外话,Tree Shaking 看起来很美好,真正能起的作用很有限。首先自己项目的代码,代码量少,能被消灭的可能只有几行。所以针对的主要还是第三方依赖,而像lodash, vue这类成熟的第三方库也都提供了各自的按需引入方式,压根用不上 Tree Shaking。此外也是像上述所说的,Tree Shaking 没法消灭不会执行的代码。

特性总结

  • webpack的 Tree Shaking 功能是由 wepack 的插件uglifyjs-webpack-plugin实现。
  • Tree Shaking 依赖于 ES Modules,对于动态的Commonjs 规范则无法生效。
  • Tree Shaking 无法消灭不会执行的代码,只能消灭没用到的代码。
  • 实际项目中,Tree Shaking 消除代码作用十分有限

##babel 在 webpack 模块化起到的作用

时至今日,babel 在前端开发领域的作用毋庸置疑, babel专门用于处理 ES20XX 转换 ES5,当然也包括ES Modules语法的转换,显然这样的转换对 webpack 模块化处理产生了一定的影响。

例子

项目结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// index.js
import esThree, { es, esTwo, esFour} from './es.js';

console.log('hello world');
console.info(es, esThree)
esFour();

// es.js
export let es = 'es';
export const esTwo = 'esTwo';
export default 'default';
export function esFour() {
console.log('ES Modules');
}

//webpack.config.js
module.exports = {
entry:'./index.js',
output:{
filename:'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
"presets": [
["env"]
]
}
}
}
]
}
};

打包结果(截取模块数组部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
[
/* 0 */
/***/ (function(module, exports, __webpack_require__) {

"use strict";


var _es = __webpack_require__(1);

var _es2 = _interopRequireDefault(_es);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

console.log('hello world');

console.info(_es.es, _es2.default);

(0, _es.esFour)();

/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {

"use strict";


Object.defineProperty(exports, "__esModule", {
value: true
});
exports.esFour = esFour;
var es = exports.es = 'es';
var esTwo = exports.esTwo = 'esTwo';
exports.default = 'default';
function esFour() {
console.log('ES Modules');
}

/***/ })
/******/ ]

es模块导出的内容在babel 处理后,转换为commonjs规范的模块

1
2
3
4
5
6
7
8
9
10
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.esFour = esFour;
var es = exports.es = 'es';
var esTwo = exports.esTwo = 'esTwo';
exports.default = 'default';
function esFour() {
console.log('ES Modules');
}

babel 转换 ES Modules非常简单,即把所有的依赖赋值给 exports,并给 __esModule设为true 表明这是个由 ES Modules 转换来的 commonjs 模块。所以,babel 能在 webpack 打包前,提前将 ES Modules 转换成 commonjs 的规范。

这种提前把 ES Modules 转换为 Commonjs的做法,无形中放弃了 ES Modules 的静态化分析特性, 上面提到的Scope HoistingTree Shaking就无法一展身手了,但是 babel 提供了一个关于ES Modules 编译的属性modules,可选的选项为:"amd" | "umd" | "systemjs" | "commonjs" | false,如果不对 ESModules进行编译的话,设为false。故在这里, babel 的推荐配置为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//webpack.config.js
module.exports = {
entry:'./index.js',
output:{
filename:'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
"presets": [
["env", {"modules": false}]
]
}
}
}
]
}
};

优化后的打包代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
[
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__es_js__ = __webpack_require__(1);


console.log('hello world');

console.info(__WEBPACK_IMPORTED_MODULE_0__es_js__["b" /* es */], __WEBPACK_IMPORTED_MODULE_0__es_js__["a" /* default */]);

Object(__WEBPACK_IMPORTED_MODULE_0__es_js__["c" /* esFour */])();

/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "b", function() { return es; });
/* unused harmony export esTwo */
/* harmony export (immutable) */ __webpack_exports__["c"] = esFour;
var es = 'es';
var esTwo = 'esTwo';
/* harmony default export */ __webpack_exports__["a"] = ('default');
function esFour() {
console.log('ES Modules');
}

/***/ })
/******/ ]

看出区别了吗,这里 ES Modules 没有被 Babel 编译,而像const这些就被 Babel 编译,完美恢复了 webpack 对ES Modules的处理。