深入理解webpack打包機制


一、單入口文件如何打包

/src/single/index.js

var index2 = require('./index2');
var util = require('./util');

console.log(index2);
console.log(util);

/src/single/index2.js

var util = require('./util');
console.log(util);
module.exports = "index 2";

/src/single/util.js

module.exports = "Hello World";

/config/webpack.config.single.js

const webpack = require('webpack');
const path = require('path');

module.exports = {
  entry: {
    index: [path.resolve(__dirname, '../src/single/index.js')]
  },
  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: '[name].[chunkhash:8].js'
  }
};

運行webpack命令

webpack --config ./config/webpack.config.single.js

得到的單個打包文件/dist/index.35dff1f1.js

 (function(modules) { // webpackBootstrap
     // The module cache已經加載過的模塊
    var installedModules = {};

    // The require function模塊加載函數
    function __webpack_require__(moduleId) {

        // Check if module is in cache判斷模塊是否已經加載過,若加載過直接返回加載的模塊
        if(installedModules[moduleId]) {
             return installedModules[moduleId].exports;
        }
         // Create a new module (and put it into the cache)
         var module = installedModules[moduleId] = {
             i: moduleId,
             l: false,
            exports: {}
         };

         // Execute the module function執行加載函數
         modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

         // Flag the module as loaded標記該模塊已經加載
        module.l = true;

        // Return the exports of the module
         return module.exports;
     }


    // expose the modules object (__webpack_modules__)
     __webpack_require__.m = modules;

     // expose the module cache
    __webpack_require__.c = installedModules;

     // define getter function for harmony exports
     __webpack_require__.d = function(exports, name, getter) {
         if(!__webpack_require__.o(exports, name)) {
             Object.defineProperty(exports, name, {
                 configurable: false,
                 enumerable: true,
                 get: getter
            });
         }
     };

     // getDefaultExport function for compatibility with non-harmony modules
     __webpack_require__.n = function(module) {
         var getter = module && module.__esModule ?
             function getDefault() { return module['default']; } :
             function getModuleExports() { return module; };
         __webpack_require__.d(getter, 'a', getter);
         return getter;
    };

     // Object.prototype.hasOwnProperty.call
     __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

     // __webpack_public_path__
     __webpack_require__.p = "";

     // Load entry module and return exports
     return __webpack_require__(__webpack_require__.s = 1);
 })([
/* 0 */
 (function(module, exports) {

module.exports = "Hello World";

 }),
/* 1 */
(function(module, exports, __webpack_require__) {

module.exports = __webpack_require__(2);


 }),
/* 2 */
(function(module, exports, __webpack_require__) {

var index2 = __webpack_require__(3);
var util = __webpack_require__(0);

console.log(index2);
console.log(util);

}),
/* 3 */
(function(module, exports, __webpack_require__) {

var util = __webpack_require__(0);
console.log(util);
module.exports = "index 2";

 }) ]);
  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、每個模塊只會加載一次,所以重復加載的模塊只會執行一次,加載過的模塊會放到 installedModules,下次需要需要該模塊的值就直接從里面拿了。

2、模塊的 id 直接通過數組下標去一一對應的,這樣能保證簡單且唯一,通過其它方式比如文件名或文件路徑的方式就比較麻煩,因為文件名可能出現重名,不唯一,文件路徑則會增大文件體積,並且將路徑暴露給前端,不夠安全。

3、modules[moduleId].call(module.exports, module, module.exports, __webpack_require__) 保證了模塊加載時 this 的指向 module.exports 並且傳入默認參數

二、多入口文件如何進行代碼切割

我們在開發一些較復雜的應用時,如果沒有對代碼進行切割,將第三方庫(jQuery)或框架(React) 和業務代碼全部打包在一起,就會導致用戶訪問頁面速度很慢,不能有效利用緩存。

// /src/multiple/pageA.js
const utilA = require('./js/utilA');
const utilB = require('./js/utilB');
console.log(utilA);
console.log(utilB);

// /src/multiple/pageB.js
const utilB = require('./js/utilB');
console.log(utilB);
// 異步加載文件,類似於 import()
const utilC = () => require.ensure(['./js/utilC'], function(require) {
  console.log(require('./js/utilC'))
});
utilC();

// /src/multiple/js/utilA.js 可類比於公共庫,如 jQuery
module.exports = "util A";

// /src/multiple/js/utilB.js
module.exports = 'util B';

// /src/multiple/js/utilC.js
module.exports = "util C";

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

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

那么 webpack 需要怎么配置呢?

// 通過 /config/webpack.config.multiple.js 打包
const webpack = require('webpack');
const path = require('path')

