標籤:

Webpack 3 的新功能:Scope Hoisting

不久前,Webpack 正式發布了它的第三個版本,這個版本提供了一個新的功能:Scope Hoisting,又譯作「作用域提升」。只需在配置文件中添加一個新的插件,就可以讓 Webpack 打包出來的代碼文件更小、運行的更快:

module.exports = {n plugins: [n new webpack.optimize.ModuleConcatenationPlugin()n ]n}n

這篇文章將會從多個方面詳細介紹這項新功能,在這之前,我們先來看看 Webpack 是如何將多個模塊打包在一起的。

Webpack 默認的模塊打包方式

現在假設我們的項目有這樣兩個文件:

// module-a.jsnexport default module An// entry.jsnimport a from ./module-anconsole.log(a)n

現在我們用 Webpack 打包一下,得到的文件大致像這樣:

// bundle.jsn// 最前面的一段代碼實現了模塊的載入、執行和緩存的邏輯,這裡直接略過n[n /* 0 */n function (module, exports, require) {n var module_a = require(1)n console.log(module_a[default])n },n /* 1 */n function (module, exports, require) {n exports[default] = module An }n]n

更深入的分析可以看這篇文章:從 Bundle 文件看 Webpack 模塊機制。

簡單來說,Webpack 將所有模塊都用函數包裹起來,然後自己實現了一套模塊載入、執行與緩存的功能,使用這樣的結構是為了更容易實現 Code Splitting(包括按需載入)、模塊熱替換等功能。

但如果你在 Webpack 3 中添加了 ModuleConcatenationPlugin 插件,這個結構會發生一些變化。

作用域提升後的 bundle.js

同樣的源文件在使用了 ModuleConcatenationPlugin 之後,打包出來的文件會變成下面這樣:

// bundle.jsn[n function (module, exports, require) {n // CONCATENATED MODULE: ./module-a.jsn var module_a_defaultExport = module Ann // CONCATENATED MODULE: ./index.jsn console.log(module_a_defaultExport)n }n]n

顯而易見,這次 Webpack 將所有模塊都放在了一個函數里,直觀感受就是——函數聲明少了很多,因此而帶來的好處有:

  1. 文件體積比之前更小。
  2. 運行代碼時創建的函數作用域也比之前少了,開銷也隨之變小。

項目中的模塊越多,上述的兩點提升就會越明顯。

它是如何實現的?

這個功能的原理很簡單:將所有模塊的代碼按照引用順序放在一個函數作用域里,然後適當的重命名一些變數以防止變數名衝突。

但到目前為止(Webpack 3.3.0),為了在 Webpack 中使用這個功能,你的代碼必須是用 ES2015 的模塊語法寫的。

暫不支持 CommonJS 模塊語法的原因是,這種模塊語法中的模塊是可以動態載入的,例如下面這段代碼:

var directory = ./modules/nif (Math.random() > 0.5) {n module.exports = require(directory + foo.js)n} else {n module.exports = require(directory + bar.js)n}n

這種情況很難分析出模塊之間的依賴關係及輸出的變數。

而 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-bailoutn

另外,當你使用這個插件的時候,模塊熱替換將不起作用,所以最好只在代碼優化的時候才使用這個插件。

最後,給 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?

推薦閱讀:

【譯】webpack 小札: 充分利用 CommonsChunkPlugin()
webpack:從入門到真實項目配置
簡單幾步助你優化React應用包體
讀懂webpack生成的26行代碼
webpack 打包JS 的運行原理

TAG:webpack |