標籤:

深入理解 webpack 文件打包機制

原文鏈接:深入理解 webpack 文件打包機制

前言

最近在重拾 webpack 一些知識點,希望對前端模塊化有更多的理解,以前對 webpack 打包機制有所好奇,沒有理解深入,淺嘗則止,最近通過對 webpack 打包後的文件進行查閱,對其如何打包 JS 文件有了更深的理解,希望通過這篇文章,能夠幫助讀者你理解:

  1. webpack 單文件如何進行打包?
  2. webpack 多文件如何進行代碼切割?
  3. webpack1 和 webpack2 在文件打包上有什麼區別?
  4. webpack2 如何做到 tree shaking?
  5. webpack3 如何做到 scope hoisting?

本文所有示例代碼全部放在我的 Github 上,看興趣的可以看看:

git clone https://github.com/happylindz/blog.gitncd blog/code/webpackBundleAnalysisnnpm installn

webpack 單文件如何打包?

首先現在 webpack 作為當前主流的前端模塊化工具,在 webpack 剛開始流行的時候,我們經常通過 webpack 將所有處理文件全部打包成一個 bundle 文件, 先通過一個簡單的例子來看:

// src/single/index.jsnvar index2 = require(./index2);nvar util = require(./util);nconsole.log(index2);nconsole.log(util);nn// src/single/index2.jsnvar util = require(./util);nconsole.log(util);nmodule.exports = "index 2";nn// src/single/util.jsnmodule.exports = "Hello World";nn// 通過 config/webpack.config.single.js 打包nconst webpack = require(webpack);nconst path = require(path)nnmodule.exports = {n entry: {n index: [path.resolve(__dirname, ../src/single/index.js)],n },n output: {n path: path.resolve(__dirname, ../dist),n filename: [name].[chunkhash:8].jsn },n}n

通過 npm run build:single 可看到打包效果,打包內容大致如下(經過精簡):

// dist/index.xxxx.jsn(function(modules) {n // 已經載入過的模塊n var installedModules = {};nn // 模塊載入函數n function __webpack_require__(moduleId) {n if(installedModules[moduleId]) {n return installedModules[moduleId].exports;n }n var module = installedModules[moduleId] = {n i: moduleId,n l: false,n exports: {}n };n modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);n module.l = true;n return module.exports;n }n return __webpack_require__(__webpack_require__.s = 3);n})([n/* 0 */n(function(module, exports, __webpack_require__) {n var util = __webpack_require__(1);n console.log(util);n module.exports = "index 2";n}),n/* 1 */n(function(module, exports) {n module.exports = "Hello World";n}),n/* 2 */n(function(module, exports, __webpack_require__) {n var index2 = __webpack_require__(0);n index2 = __webpack_require__(0);n var util = __webpack_require__(1);n console.log(index2);n console.log(util);n}),n/* 3 */n(function(module, exports, __webpack_require__) {n module.exports = __webpack_require__(2);n})]);n

將相對無關的代碼剔除掉後,剩下主要的代碼:

  1. 首先 webpack 將所有模塊(可以簡單理解成文件)包裹於一個函數中,並傳入默認參數,這裡有三個文件再加上一個入口模塊一共四個模塊,將它們放入一個數組中,取名為 modules,並通過數組的下標來作為 moduleId。
  2. 將 modules 傳入一個自執行函數中,自執行函數中包含一個 installedModules 已經載入過的模塊和一個模塊載入函數,最後載入入口模塊並返回。
  3. __webpack_require__ 模塊載入,先判斷 installedModules 是否已載入,載入過了就直接返回 exports 數據,沒有載入過該模塊就通過 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__) 執行模塊並且將 module.exports 給返回。

很簡單是不是,有些點需要注意的是:

  1. 每個模塊 webpack 只會載入一次,所以重複載入的模塊只會執行一次,載入過的模塊會放到 installedModules,下次需要需要該模塊的值就直接從裡面拿了。
  2. 模塊的 id 直接通過數組下標去一一對應的,這樣能保證簡單且唯一,通過其它方式比如文件名或文件路徑的方式就比較麻煩,因為文件名可能出現重名,不唯一,文件路徑則會增大文件體積,並且將路徑暴露給前端,不夠安全。
  3. modules[moduleId].call(module.exports, module, module.exports, __webpack_require__) 保證了模塊載入時 this 的指向 module.exports 並且傳入默認參數,很簡單,不過多解釋。

