摘要:它是如何用原生實現模塊間的依賴管理的呢對于按需加載的模塊,它是通過什么方式動態獲取的打包完成后那一堆開頭的代碼是用來干什么的本文將圍繞以上個問題,對照著源碼給出解答。
歡迎關注我的公眾號睿Talk,獲取我最新的文章:
雖然每天都在用webpack,但一直覺得隔著一層神秘的面紗,對它的工作原理一直似懂非懂。它是如何用原生JS實現模塊間的依賴管理的呢?對于按需加載的模塊,它是通過什么方式動態獲取的?打包完成后那一堆/******/開頭的代碼是用來干什么的?本文將圍繞以上3個問題,對照著源碼給出解答。
如果你對webpack的配置調優感興趣,可以看看我之前寫的這篇文章:webpack調優總結
二、模塊管理先寫一個簡單的JS文件,看看webpack打包后會是什么樣子:
// 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文件。去掉不必要的干擾后,核心代碼如下:
// 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的模塊,由一個函數包裹著,注入了module和exports2個變量(本例沒用到)。
核心代碼是__webpack_require__這個函數,它的功能是根據傳入的模塊id,返回模塊export的內容。模塊id由webpack根據文件的依賴關系自動生成,是一個從0開始遞增的數字,入口文件的id為0。所有的模塊都會被webpack用一個函數包裹,按照順序存入上面提到的數組實參當中。
模塊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"); }
啟動文件的模塊數組:
[ /* 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__);
執行的結果也保存在緩存對象中了。
執行流程如下圖所示:
再對代碼進行改造,來研究webpack是如何實現動態加載的:
// main.js console.log("Hello Dickens"); import("./logger").then(logger => { logger.default(); });
logger文件保持不變,編譯后比之前多出了1個chunk。
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加載完成后,對應的值是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。
執行流程如下圖所示:
四、總結本文通過分析webpack生成的啟動代碼,講解了webpack是如何實現模塊管理和動態加載的,希望對你有所幫助。
如果你對webpack的配置調優感興趣,可以看看我之前寫的這篇文章:webpack調優總結
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/97982.html
摘要:如果此時我們不想把文件輸出到內存里,可以通過修改的源代碼來實現。服務啟動成功。。。根據請求的,拼接出 ? webpack-dev-middleware 是express的一個中間件,它的主要作用是以監聽模式啟動webpack,將webpack編譯后的文件輸出到內存里,然后將內存的文件輸出到epxress服務器上;下面通過一張圖片來看一下它的工作原理: showImg(https:...
摘要:主模塊的入口模塊就是。主要就做兩件事引入個功能模塊,并掛載至同一個對象上,對外暴露。在非環境下壓縮代碼,給予警告。后續的源碼解讀和測試例子可以關注源碼解讀倉庫 主模塊 redux的入口模塊就是src/index.js。這個文件的代碼十分簡單。主要就做兩件事: 引入個功能模塊,并掛載至同一個對象上,對外暴露。 在非production環境下壓縮代碼,給予警告。 下面是模塊的源碼(只包...
摘要:應用源碼分析解讀結論熱更新的流程在構建項目時會創建服務端基于和客戶端通常指瀏覽器,項目正式啟動運行時雙方會通過保持連接,用來滿足前后端實時通訊。服務端源碼的關鍵部分每一個都是沒有屬性,表示沒有發生變化。 webpack-dev-server 簡介 Use webpack with a development server that provides live reloading. Th...
摘要:注冊方法之后,當執行了當前的,那么掛載正在當前上的方法就會被執行。比如在開始編譯之前,就能觸發鉤子,就用到了當前的。上面都是前置知識,下面通過解讀一個源碼來鞏固下。先看一段簡單的源碼。,是眾多的的一個,官網的解釋是編譯創建之后,執行插件。 通過解讀webpack-manifest-plugin,了解下plugin機制 先簡單說一下這個插件的功能,生成一份資源清單的json文件,如下 s...
閱讀 3973·2021-10-09 09:43
閱讀 2880·2021-10-08 10:05
閱讀 2740·2021-09-08 10:44
閱讀 889·2019-08-30 15:52
閱讀 2817·2019-08-26 17:01
閱讀 3024·2019-08-26 13:54
閱讀 1657·2019-08-26 10:48
閱讀 815·2019-08-23 14:41