module.exports = {
  entry: {
    pageA: [path.resolve(__dirname, '../src/multiple/pageA.js')],
    pageB: path.resolve(__dirname, '../src/multiple/pageB.js'),
  },
  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: '[name].[chunkhash:8].js',
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: 2,
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      chunks: ['vendor']
    })
  ]
}

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

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

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

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

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

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

因為這樣可以給 vendor 生成穩定的 hash 值,每次修改業務代碼(pageA),這段初始化時代碼就會發生變化,那么如果將這段初始化代碼放在 vendor 文件中的話,每次都會生成新的 vendor.xxxx.js,這樣不利於持久化緩存

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

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

mainifest.xxxx.js // 初始化代碼
vendor.xxxx.js    // pageA 和 pageB 共同用到的模塊,抽離
pageX.xxxx.js     // 業務代碼 
當 pageB 需要 utilC 時候則異步加載 utilC

執行命令

webpack --config ./config/webpack.config.multiple.js

結果生成了5個文件:異步加載文件utilC.js單獨打包成了一個文件0.×××.js,入口pageA,pageB分別打包成文件,pageA和pageB共用模塊單獨打包成vendor, 初始化代碼manifest

那么manifest如何做初始化工作呢?

(function(modules) { // webpackBootstrap
     // install a JSONP callback for chunk loading
     var parentJsonpFunction = window["webpackJsonp"];
     window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
         // add "moreModules" to the modules object,
         // then flag all "chunkIds" as loaded and fire callback
         var moduleId, chunkId, i = 0, resolves = [], result;
         for(;i < chunkIds.length; i++) {
             chunkId = chunkIds[i];
             if(installedChunks[chunkId]) {
                 resolves.push(installedChunks[chunkId][0]);
             }
             installedChunks[chunkId] = 0;
         }
         for(moduleId in moreModules) {
             if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
                 modules[moduleId] = moreModules[moduleId];
             }
         }
         if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);
         while(resolves.length) {
             resolves.shift()();
         }
         if(executeModules) {
             for(i=0; i < executeModules.length; i++) {
                 result = __webpack_require__(__webpack_require__.s = executeModules[i]);
             }
         }
         return result;
     };

     // The module cache
     var installedModules = {};

     // objects to store loaded and loading chunks
     var installedChunks = {
         4: 0
     };

     // The require function模塊加載函數
     function __webpack_require__(moduleId) {

         // Check if module is in cache
         if(installedModules[moduleId]) {
             return installedModules[moduleId].exports;
         }
         // Create a new module (and put it into the cache)
         var module = installedModules[moduleId] = {
             i: moduleId,
             l: false,
             exports: {}
         };

         // Execute the module function
         modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

         // Flag the module as loaded
         module.l = true;

         // Return the exports of the module
        return module.exports;
     }

     // This file contains only the entry chunk.
     // The chunk loading function for additional chunks
     __webpack_require__.e = function requireEnsure(chunkId) {
         var installedChunkData = installedChunks[chunkId];
         if(installedChunkData === 0) {
             return new Promise(function(resolve) { resolve(); });
         }

        // a Promise means "currently loading".
        if(installedChunkData) {
             return installedChunkData[2];
         }

         // setup Promise in chunk cache
         var promise = new Promise(function(resolve, reject) {
             installedChunkData = installedChunks[chunkId] = [resolve, reject];
         });
         installedChunkData[2] = promise;

         // start chunk loading
         var head = document.getElementsByTagName('head')[0];
         var script = document.createElement('script');
         script.type = "text/javascript";
        script.charset = 'utf-8';
         script.async = true;
         script.timeout = 120000;

         if (__webpack_require__.nc) {
             script.setAttribute("nonce", __webpack_require__.nc);
        }
         script.src = __webpack_require__.p + "" + chunkId + "." + {"0":"ae9c5f5f"}[chunkId] + ".js";
         var timeout = setTimeout(onScriptComplete, 120000);
         script.onerror = script.onload = onScriptComplete;
         function onScriptComplete() {
             // avoid mem leaks in IE.
             script.onerror = script.onload = null;
             clearTimeout(timeout);
             var chunk = installedChunks[chunkId];
             if(chunk !== 0) {
                 if(chunk) {
                     chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
                 }
                 installedChunks[chunkId] = undefined;
             }
         };
         head.appendChild(script);

         return promise;
     };

     // expose the modules object (__webpack_modules__)
     __webpack_require__.m = modules;

     // expose the module cache
     __webpack_require__.c = installedModules;

     // define getter function for harmony exports
     __webpack_require__.d = function(exports, name, getter) {
         if(!__webpack_require__.o(exports, name)) {
             Object.defineProperty(exports, name, {
                 configurable: false,
                 enumerable: true,
                 get: getter
             });
        }
     };

     // getDefaultExport function for compatibility with non-harmony modules
     __webpack_require__.n = function(module) {
         var getter = module && module.__esModule ?
             function getDefault() { return module['default']; } :
             function getModuleExports() { return module; };
         __webpack_require__.d(getter, 'a', getter);
         return getter;
     };

     // Object.prototype.hasOwnProperty.call
     __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

     // __webpack_public_path__
     __webpack_require__.p = "";

     // on error function for async loading
    __webpack_require__.oe = function(err) { console.error(err); throw err; };
 })([]);

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

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

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

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

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

