webpack模塊化原理-Code Splitting

webpack的模塊化不只支持commonjs和es module,還能經過code splitting實現模塊的動態加載。根據wepack官方文檔,實現動態加載的方式有兩種:importrequire.ensurejavascript

那麼,這篇文檔就來分析一下,webpack是如何實現code splitting的。java

PS:若是你對webpack如何實現commonjs和es module感興趣,能夠查看個人前兩篇文章:webpack模塊化原理-commonjswebpack模塊化原理-ES modulewebpack

準備

首先咱們依然建立一個簡單入口模塊index.js和兩個依賴模塊foo.jsbar.jses6

// index.js
'use strict';
import(/* webpackChunkName: "foo" */ './foo').then(foo => {
    console.log(foo());
})
import(/* webpackChunkName: "bar" */ './bar').then(bar => {
    console.log(bar());
})
// foo.js
'use strict';
exports.foo = function () {
    return 2;
}
// bar.js
'use strict';
exports.bar = function () {
    return 1;
}

webpack配置以下:web

var path = require("path");

module.exports = {
    entry: path.join(__dirname, 'index.js'),
    output: {
        path: path.join(__dirname, 'outs'),
        filename: 'index.js',
        chunkFilename: '[name].bundle.js'
    },
};

這是一個最簡單的配置,指定了模塊入口和打包文件輸出路徑,值得注意的是,此次還指定了分離模塊的文件名[name].bundle.js(不指定會有默認文件名)。json

在根目錄下執行webpack,獲得通過webpack打包的代碼以下(去掉了沒必要要的註釋):segmentfault

(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()();
        }
    };
    // The module cache
    var installedModules = {};
    // objects to store loaded and loading chunks
    var installedChunks = {
        2: 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 + "" + ({"0":"foo","1":"bar"}[chunkId]||chunkId) + ".bundle.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; };
    // Load entry module and return exports
    return __webpack_require__(__webpack_require__.s = 0);
})
([
(function(module, exports, __webpack_require__) {
    "use strict";
    __webpack_require__.e/* import() */(0).then(__webpack_require__.bind(null, 1)).then(foo => {
        console.log(foo());
    })
    __webpack_require__.e/* import() */(1).then(__webpack_require__.bind(null, 2)).then(bar => {
        console.log(bar());
    })
})
]);

分析

編譯後的代碼,總體跟前兩篇文章中使用commonjs和es6 module編寫的代碼編譯後的結構差異不大,都是經過IFFE的方式啓動代碼,而後使用webpack實現的requireexports實現的模塊化。數組

而對於code splitting的支持,區別在於這裏使用__webpack_require__.e實現動態加載模塊和實現基於promise的模塊導入。promise

因此首先分析__webpack_require__.e函數的定義,這個函數實現了動態加載:緩存

__webpack_require__.e = function requireEnsure(chunkId) {
    // 一、緩存查找
    var installedChunkData = installedChunks[chunkId];
    if(installedChunkData === 0) {
        return new Promise(function(resolve) { resolve(); });
    }
    if(installedChunkData) {
        return installedChunkData[2];
    }
    // 二、緩存模塊
    var promise = new Promise(function(resolve, reject) {
        installedChunkData = installedChunks[chunkId] = [resolve, reject];
    });
    installedChunkData[2] = promise;
    // 三、加載模塊
    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 + "" + ({"0":"foo"}[chunkId]||chunkId) + ".bundle.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);
    // 五、返回promise
    return promise;
};

代碼大體邏輯以下:

  1. 緩存查找:從緩存installedChunks中查找是否有緩存模塊,若是緩存標識爲0,則表示模塊已加載過,直接返回promise;若是緩存爲數組,表示緩存正在加載中,則返回緩存的promise對象
  2. 若是沒有緩存,則建立一個promise,並將promiseresolvereject緩存在installedChunks
  3. 構建一個script標籤,append到head標籤中,src指向加載的模塊腳本資源,實現動態加載js腳本
  4. 添加script標籤onload、onerror 事件,若是超時或者模塊加載失敗,則會調用reject返回模塊加載失敗異常
  5. 若是模塊加載成功,則返回當前模塊promise,對應於import()

以上即是模塊加載的過程,當資源加載完成,模塊代碼開始執行,那麼咱們來看一下模塊代碼的結構:

webpackJsonp([0],[
/* 0 */,
/* 1 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
exports.foo = function () {
    return 2;
}
/***/ })
]);

能夠看到,模塊代碼不只被包在一個函數中(用來模擬模塊做用域),外層還被當作參數傳入webpackJsonp中。那麼這個webpackJsonp函數的做用是什麼呢?

其實這裏的webpackJsonp相似於jsonp中的callback,做用是做爲模塊加載和執行完成的回調,從而觸發importresolve

具體細看webpackJsonp代碼來分析:

window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
    var moduleId, chunkId, i = 0, resolves = [], result;
    // 一、收集模塊resolve
    for(;i < chunkIds.length; i++) {
        chunkId = chunkIds[i];
        if(installedChunks[chunkId]) {
            resolves.push(installedChunks[chunkId][0]);
        }
        installedChunks[chunkId] = 0;
    }
    // 二、copy模塊到modules
    for(moduleId in moreModules) {
        if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
            modules[moduleId] = moreModules[moduleId];
        }
    }
    if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);
    // 三、resolve import
    while(resolves.length) {
        resolves.shift()();
    }
};

代碼大體邏輯以下:

  1. 根據chunkIds收集對應模塊的resolve,這裏的chunkIds爲數組是由於require.ensure是能夠實現異步加載多個模塊的,因此須要兼容
  2. 把動態模塊添加到IFFE的modules中,提供其餘CMD方案使用模塊
  3. 直接調用resolve,完成整個異步加載

總結

webpack經過__webpack_require__.e函數實現了動態加載,再經過webpackJsonp函數實現異步加載回調,把模塊內容以promise的方式暴露給調用方,從而實現了對code splitting的支持。

相關文章
相關標籤/搜索