webpack啓動代碼源碼解讀

歡迎關注個人公衆號睿Talk,獲取我最新的文章:
clipboard.pngjavascript

1、前言

雖然天天都在用webpack,但一直以爲隔着一層神祕的面紗,對它的工做原理一直似懂非懂。它是如何用原生JS實現模塊間的依賴管理的呢?對於按需加載的模塊,它是經過什麼方式動態獲取的?打包完成後那一堆/******/開頭的代碼是用來幹什麼的?本文將圍繞以上3個問題,對照着源碼給出解答。java

若是你對webpack的配置調優感興趣,能夠看看我以前寫的這篇文章:webpack調優總結webpack

2、模塊管理

先寫一個簡單的JS文件,看看webpack打包後會是什麼樣子:web

// main.js
console.log('Hello Dickens');

// webpack.config.js
const path = require('path');
module.exports = {
  entry: './main.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};

在當前目錄下運行webpack,會在dist目錄下面生成打包好的bundle.js文件。去掉沒必要要的干擾後,核心代碼以下:segmentfault

// webpack啓動代碼
(function (modules) { 
    // 模塊緩存對象
    var installedModules = {};

    // webpack實現的require函數
    function __webpack_require__(moduleId) {
        // 檢查緩存對象,看模塊是否加載過
        if (installedModules[moduleId]) {
            return installedModules[moduleId].exports;
        }

        // 建立一個新的模塊緩存,再存入緩存對象
        var module = installedModules[moduleId] = {
            i: moduleId,
            l: false,
            exports: {}
        };

        // 執行模塊代碼
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
        
        // 將模塊標識爲已加載
        module.l = true;

        // 返回export的內容
        return module.exports;
    }

    ...

    // 加載入口模塊
    return __webpack_require__(__webpack_require__.s = 0);
})
([
    /* 0 */
    (function (module, exports) {
        console.log('Hello Dickens');
    })
]);

代碼是一個當即執行函數,參數modules是由各個模塊組成的數組,本例子只有一個編號爲0的模塊,由一個函數包裹着,注入了moduleexports2個變量(本例沒用到)。數組

核心代碼是__webpack_require__這個函數,它的功能是根據傳入的模塊id,返回模塊export的內容。模塊id由webpack根據文件的依賴關係自動生成,是一個從0開始遞增的數字,入口文件的id爲0。全部的模塊都會被webpack用一個函數包裹,按照順序存入上面提到的數組實參當中。promise

模塊export的內容會被緩存在installedModules中。當獲取模塊內容的時候,若是已經加載過,則直接從緩存返回,不然根據id從modules形參中取出模塊內容並執行,同時將結果保存到緩存對象當中。緩存對象數據結構以下:緩存

模塊緩存對象

咱們再添加一個文件,在入口文件處導入,再來看看生成的啓動文件是怎樣的。數據結構

// main.js
import logger from './logger';

console.log('Hello Dickens');
logger();

//logger.js
export default function log() {
    console.log('Log from logger');
}

啓動文件的模塊數組:app

[
    /* 0 */
    (function (module, __webpack_exports__, __webpack_require__) {

        "use strict";
        Object.defineProperty(__webpack_exports__, "__esModule", {
            value: true
        });
        /* harmony import */
        var __WEBPACK_IMPORTED_MODULE_0__logger__ = __webpack_require__(1);

        console.log('Hello Dickens');

        Object(__WEBPACK_IMPORTED_MODULE_0__logger__["a" /* default */ ])();
    }),
    /* 1 */
    (function (module, __webpack_exports__, __webpack_require__) {
    
        "use strict";
        /* harmony export (immutable) */
        __webpack_exports__["a"] = log;

        function log() {
            console.log('Log from logger');
        }
    })
]

能夠看到如今有2個模塊,每一個模塊的包裹函數都傳入了module, __webpack_exports__, __webpack_require__三個參數,它們是經過上文提到的__webpack_require__注入的:

// 執行模塊代碼
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

執行的結果也保存在緩存對象中了。

執行流程以下圖所示:
同步

3、按需加載

再對代碼進行改造,來研究webpack是如何實現動態加載的:

// main.js
console.log('Hello Dickens');

import('./logger').then(logger => {
    logger.default();
});

logger文件保持不變,編譯後比以前多出了1個chunk。
clipboard.png

bundle_asy的內容以下:

(function (modules) {
    // 加載成功後的JSONP回調函數
    var parentJsonpFunction = window["webpackJsonp"];

    // 加載成功後的JSONP回調函數
    window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
        var moduleId, chunkId, i = 0,
            resolves = [],
            result;

        for (; i < chunkIds.length; i++) {
            chunkId = chunkIds[i];
            
            // installedChunks[chunkId]不爲0且不爲undefined,將其放入加載成功數組
            if (installedChunks[chunkId]) {
                // promise的resolve
                resolves.push(installedChunks[chunkId][0]);
            }
            
            // 標記模塊加載完成
            installedChunks[chunkId] = 0;
        }

        // 將動態加載的模塊添加到modules數組中,以供後續的require使用
        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()();
        }
    };

    // 模塊緩存對象
    var installedModules = {};

    // 記錄正在加載和已經加載的chunk的對象,0表示已經加載成功
    // 1是當前模塊的編號,已加載完成
    var installedChunks = {
        1: 0
    };

    // require函數,跟上面的同樣
    function __webpack_require__(moduleId) {
        if (installedModules[moduleId]) {
            return installedModules[moduleId].exports;
        }

        var module = installedModules[moduleId] = {
            i: moduleId,
            l: false,
            exports: {}
        };

        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

        module.l = true;

        return module.exports;
    }

    // 按需加載,經過動態添加script標籤實現
    __webpack_require__.e = function requireEnsure(chunkId) {
        var installedChunkData = installedChunks[chunkId];

        // chunk已經加載成功
        if (installedChunkData === 0) {
            return new Promise(function (resolve) {
                resolve();
            });
        }

        // 加載中,返回以前建立的promise(數組下標爲2)
        if (installedChunkData) {
            return installedChunkData[2];
        }

        // 將promise相關函數保持到installedChunks中方便後續resolve或reject
        var promise = new Promise(function (resolve, reject) {
            installedChunkData = installedChunks[chunkId] = [resolve, reject];
        });
        installedChunkData[2] = promise;

        // 啓動chunk的異步加載
        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 + ".bundle_async.js";
        script.onerror = script.onload = onScriptComplete;
        var timeout = setTimeout(onScriptComplete, 120000);

        function onScriptComplete() {
            script.onerror = script.onload = null;
            
            clearTimeout(timeout);
            
            var chunk = installedChunks[chunkId];
            
            // 正常的流程,模塊加載完後會調用webpackJsonp方法,將chunk置爲0
            // 若是不爲0,則多是加載失敗或者超時
            if (chunk !== 0) {
                if (chunk) {
                    // 調用promise的reject
                    chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
                }
                installedChunks[chunkId] = undefined;
            }
        };
        
        head.appendChild(script);
        
        return promise;
    };

    ...
    
    // 加載入口模塊
    return __webpack_require__(__webpack_require__.s = 0);
})
([
    /* 0 */
    (function (module, exports, __webpack_require__) {

        console.log('Hello Dickens');

        // promise resolve後,會指定加載哪一個模塊
        __webpack_require__.e /* import() */(0)
            .then(__webpack_require__.bind(null, 1))
            .then(logger => {
                logger.default();
            });
    })
]);