webpackJsonp([3],[
/* 0 */
(function(module, exports) {

module.exports = 'util B';

 })
]);

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

webpack如何加載異步腳本:

// 異步加載函數掛載在__webpack_require__.e
// This file contains only the entry chunk.
     // The chunk loading function for additional chunks
     __webpack_require__.e = function requireEnsure(chunkId) {
         var installedChunkData = installedChunks[chunkId];
         if(installedChunkData === 0) {
             return new Promise(function(resolve) { resolve(); });
         }

        // a Promise means "currently loading".
        if(installedChunkData) {
             return installedChunkData[2];
         }

         // setup Promise in chunk cache
         var promise = new Promise(function(resolve, reject) {
             installedChunkData = installedChunks[chunkId] = [resolve, reject];
         });
         installedChunkData[2] = promise;

         // start chunk loading
         var head = document.getElementsByTagName('head')[0];
         var script = document.createElement('script');
         script.type = "text/javascript";
        script.charset = 'utf-8';
         script.async = true;
         script.timeout = 120000;

         if (__webpack_require__.nc) {
             script.setAttribute("nonce", __webpack_require__.nc);
        }
         script.src = __webpack_require__.p + "" + chunkId + "." + {"0":"ae9c5f5f"}[chunkId] + ".js";
         var timeout = setTimeout(onScriptComplete, 120000);
         script.onerror = script.onload = onScriptComplete;
         function onScriptComplete() {
             // avoid mem leaks in IE.
             script.onerror = script.onload = null;
             clearTimeout(timeout);
             var chunk = installedChunks[chunkId];
             if(chunk !== 0) {
                 if(chunk) {
                     chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
                 }
                 installedChunks[chunkId] = undefined;
             }
         };
         head.appendChild(script);

         return promise;
     };

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

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

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

webpackJsonp([0],[
/* 0 */,
/* 1 */
 (function(module, exports) {
module.exports = "util C";
 })
]);

pageB需要異步加載這個chunk:

webpackJsonp([2],{
4:
 (function(module, exports, __webpack_require__) {
const utilB = __webpack_require__(0);
console.log(utilB);
const utilC = () => __webpack_require__.e/* require.ensure */(0).then((function(require) {
  console.log(__webpack_require__(1))
}).bind(null, __webpack_require__)).catch(__webpack_require__.oe);
utilC();
 })
},[4]);

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

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

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

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

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

在 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.js
import {
  utilA,
  funcA,    // 引入 funcA 但未使用, 故 funcA 會被清除
} from './js/utilA';
import utilB from './js/utilB';   // 引入 utilB(函數) 未使用,會被清除
import classC from './js/utilC';   // 引入 classC(類) 未使用,不會被清除
console.log(utilA);

// src/es6/js/utilA.js
export const utilA = 'util A';
export function funcA() {
  console.log('func A');
}

// src/es6/js/utilB.js
export default function() {
  console.log('func B');
}
if(false) {  // 被清除
  console.log('never use');
}
while(true) {}
console.log('never use');

// src/es6/js/utilC.js
const classC = function() {}  // 類方法不會被清除
classC.prototype.saySomething = function() {
  console.log('class C');
}
export default classC;

打包的配置也很簡單:

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

對壓縮的文件進行分析:

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

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

const classC = function() {}
var a = 'class' + 'C';
var b;
if(a === 'Array') {
  b = a;
}else {
  b = 'classC';
}
b.prototype.saySomething = function() {
  console.log('class C');
}
export default classC;

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.js
export const utilA = 'util A';
export function funcA() {
  console.log('func A');
}

// src/hoist/utilB.js
export const utilB = 'util B';
export function funcB() {
  console.log('func B');
}

// src/hoist/utilC.js
export const utilC = 'util C';

// src/hoist/pageA.js
import { utilA, funcA } from './utilA';
console.log(utilA);
funcA();

// src/hoist/pageB.js
import { utilA } from './utilA';
import { utilB, funcB } from './utilB';

funcB();
import('./utilC').then(function(utilC) {
  console.log(utilC);
})

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

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

webpack 配置如下:

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

運行webpack --config ./config/webpack.config.hoist.js進行編譯,簡單看下生成的 pageB 代碼:

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

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

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

轉自:https://github.com/happylindz/blog/issues/6


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM