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