Webpack 3 的新功能:Scope Hoisting

1,563 阅读4分钟
原文链接: zhuanlan.zhihu.com

不久前,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 将所有模块都放在了一个函数里,直观感受就是——函数声明少了很多,因此而带来的好处有:

  1. 文件体积比之前更小。
  2. 运行代码时创建的函数作用域也比之前少了,开销也随之变小。

项目中的模块越多,上述的两点提升就会越明显。

它是如何实现的?

这个功能的原理很简单:将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突。

但到目前为止(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 的模块打包进来。

最后,希望这篇文章能对你有所帮助。

参考文章