摘要:它是如何用原生實現(xiàn)模塊間的依賴管理的呢對于按需加載的模塊,它是通過什么方式動態(tài)獲取的打包完成后那一堆開頭的代碼是用來干什么的本文將圍繞以上個問題,對照著源碼給出解答。
歡迎關(guān)注我的公眾號睿Talk,獲取我最新的文章:
雖然每天都在用webpack,但一直覺得隔著一層神秘的面紗,對它的工作原理一直似懂非懂。它是如何用原生JS實現(xiàn)模塊間的依賴管理的呢?對于按需加載的模塊,它是通過什么方式動態(tài)獲取的?打包完成后那一堆/******/開頭的代碼是用來干什么的?本文將圍繞以上3個問題,對照著源碼給出解答。
如果你對webpack的配置調(diào)優(yōu)感興趣,可以看看我之前寫的這篇文章:webpack調(diào)優(yōu)總結(jié)
二、模塊管理先寫一個簡單的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") } };
在當(dāng)前目錄下運行webpack,會在dist目錄下面生成打包好的bundle.js文件。去掉不必要的干擾后,核心代碼如下:
// webpack啟動代碼 (function (modules) { // 模塊緩存對象 var installedModules = {}; // webpack實現(xiàn)的require函數(shù) function __webpack_require__(moduleId) { // 檢查緩存對象,看模塊是否加載過 if (installedModules[moduleId]) { return installedModules[moduleId].exports; } // 創(chuàng)建一個新的模塊緩存,再存入緩存對象 var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; // 執(zhí)行模塊代碼 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // 將模塊標識為已加載 module.l = true; // 返回export的內(nèi)容 return module.exports; } ... // 加載入口模塊 return __webpack_require__(__webpack_require__.s = 0); }) ([ /* 0 */ (function (module, exports) { console.log("Hello Dickens"); }) ]);
代碼是一個立即執(zhí)行函數(shù),參數(shù)modules是由各個模塊組成的數(shù)組,本例子只有一個編號為0的模塊,由一個函數(shù)包裹著,注入了module和exports2個變量(本例沒用到)。
核心代碼是__webpack_require__這個函數(shù),它的功能是根據(jù)傳入的模塊id,返回模塊export的內(nèi)容。模塊id由webpack根據(jù)文件的依賴關(guān)系自動生成,是一個從0開始遞增的數(shù)字,入口文件的id為0。所有的模塊都會被webpack用一個函數(shù)包裹,按照順序存入上面提到的數(shù)組實參當(dāng)中。
模塊export的內(nèi)容會被緩存在installedModules中。當(dāng)獲取模塊內(nèi)容的時候,如果已經(jīng)加載過,則直接從緩存返回,否則根據(jù)id從modules形參中取出模塊內(nèi)容并執(zhí)行,同時將結(jié)果保存到緩存對象當(dāng)中。緩存對象數(shù)據(jù)結(jié)構(gòu)如下:
我們再添加一個文件,在入口文件處導(dǎo)入,再來看看生成的啟動文件是怎樣的。
// main.js import logger from "./logger"; console.log("Hello Dickens"); logger(); //logger.js export default function log() { console.log("Log from logger"); }
啟動文件的模塊數(shù)組:
[ /* 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"); } }) ]
可以看到現(xiàn)在有2個模塊,每個模塊的包裹函數(shù)都傳入了module, __webpack_exports__, __webpack_require__三個參數(shù),它們是通過上文提到的__webpack_require__注入的:
// 執(zhí)行模塊代碼 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
執(zhí)行的結(jié)果也保存在緩存對象中了。
執(zhí)行流程如下圖所示:
再對代碼進行改造,來研究webpack是如何實現(xiàn)動態(tài)加載的:
// main.js console.log("Hello Dickens"); import("./logger").then(logger => { logger.default(); });
logger文件保持不變,編譯后比之前多出了1個chunk。
bundle_asy的內(nèi)容如下:
(function (modules) { // 加載成功后的JSONP回調(diào)函數(shù) var parentJsonpFunction = window["webpackJsonp"]; // 加載成功后的JSONP回調(diào)函數(shù) 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,將其放入加載成功數(shù)組 if (installedChunks[chunkId]) { // promise的resolve resolves.push(installedChunks[chunkId][0]); } // 標記模塊加載完成 installedChunks[chunkId] = 0; } // 將動態(tài)加載的模塊添加到modules數(shù)組中,以供后續(xù)的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 = {}; // 記錄正在加載和已經(jīng)加載的chunk的對象,0表示已經(jīng)加載成功 // 1是當(dāng)前模塊的編號,已加載完成 var installedChunks = { 1: 0 }; // require函數(shù),跟上面的一樣 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; } // 按需加載,通過動態(tài)添加script標簽實現(xiàn) __webpack_require__.e = function requireEnsure(chunkId) { var installedChunkData = installedChunks[chunkId]; // chunk已經(jīng)加載成功 if (installedChunkData === 0) { return new Promise(function (resolve) { resolve(); }); } // 加載中,返回之前創(chuàng)建的promise(數(shù)組下標為2) if (installedChunkData) { return installedChunkData[2]; } // 將promise相關(guān)函數(shù)保持到installedChunks中方便后續(xù)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]; // 正常的流程,模塊加載完后會調(diào)用webpackJsonp方法,將chunk置為0 // 如果不為0,則可能是加載失敗或者超時 if (chunk !== 0) { if (chunk) { // 調(diào)用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(); }); }) ]);
這里用戶記錄異步模塊加載狀態(tài)的對象installedChunks的數(shù)據(jù)結(jié)構(gòu)如下:
當(dāng)chunk加載完成后,對應(yīng)的值是0。在加載過程中,對應(yīng)的值是一個數(shù)組,數(shù)組內(nèi)保存了promise的相關(guān)信息。
掛在到window下面的webpackJsonp函數(shù)是動態(tài)加載模塊代碼下載后的回調(diào),它會通知webpack模塊下載完成并將模塊加入到modules當(dāng)中。
__webpack_require__.e函數(shù)是動態(tài)加載的核心實現(xiàn),它通過動態(tài)創(chuàng)建一個script標簽來實現(xiàn)代碼的異步加載。加載開始前會創(chuàng)建一個promise存到installedChunks對象當(dāng)中,加載成功則調(diào)用resolve,失敗則調(diào)用reject。resolve后不會傳入模塊本身,而是通過__webpack_require__來加載模塊內(nèi)容,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的指正)。詳細請看動態(tài)加載的chunk的代碼,0.bundle_asy的內(nèi)容如下:
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"); } }) ]);
代碼非常好理解,加載成功后立即調(diào)用上文提到的webpackJsonp方法,將chunkId和模塊內(nèi)容傳入。這里要分清2個概念,一個是chunkId,一個moduleId。這個chunk的chunkId是0,里面只包含一個module,moduleId是1。一個chunk里面可以包含多個module。
執(zhí)行流程如下圖所示:
四、總結(jié)本文通過分析webpack生成的啟動代碼,講解了webpack是如何實現(xiàn)模塊管理和動態(tài)加載的,希望對你有所幫助。
如果你對webpack的配置調(diào)優(yōu)感興趣,可以看看我之前寫的這篇文章:webpack調(diào)優(yōu)總結(jié)
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/97982.html
摘要:如果此時我們不想把文件輸出到內(nèi)存里,可以通過修改的源代碼來實現(xiàn)。服務(wù)啟動成功。。。根據(jù)請求的,拼接出 ? webpack-dev-middleware 是express的一個中間件,它的主要作用是以監(jiān)聽模式啟動webpack,將webpack編譯后的文件輸出到內(nèi)存里,然后將內(nèi)存的文件輸出到epxress服務(wù)器上;下面通過一張圖片來看一下它的工作原理: showImg(https:...
摘要:主模塊的入口模塊就是。主要就做兩件事引入個功能模塊,并掛載至同一個對象上,對外暴露。在非環(huán)境下壓縮代碼,給予警告。后續(xù)的源碼解讀和測試例子可以關(guān)注源碼解讀倉庫 主模塊 redux的入口模塊就是src/index.js。這個文件的代碼十分簡單。主要就做兩件事: 引入個功能模塊,并掛載至同一個對象上,對外暴露。 在非production環(huán)境下壓縮代碼,給予警告。 下面是模塊的源碼(只包...
摘要:應(yīng)用源碼分析解讀結(jié)論熱更新的流程在構(gòu)建項目時會創(chuàng)建服務(wù)端基于和客戶端通常指瀏覽器,項目正式啟動運行時雙方會通過保持連接,用來滿足前后端實時通訊。服務(wù)端源碼的關(guān)鍵部分每一個都是沒有屬性,表示沒有發(fā)生變化。 webpack-dev-server 簡介 Use webpack with a development server that provides live reloading. Th...
摘要:注冊方法之后,當(dāng)執(zhí)行了當(dāng)前的,那么掛載正在當(dāng)前上的方法就會被執(zhí)行。比如在開始編譯之前,就能觸發(fā)鉤子,就用到了當(dāng)前的。上面都是前置知識,下面通過解讀一個源碼來鞏固下。先看一段簡單的源碼。,是眾多的的一個,官網(wǎng)的解釋是編譯創(chuàng)建之后,執(zhí)行插件。 通過解讀webpack-manifest-plugin,了解下plugin機制 先簡單說一下這個插件的功能,生成一份資源清單的json文件,如下 s...
閱讀 4089·2021-10-09 09:43
閱讀 2935·2021-10-08 10:05
閱讀 2825·2021-09-08 10:44
閱讀 938·2019-08-30 15:52
閱讀 2901·2019-08-26 17:01
閱讀 3082·2019-08-26 13:54
閱讀 1724·2019-08-26 10:48
閱讀 881·2019-08-23 14:41