這裏用戶記錄異步模塊加載狀態的對象installedChunks的數據結構以下:

chunk

當chunk加載完成後,對應的值是0。在加載過程當中,對應的值是一個數組,數組內保存了promise的相關信息。

掛在到window下面的webpackJsonp函數是動態加載模塊代碼下載後的回調,它會通知webpack模塊下載完成並將模塊加入到modules當中。

__webpack_require__.e函數是動態加載的核心實現,它經過動態建立一個script標籤來實現代碼的異步加載。加載開始前會建立一個promise存到installedChunks對象當中,加載成功則調用resolve,失敗則調用reject。resolve後不會傳入模塊自己,而是經過__webpack_require__來加載模塊內容,require的模塊id由webpack來生成:

__webpack_require__.e /* import() */(0)
    .then(__webpack_require__.bind(null, 1))
    .then(logger => {
        logger.default();
    });

這裏之因此要加上default是由於遇到按需加載時,若是使用的是ES Module,webpack會將export default編譯成__webpack_exports__對象的default屬性(感謝@MrCanJu的指正)。詳細請看動態加載的chunk的代碼,0.bundle_asy的內容以下:

webpackJsonp([0], [
    /* 0 */
    ,
    /* 1 */
    (function (module, __webpack_exports__, __webpack_require__) {

        "use strict";
        Object.defineProperty(__webpack_exports__, "__esModule", {
            value: true
        });
        /* harmony export (immutable) */
        __webpack_exports__["default"] = log;

        function log() {
            console.log('Log from logger');
        }
    })
]);

代碼很是好理解,加載成功後當即調用上文提到的webpackJsonp方法,將chunkId和模塊內容傳入。這裏要分清2個概念,一個是chunkId,一個moduleId。這個chunk的chunkId是0,裏面只包含一個module,moduleId是1。一個chunk裏面能夠包含多個module。

執行流程以下圖所示:

異步

4、總結

本文經過分析webpack生成的啓動代碼,講解了webpack是如何實現模塊管理和動態加載的,但願對你有所幫助。

若是你對webpack的配置調優感興趣,能夠看看我以前寫的這篇文章:webpack調優總結

相關文章
相關標籤/搜索