webpack 多文件如何進行代碼切割?

webpack 單文件打包的方式應付一些簡單場景就足夠了,但是我們在開發一些複雜的應用,如果沒有對代碼進行切割,將第三方庫(jQuery)或框架(React)和業務代碼全部打包在一起,就會導致用戶訪問頁面速度很慢,不能有效利用緩存,你的老闆可能就要找你談話了。

那麼 webpack 多文件入口如何進行代碼切割,讓我先寫一個簡單的例子:

// src/multiple/pageA.jsnconst utilA = require(./js/utilA);nconst utilB = require(./js/utilB);nconsole.log(utilA);nconsole.log(utilB);nn// src/multiple/pageB.jsnconst utilB = require(./js/utilB);nconsole.log(utilB);n// 非同步載入文件,類似於 import()nconst utilC = () => require.ensure([./js/utilC], function(require) {n console.log(require(./js/utilC))n});nutilC();nn// src/multiple/js/utilA.js 可類比於公共庫,如 jQuerynmodule.exports = "util A";nn// src/multiple/js/utilB.jsnmodule.exports = util B;nn// src/multiple/js/utilC.jsnmodule.exports = "util C";n

這裡我們定義了兩個入口 pageA 和 pageB 和三個庫 util,我們希望代碼切割做到:

  1. 因為兩入口都是用到了 utilB,我們希望把它抽離成單獨文件,並且當用戶訪問 pageA 和 pageB 的時候都能去載入 utilB 這個公共模塊,而不是存在於各自的入口文件中。
  2. pageB 中 utilC 不是頁面一開始載入時候就需要的內容,假如 utilC 很大,我們不希望頁面載入時就直接載入 utilC,而是當用戶達到某種條件(如:點擊按鈕)才去非同步載入 utilC,這時候我們需要將 utilC 抽離成單獨文件,當用戶需要的時候再去載入該文件。

那麼 webpack 需要怎麼配置呢?

// 通過 config/webpack.config.multiple.js 打包nconst webpack = require(webpack);nconst path = require(path)nnmodule.exports = {n entry: {n pageA: [path.resolve(__dirname, ../src/multiple/pageA.js)],n pageB: path.resolve(__dirname, ../src/multiple/pageB.js),n },n output: {n path: path.resolve(__dirname, ../dist),n filename: [name].[chunkhash:8].js,n },n plugins: [n new webpack.optimize.CommonsChunkPlugin({n name: vendor,n minChunks: 2,n }),n new webpack.optimize.CommonsChunkPlugin({n name: manifest,n chunks: [vendor]n })n ]n}n

單單配置多 entry 是不夠的,這樣只會生成兩個 bundle 文件,將 pageA 和 pageB 所需要的內容全部放入,跟單入口文件並沒有區別,要做到代碼切割,我們需要藉助 webpack 內置的插件 CommonsChunkPlugin。

首先 webpack 執行存在一部分運行時代碼,即一部分初始化的工作,就像之前單文件中的__webpack_require__,這部分代碼需要載入於所有文件之前,相當於初始化工作,少了這部分初始化代碼,後面載入過來的代碼就無法識別並工作了。

new webpack.optimize.CommonsChunkPlugin({n name: vendor,n minChunks: 2,n})n

這段代碼的含義是,在這些入口文件中,找到那些引用兩次的模塊(如:utilB),幫我抽離成一個叫 vendor 文件,此時那部分初始化工作的代碼會被抽離到 vendor 文件中。

