打包中的Scope Hoisting功能
不久前,Webpack 正式发布了它的第三个版本,这个版本提供了一个新的功能:Scope Hoisting,又译作“作用域提升”。只需在配置文件中添加一个新的插件,就可以让 Webpack 打包出来的代码文件更小、运行的更快:
module.exports = {
plugins: [
new webpack.optimize.ModuleConcatenationPlugin()
]
}
这篇文章将会从多个方面详细介绍这项新功能,在这之前,我们先来看看 Webpack 是如何将多个模块打包在一起的。
Webpack 默认的模块打包方式
现在假设我们的项目有这样两个文件:
// module-a.js
export default 'module A'
// entry.js
import a from './module-a'
console.log(a)
现在我们用 Webpack 打包一下,得到的文件大致像这样:
// bundle.js
// 最前面的一段代码实现了模块的加载、执行和缓存的逻辑,这里直接略过
[
/*0*/
function (module, exports, require) {
var module_a = require(1)
console.log(module_a['default'])
},
/*1*/
function (module, exports, require) {
exports['default'] = 'module A'
}
]
更深入的分析可以看这篇文章:从 Bundle 文件看 Webpack 模块机制。
简单来说,Webpack 将所有模块都用函数包裹起来,然后自己实现了一套模块加载、执行与缓存的功能,使用这样的结构是为了更容易实现 Code Splitting(包括按需加载)、模块热替换等功能。
但如果你在 Webpack 3 中添加了 ModuleConcatenationPlugin 插件,这个结构会发生一些变化。
作用域提升后的 bundle.js
同样的源文件在使用了 ModuleConcatenationPlugin 之后,打包出来的文件会变成下面这样:
// bundle.js
[
function (module, exports, require) {
// CONCATENATED MODULE: ./module-a.js
var module_a_defaultExport = 'module A'
// CONCATENATED MODULE: ./index.js
console.log(module_a_defaultExport)
}
]
显而易见,这次 Webpack 将所有模块都放在了一个函数里,直观感受就是——函数声明少了很多,因此而带来的好处有:
- 文件体积比之前更小。
- 运行代码时创建的函数作用域也比之前少了,开销也随之变小。
项目中的模块越多,上述的两点提升就会越明显。
它是如何实现的?
这个功能的原理很简单:将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突。
但到目前为止(Webpack 3.3.0),为了在 Webpack 中使用这个功能,你的代码必须是用 ES2015 的模块语法写的。
暂不支持 CommonJS 模块语法的原因是,这种模块语法中的模块是可以动态加载的,例如下面这段代码:
var directory = './modules/'
if (Math.random() > 0.5) {
module.exports = require(directory + 'foo.js')
} else {
module.exports = require(directory + 'bar.js')
}
这种情况很难分析出模块之间的依赖关系及输出的变量。
而 ES2015 的模块语法规定 import 和 export 关键字必须在顶层、模块路径只能用字符串字面量,这种“强制静态化”的做法使代码在编译时就能确定模块的依赖关系,以及输入和输出的变量,所以这种功能实现起来会更加简便。
不过,未来 Webpack 可能也会支持 CommonJS 的模块语法。
等等,为什么在我的项目中不起作用?
一些同学可能已经在自己的项目中加上了 ModuleConcatenationPlugin,但却发现打包出来的代码完全没有发生变化。
前面说过,要使用 Scope Hoisting,你的代码必须是用 ES2015 的模块语法写的,但是大部分 NPM 中的模块仍然是 CommonJS 语法(例如 lodash),所以导致 Webpack 回退到了默认的打包方式。
其他可能的原因还有:
- 使用了 ProvidePlugin
- 使用了 eval() 函数
- 你的项目有多个 entry
运行 Webpack 时加上 -display-optimization-bailout 参数可以得知为什么你的项目无法使用 Scope Hoisting:
webpack --display-optimization-bailout
另外,当你使用这个插件的时候,模块热替换将不起作用,所以最好只在代码优化的时候才使用这个插件。
最后,给 Rollup 打个广告
Tree Shaking 与 Scope Hoisting 最初都是由 Rollup 实现的。尽管 Webpack 现在也实现了这两个功能,但是 Rollup 比 Webpack 更适合打包 JavaScript 框架(库),因为:
- Rollup 的配置比 Webpack 简单得多。
- Rollup 不用支持 Code Spliting,所以打包出来的代码开头没有 Webpack 那段模块的加载、执行和缓存的代码。
- Rollup 本身就支持 Scope Hoisting,在使用一些插件之后也能把 CommonJS 的模块打包进来。
最后,希望这篇文章能对你有所帮助。
参考文章
- webpack 3: Official Release!!
- webpack freelancing log book (week 5–7)
- Webpack and Rollup: the same but different
- What is flat bundling and why is Rollup better at this than Webpack?