[TOC]
不采用Scope Hoisting的问题
在不开启Scope Hoisting时,构建后的代码存在大量的闭包代码。
带来的问题:
- 大量函数闭包包裹代码,导致体积增大(模块越多越明显);
- 运行代码时创建的函数作用域变多,内存开销变大。
模块转换分析
- 被webpack转换后的模块会带上一层包裹;
- import会被转换成
__webpack_require__
。
webpack模块机制分析
- 打包出来的是一个IIFE(匿名闭包);
- modules是一个数组,每一项是一个模块初始化函数;
__webpack_require
用来加载模块,返回module.exports;- 通过
__webpack_require__(0)
来启动程序。这里是0是moduleId,一般就是入口文件的id。
Scope Hoisting(作用域提升)原理
原理:将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突。
通过Scope Hoisting可以减少函数声明代码和内存开销。
Scope Hoisting
可以让Webpack打包出来的代码文件更小、运行的更快,它又译作作用域提升,是在Webpack3
中新推出的功能。下面来详细介绍Scope Hoisting
。
认识Scope Hoisting
Scope Hoisting和tree shaking一样都是从rollup借鉴而来的。
让我们先来看看在没有Scope Hoisting
之前Webpack的打包方式。
假如现在有两个文件分别是util.js
:
export default 'Hello, Webpack';
入口文件main.js:
import str from './util.js';
console.log(str);
2
以上源码用Webpack打包后输出中的部分代码如下:
[
(function (module, __webpack_exports__, __webpack_require__) {
var __WEBPACK_IMPORTED_MODULE_0__util_js__ = __webpack_require__(1);
console.log(__WEBPACK_IMPORTED_MODULE_0__util_js__["a"]);
}),
(function (module, __webpack_exports__, __webpack_require__) {
__webpack_exports__["a"] = ('Hello,Webpack');
})
]
2
3
4
5
6
7
8
9
在开启Scope Hoisting
后,同样的源码输出的部分代码如下:
[
(function (module, __webpack_exports__, __webpack_require__) {
var util = ('Hello,Webpack');
console.log(util);
})
]
2
3
4
5
6
可以看出开启Scope Hoisting
后,函数申明由两个变成了一个,util.js中定义的内容被直接注入到了main.js
对应的模块中。这样做的好处是:
- 代码体积更小,因为函数申明语句会产生大量的代码;
- 代码在运行时因为创建的函数作用域变少了,所以内存开销也变小了。
Scope Hoisting
的实现原理其实很简单:分析出模块之间的依赖关系,尽可能将被打散的模块合并到一个函数中,但前提是不能造成代码冗余。因此只有那些被引用了一次的模块才能被合并。
由于Scope Hoisting
需要分析出模块之间的依赖关系,因此源码必须采用ES6模块化语句,不然它将无法生效。
Scope Hoisting
使用Scope Hoisting
是在webpack3中引入的。
webpack3中用法
在Webpack
中使用Scope Hoisting
非常简单,因为这是Webpack
内置的功能,只需要配置插件module-concatenation-plugin,相关代码如下:
const webpack = require('webpack');
module.exports = {
plugins: [
// 开启 Scope Hoisting
new webpack.optimize.ModuleConcatenationPlugin(),
],
};
2
3
4
5
6
7
同时,考虑到Scope Hoisting
依赖源码时需采用ES6
模块化语法,还需要配置 mainFields
。原因在使用TreeShaking
中提到过:因为大部分Npm
中的第三方库采用了CommonJS
语法,但部分库会同时提供ES6
模块化的代码,所以为了充分发挥Scope Hoisting
的作用,需要增加以下配置:
module.exports = {
resolve: {
// 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件
mainFields: ['jsnext:main', 'browser', 'main']
},
};
2
3
4
5
6
对于采用了非ES6
模块化语法的代码,Webpack
会降级处理且不使用Scope Hoisting
优化,为了知道Webpack
对哪些代码做了降级处理,我们可以在启动 Webpack
时带上--display-optimization-bailout
参数,这样在输出日志中就会包含类似如下的日志:
[0] ./main.js + 1 modules 80 bytes {0} [built]
ModuleConcatenation bailout: Module is not an ECMAScript module
2
其中的ModuleConcatenation bailout
告诉了我们哪个文件因为什么原因导致了降级处理。
也就是说,要开启Scope Hoisting
并发挥最大作用的配置如下:
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
module.exports = {
resolve: {
// 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件
mainFields: ['jsnext:main', 'browser', 'main']
},
plugins: [
// 开启 Scope Hoisting
new ModuleConcatenationPlugin(),
],
};
2
3
4
5
6
7
8
9
10
11
12
webpack4.x用法
webpack4.x版本在生产环境下默认开启Scope Hoisting。必须是ES6语法,不支持commonjs语法。
// 源代码如下
import { helloworld } from './helloworld';
import {common} from '../../common/';
document.write(helloworld());
2
3
4
5
// 不开启Scope Hoisting,打包后的代码如下
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _helloworld__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
/* harmony import */ var _common___WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2);
document.write(Object(_helloworld__WEBPACK_IMPORTED_MODULE_0__["helloworld"])());
/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "helloworld", function() { return helloworld; });
function helloworld() {
return 'Hello webpack';
}
/***/ })
/******/ ]);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
手动开启Scope Hoisting:
new webpack.optimize.ModuleConcatenationPlugin()
// 打包结果如下:
/******/ ({
/***/ 15:
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
// CONCATENATED MODULE: ./src/index/helloworld.js
function helloworld() {
return 'Hello webpack';
}
// EXTERNAL MODULE: ./common/index.js
var common = __webpack_require__(0);
// CONCATENATED MODULE: ./src/index/index.js
document.write(helloworld());
/***/ })
/******/ });
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23