new webpack.optimize.CommonsChunkPlugin({n name: manifest,n chunks: [vendor],n // minChunks: Infinity // 可寫可不寫n})n

這段代碼的含義是在 vendor 文件中幫我把初始化代碼抽離到 mainifest 文件中,此時 vendor 文件中就只剩下 utilB 這個模塊了。你可能會好奇為什麼要這麼做?

因為這樣可以給 vendor 生成穩定的 hash 值,每次修改業務代碼(pageA),這段初始化時代碼就會發生變化,那麼如果將這段初始化代碼放在 vendor 文件中的話,每次都會生成新的 vendor.xxxx.js,這樣不利於持久化緩存,如果不理解也沒關係,下次我會另外寫一篇文章來講述這部分內容。

另外 webpack 默認會抽離非同步載入的代碼,這個不需要你做額外的配置,pageB 中非同步載入的 utilC 文件會直接抽離為 chunk.xxxx.js 文件。

所以這時候我們頁面載入文件的順序就會變成:

mainifest.xxxx.js // 初始化代碼nvendor.xxxx.js // pageA 和 pageB 共同用到的模塊,抽離npageX.xxxx.js // 業務代碼 n當 pageB 需要 utilC 時候則非同步載入 utilCn

執行 npm run build:multiple 即可查看打包內容,首先來看下 manifest 如何做初始化工作(精簡版)?

// dist/mainifest.xxxx.jsn(function(modules) { n window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) {n var moduleId, chunkId, i = 0, callbacks = [];n for(;i < chunkIds.length; i++) {n chunkId = chunkIds[i];n if(installedChunks[chunkId])n callbacks.push.apply(callbacks, installedChunks[chunkId]);n installedChunks[chunkId] = 0;n }n for(moduleId in moreModules) {n if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {n modules[moduleId] = moreModules[moduleId];n }n }n while(callbacks.length)n callbacks.shift().call(null, __webpack_require__);n if(moreModules[0]) {n installedModules[0] = 0;n return __webpack_require__(0);n }n };n var installedModules = {};n var installedChunks = {n 4:0n };n function __webpack_require__(moduleId) {n // 和單文件一致n }n __webpack_require__.e = function requireEnsure(chunkId, callback) {n if(installedChunks[chunkId] === 0)n return callback.call(null, __webpack_require__);n if(installedChunks[chunkId] !== undefined) {n installedChunks[chunkId].push(callback);n } else {n installedChunks[chunkId] = [callback];n var head = document.getElementsByTagName(head)[0];n var script = document.createElement(script);n script.type = text/javascript;n script.charset = utf-8;n script.async = true;n script.src = __webpack_require__.p + "" + chunkId + "." + ({"0":"pageA","1":"pageB","3":"vendor"}[chunkId]||chunkId) + "." + {"0":"e72ce7d4","1":"69f6bbe3","2":"9adbbaa0","3":"53fa02a7"}[chunkId] + ".js";n head.appendChild(script);n }n };n})([]);n

與單文件內容一致,定義了一個自執行函數,因為它不包含任何模塊,所以傳入一個空數組。除了定義了 __webpack_require__,還另外定義了兩個函數用來進行載入模塊。

首先講解代碼前需要理解兩個概念,分別是 module 和 chunk

  1. chunk 代表生成後 js 文件,一個 chunkId 對應一個打包好的 js 文件(一共五個),從這段代碼可以看出,manifest 的 chunkId 為 4,並且從代碼中還可以看到:0-3 分別對應 pageA, pageB, 非同步 utilC, vendor 公共模塊文件,這也就是我們為什麼不能將這段代碼放在 vendor 的原因,因為文件的 hash 值會變。內容變了,vendor 生成的 hash 值也就變了。
  2. module 對應著模塊,可以簡單理解為打包前每個 js 文件對應一個模塊,也就是之前 __webpack_require__ 載入的模塊,同樣的使用數組下標作為 moduleId 且是唯一不重複的。

那麼為什麼要區分 chunk 和 module 呢?

首先使用 installedChunks 來保存每個 chunkId 是否被載入過,如果被載入過,則說明該 chunk 中所包含的模塊已經被放到了 modules 中,注意是 modules 而不是 installedModules。我們先來簡單看一下 vendor chunk 打包出來的內容。

// vendor.xxxx.jsnwebpackJsonp([3,4],{n 3: (function(module, exports) {n module.exports = util B;n })n});n

在執行完 manifest 後就會先執行 vendor 文件,結合上面 webpackJsonp 的定義,我們可以知道 [3, 4] 代表 chunkId,當載入到 vendor 文件後,installedChunks[3] 和 installedChunks[4] 將會被置為 0,這表明 chunk3,chunk4 已經被載入過了。

webpackJsonpCallback 一共有兩個參數,chuckIds 一般包含該 chunk 文件依賴的 chunkId 以及自身 chunkId,moreModules 代表該 chunk 文件帶來新的模塊。

var moduleId, chunkId, i = 0, callbacks = [];nfor(;i < chunkIds.length; i++) {n chunkId = chunkIds[i];n if(installedChunks[chunkId])n callbacks.push.apply(callbacks, installedChunks[chunkId]);n installedChunks[chunkId] = 0;n}nfor(moduleId in moreModules) {n if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {n modules[moduleId] = moreModules[moduleId];n }n}nwhile(callbacks.length)n callbacks.shift().call(null, __webpack_require__);nif(moreModules[0]) {n installedModules[0] = 0;n return __webpack_require__(0);n}n

簡單說說 webpackJsonpCallback 做了哪些事,首先判斷 chunkIds 在 installedChunks 里有沒有回調函數函數未執行完,有的話則放到 callbacks 里,並且等下統一執行,並將 chunkIds 在 installedChunks 中全部置為 0, 然後將 moreModules 合併到 modules。

這裡面只有 modules[0] 是不固定的,其它 modules 下標都是唯一的,在打包的時候 webpack 已經為它們統一編號,而 0 則為入口文件即 pageA,pageB 各有一個 module[0]。

然後將 callbacks 執行並清空,保證了該模塊載入開始前所以前置依賴內容已經載入完畢,最後判斷 moreModules[0], 有值說明該文件為入口文件,則開始執行入口模塊 0。

上面解釋了一大堆,但是像 pageA 這種同步載入 manifest, vendor 以及 pageA 文件來說,每次載入的時候 callbacks 都是為空的,因為它們在 installedChunks 中的值要嘛為 undefined(未載入), 要嘛為 0(已被載入)。installedChunks[chunkId] 的值永遠為 false,所以在這種情況下 callbacks 里根本不會出現函數,如果僅僅是考慮這樣的場景,上面的 webpackJsonpCallback 完全可以寫成下面這樣:

var moduleId, chunkId, i = 0, callbacks = [];nfor(;i < chunkIds.length; i++) {n chunkId = chunkIds[i];n installedChunks[chunkId] = 0;n}nfor(moduleId in moreModules) {n if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {n modules[moduleId] = moreModules[moduleId];n }n}nif(moreModules[0]) {n installedModules[0] = 0;n return __webpack_require__(0);n}n

但是考慮到非同步載入 js 文件的時候(比如 pageB 非同步載入 utilC 文件),就沒那麼簡單,我們先來看下 webpack 是如何載入非同步腳本的:

// 非同步載入函數掛載在 __webpack_require__.e 上n__webpack_require__.e = function requireEnsure(chunkId, callback) {n if(installedChunks[chunkId] === 0)n return callback.call(null, __webpack_require__);n n if(installedChunks[chunkId] !== undefined) {n installedChunks[chunkId].push(callback);n } else {n installedChunks[chunkId] = [callback];n var head = document.getElementsByTagName(head)[0];n var script = document.createElement(script);n script.type = text/javascript;n script.charset = utf-8;n script.async = true;nn script.src = __webpack_require__.p + "" + chunkId + "." + ({"0":"pageA","1":"pageB","3":"vendor"}[chunkId]||chunkId) + "." + {"0":"e72ce7d4","1":"69f6bbe3","2":"9adbbaa0","3":"53fa02a7"}[chunkId] + ".js";n head.appendChild(script);n }n};n

大致分為三種情況,(已經載入過,正在載入中以及從未載入過)

  1. 已經載入過該 chunk 文件,那就不用再重新載入該 chunk 了,直接執行回調函數即可,可以理解為假如頁面有兩種操作需要載入載入非同步腳本,但是兩個腳本都依賴於公共模塊,那麼第二次載入的時候發現之前第一次操作已經載入過了該 chunk,則不用再去獲取非同步腳本了,因為該公共模塊已經被執行過了。
  2. 從未載入過,則動態地去插入 script 腳本去請求 js 文件,這也就為什麼取名 webpackJsonpCallback,因為跟 jsonp 的思想很類似,所以這種非同步載入腳本在做腳本錯誤監控時經常出現 Script error,具體原因可以查看我之前寫的文章:前端代碼異常監控實戰
  3. 正在載入中代表該 chunk 文件已經在載入中了,比如說點擊按鈕觸發非同步腳本,用戶點太快了,連點兩次就可能出現這種情況,此時將回調函數放入 installedChunks。

我們通過 utilC 生成的 chunk 來進行講解:

webpackJsonp([2,4],{n 4: (function(module, exports) {n module.exports = "util C";n })n});n

pageB 需要非同步載入這個 chunk:

webpackJsonp([1,4],[n/* 0 */n (function(module, exports, __webpack_require__) {n const utilB = __webpack_require__(3);n console.log(utilB);n const utilC = () => __webpack_require__.e/* nsure */(2, function(require) {n console.log(__webpack_require__(4))n });n utilC();n })n]);n

當 pageB 進行某種操作需要載入 utilC 時就會執行 __webpack_require__.e(2, callback) 2,代表需要載入的模塊 chunkId(utilC),非同步載入 utilC 並將 callback 添加到 installedChunks[2] 中,然後當 utilC 的 chunk 文件載入完畢後,chunkIds 包含 2,發現 installedChunks[2] 是個數組,裡面還有之前還未執行的 callback 函數。

既然這樣,那我就將我自己帶來的模塊先放到 modules 中,然後再統一執行之前未執行完的 callbacks 函數,這裡指的是存放於 installedChunks[2] 中的回調函數 (可能存在多個),這也就是說明這裡的先後順序:

// 先將 moreModules 合併到 modules, 再去執行 callbacks, 不然之前未執行的 callback 依賴於新來的模塊,你不放進 module 我豈不是得不到想要的模塊nfor(moduleId in moreModules) {n if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {n modules[moduleId] = moreModules[moduleId];n }n}nwhile(callbacks.length)n callbacks.shift().call(null, __webpack_require__);n

webpack1 和 webpack2 在文件打包上有什麼區別?

經過我對打包文件的觀察,從 webpack1 到 webpack2 在打包文件上有下面這些主要的改變:

首先,moduleId[0] 不再為入口執行函數做保留,所以說不用傻傻看到 moduleId[0] 就認為是打包文件的入口模塊,取而代之的是 window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {} 傳入了第三個參數 executeModules,是個數組,如果參數存在則說明它是入口模塊,然後就去執行該模塊。

if(executeModules) {n for(i=0; i < executeModules.length; i++) {n result = __webpack_require__(__webpack_require__.s = executeModules[i]);n }n}n

其次,webpack2 中會默認載入 OccurrenceOrderPlugin 這個插件,即你不用 plugins 中添加這個配置它也會默認執行,那它有什麼用途呢?主要是在 webpack1 中 moduleId 的不確定性導致的,在 webpack1 中 moduleId 取決於引入文件的順序,這就會導致這個 moduleId 可能會時常發生變化, 而 OccurrenceOrderPlugin 插件會按引入次數最多的模塊進行排序,引入次數的模塊的 moduleId 越小,比如說上面引用的 utilB 模塊引用次數為 2(最多),所以它的 moduleId 為 0。

webpackJsonp([3],[n/* 0 */n (function(module, exports) {n module.exports = util B;n })n]);n

最後說下在非同步載入模塊時, webpack2 是基於 Promise 的,所以說如果你要兼容低版本瀏覽器,需要引入 Promise-polyfill,另外為引入請求添加了錯誤處理。

__webpack_require__.e = function requireEnsure(chunkId) {n var promise = new Promise(function(resolve, reject) {n installedChunkData = installedChunks[chunkId] = [resolve, reject];n });n installedChunkData[2] = promise;n // start chunk loadingn var head = document.getElementsByTagName(head)[0];n var script = document.createElement(script);n script.type = text/javascript;n script.charset = utf-8;n script.async = true;n script.timeout = 120000;n script.src = __webpack_require__.p + "" + chunkId + "." + {"0":"ae9c5f5f","1":"0ac69acb","2":"20651a9c","3":"0cdc6c84"}[chunkId] + ".js";n var timeout = setTimeout(onScriptComplete, 120000);n script.onerror = script.onload = onScriptComplete;n function onScriptComplete() {n // 防止內存泄漏n script.onerror = script.onload = null;n clearTimeout(timeout);n var chunk = installedChunks[chunkId];n if(chunk !== 0) {n if(chunk) {n chunk[1](new Error(Loading chunk + chunkId + failed.));n }n installedChunks[chunkId] = undefined;n }n };n head.appendChild(script);n return promise;n};n

可以看出,原本基於回調函數的方式已經變成基於 Promise 做非同步處理,另外添加了 onScriptComplete 用於做腳本載入失敗處理。

在 webpack1 的時候,如果由於網路原因當你載入腳本失敗後,即使網路恢復了,你再次進行某種操作需要同個 chunk 時候都會無效,主要原因是失敗之後沒把 installedChunks[chunkId] = undefined; 導致之後不會再對該 chunk 文件發起非同步請求。

而在 webpack2 中,當腳本請求超時了(2min)或者載入失敗,會將 installedChunks[chunkId] 清空,當下次重新請求該 chunk 文件會重新載入,提高了頁面的容錯性。

這些是我在打包文件中看到主要的區別,難免有所遺漏,如果你有更多的見解,歡迎在評論區留言。

webpack2 如何做到 tree shaking?

什麼是 tree shaking,即 webpack 在打包的過程中會將沒用的代碼進行清除(dead code)。一般 dead code 具有一下的特徵:

  1. 代碼不會被執行,不可到達
  2. 代碼執行的結果不會被用到
  3. 代碼只會影響死變數(只寫不讀)

是不是很神奇,那麼需要怎麼做才能使 tree shaking 生效呢?

首先,模塊引入要基於 ES6 模塊機制,不再使用 commonjs 規範,因為 es6 模塊的依賴關係是確定的,和運行時的狀態無關,可以進行可靠的靜態分析,然後清除沒用的代碼。而 commonjs 的依賴關係是要到運行時候才能確定下來的。

其次,需要開啟 UglifyJsPlugin 這個插件對代碼進行壓縮。

我們先寫一個例子來說明:

// src/es6/pageA.jsnimport {n utilA,n funcA, // 引入 funcA 但未使用, 故 funcA 會被清除n} from ./js/utilA;nimport utilB from ./js/utilB; // 引入 utilB(函數) 未使用,會被清除nimport classC from ./js/utilC; // 引入 classC(類) 未使用,不會被清除nconsole.log(utilA);nn// src/es6/js/utilA.jsnexport const utilA = util A;nexport function funcA() {n console.log(func A);n}nn// src/es6/js/utilB.jsnexport default function() {n console.log(func B);n}nif(false) { // 被清除n console.log(never use);n}nwhile(true) {}nconsole.log(never use);nn// src/es6/js/utilC.jsnconst classC = function() {} // 類方法不會被清除nclassC.prototype.saySomething = function() {n console.log(class C);n}nexport default classC;n

打包的配置也很簡單:

const webpack = require(webpack);nconst path = require(path)nmodule.exports = {n entry: {n pageA: path.resolve(__dirname, ../src/es6/pageA.js),n },n output: {n path: path.resolve(__dirname, ../dist),n filename: [name].[chunkhash:8].jsn },n plugins: [n new webpack.optimize.CommonsChunkPlugin({n name: manifest,n minChunks: Infinity,n }),n new webpack.optimize.UglifyJsPlugin({n compress: {n warnings: falsen }n })n ]n}n

通過 npm run build:es6 對壓縮的文件進行分析:

// dist/pageA.xxxx.jsnwebpackJsonp([0],[n function(o, t, e) {n use strict;n Object.defineProperty(t, __esModule, { value: !0 });n var n = e(1);n e(2), e(3);n console.log(n.a);n },function(o, t, e) {n use strict;n t.a = util A;n },function(o, t, e) {n use strict;n for (;;);n console.log(never use);n },n function(o, t, e) {n use strict;n const n = function() {};n n.prototype.saySomething = function() {n console.log(class C);n };n }n],[0]);n

引入但是沒用的變數,函數都會清除,未執行的代碼也會被清除。但是類方法是不會被清除的。因為 webpack 不會區分不了是定義在 classC 的 prototype 還是其它 Array 的 prototype 的,比如 classC 寫成下面這樣:

const classC = function() {}nvar a = class + C;nvar b;nif(a === Array) {n b = a;n}else {n b = classC;n}nb.prototype.saySomething = function() {n console.log(class C);n}nexport default classC;n

webpack 無法保證 prototype 掛載的對象是 classC,這種代碼,靜態分析是分析不了的,就算能靜態分析代碼,想要正確完全的分析也比較困難。所以 webpack 乾脆不處理類方法,不對類方法進行 tree shaking。

更多的 tree shaking 的副作用可以查閱:Tree shaking class methods

webpack3 如何做到 scope hoisting?

scope hoisting,顧名思義就是將模塊的作用域提升,在 webpack 中不能將所有所有的模塊直接放在同一個作用域下,有以下幾個原因:

  1. 按需載入的模塊
  2. 使用 commonjs 規範的模塊
  3. 被多 entry 共享的模塊

在 webpack3 中,這些情況生成的模塊不會進行作用域提升,下面我就舉個例子來說明:

// src/hoist/utilA.jsnexport const utilA = util A;nexport function funcA() {n console.log(func A);n}nn// src/hoist/utilB.jsnexport const utilB = util B;nexport function funcB() {n console.log(func B);n}nn// src/hoist/utilC.jsnexport const utilC = util C;nn// src/hoist/pageA.jsnimport { utilA, funcA } from ./utilA;nconsole.log(utilA);nfuncA();nn// src/hoist/pageB.jsnimport { utilA } from ./utilA;nimport { utilB, funcB } from ./utilB;nnfuncB();nimport(./utilC).then(function(utilC) {n console.log(utilC);n})n

這個例子比較典型,utilA 被 pageA 和 pageB 所共享,utilB 被 pageB 單獨載入,utilC 被 pageB 非同步載入。

想要 webpack3 生效,則需要在 plugins 中添加 ModuleConcatenationPlugin。

webpack 配置如下:

const webpack = require(webpack);nconst path = require(path)nmodule.exports = {n entry: {n pageA: path.resolve(__dirname, ../src/hoist/pageA.js),n pageB: path.resolve(__dirname, ../src/hoist/pageB.js),n },n output: {n path: path.resolve(__dirname, ../dist),n filename: [name].[chunkhash:8].jsn },n plugins: [n new webpack.optimize.ModuleConcatenationPlugin(),n new webpack.optimize.CommonsChunkPlugin({n name: vendor,n minChunks: 2,n }),n new webpack.optimize.CommonsChunkPlugin({n name: manifest,n minChunks: Infinity,n })n ]n}n

運行 npm run build:hoist 進行編譯,簡單看下生成的 pageB 代碼:

webpackJsonp([2],{n 2: (function(module, __webpack_exports__, __webpack_require__) {n "use strict";n var utilA = __webpack_require__(0);n // CONCATENATED MODULE: ./src/hoist/utilB.jsn const utilB = util B;n function funcB() {n console.log(func B);n }n // CONCATENATED MODULE: ./src/hoist/pageB.jsn funcB();n __webpack_require__.e/* import() */(0).then(__webpack_require__.bind(null, 3)).then(function(utilC) {n console.log(utilC);n })n })n},[2]);n

通過代碼分析,可以得出下面的結論:

  1. 因為我們配置了共享模塊抽離,所以 utilA 被抽出為單獨模塊,故這部分內容不會進行作用域提升。
  2. utilB 無牽無掛,被 pageB 單獨載入,所以這部分不會生成新的模塊,而是直接作用域提升到 pageB 中。
  3. utilC 被非同步載入,需要抽離成單獨模塊,很明顯沒辦法作用域提升。

結尾

好了,講到這差不多就完了,理解上面的內容對前端模塊化會有更多的認知,如果有什麼寫的不對或者不完整的地方,還望補充說明,希望這篇文章能幫助到你。

推薦閱讀:

淺析 Webpack 插件化設計
webpack增量打包
讀懂webpack生成的26行代碼
你的Tree-Shaking並沒什麼卵用

TAG:webpack |