webpack5的Runtime代碼淺析

語言: CN / TW / HK

閲讀須知:本篇內容涵蓋了非常多的代碼不建議粗略的閲讀它應該去親自的調試代碼仔細的去看,我想這樣才能有所收穫,本文就是帶着大家明白Runtime源碼中主要函數有那些通過什麼去調用的以及它的功能。閲讀源碼最關鍵的是有一個粗大的神經去閲讀,js中的代碼就是一個函數跳到另外一個函數的執行,你首先要明白的就是這個函數的主要功能是幹啥的再去閲讀下一個函數。我沒有使用到調試工具,本文是通過入口文件的引入文件慢慢去往下查找的,非常推薦使用 vscode 的調試工具去打斷點調試。

簡介

本篇主要是分析了 webpack5 的打包後代碼,文件經過 webpack 打包後會增加很多東西那這些東西就是我們現在要做的,順便分析了以下 import() 這種按需加載它做出了那些工作是怎麼個按需加載法。最後也希望大家能通過這篇文章的學習了也能寫出一個簡單打包器。

準備工作

首先的準備工作是

//index.js 入口文件 import _, { name } from './es'; let co = require('./common'); co.sayHello(name); export default _; ​ //es.js export const age = 18; export const name = "前端事務所"; export default "ESModule"; ​ //common.js exports.sayHello = (name, desc) => {  console.log(`歡迎關注[前端事務所]~`); }

webpack.config.js 中的mode:"development" 不使用 optimazation.runtimeChunk: "single" 然後在 npx webpack 開始打包獲得一個 打包後的文件

webpack產出的代碼

/******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ({ ​ /***/ "./scr/common.js": /***/ ((__unused_webpack_module, exports) => { ​  eval("//common.js\r\nexports.sayHello = (name, desc) => {\r\n console.log(`歡迎關注[前端事務所]~`);\r\n}\n\n//# sourceURL=webpack://webpack-demo-two/./scr/common.js?");  /***/ }),    /***/ "./scr/es.js":  /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {    "use strict";  eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   "age": () => (/* binding */ age),\n/* harmony export */   "name": () => (/* binding */ name),\n/* harmony export */   "default": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\nconst age = 18;\r\nconst name = "前端事務所";\r\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("ESModule");\n\n//# sourceURL=webpack://webpack-demo-two/./scr/es.js?");  /***/ }),    /***/ "./scr/index.js":  /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {    "use strict";  eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   "default": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _es__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./es */ "./scr/es.js");\n\r\n//index.js 入口文件\r\n\r\nlet co = __webpack_require__(/*! ./common */ "./scr/common.js");\r\nco.sayHello(_es__WEBPACK_IMPORTED_MODULE_0__.name);\r\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (_es__WEBPACK_IMPORTED_MODULE_0__["default"]);\n\n//# sourceURL=webpack://webpack-demo-two/./scr/index.js?");  /***/ })    /******/ });  /******/ // The module cache  /******/ var __webpack_module_cache__ = {};  /******/  /******/ // The require function  /******/ function __webpack_require__(moduleId) {  /******/ // Check if module is in cache  /******/ var cachedModule = __webpack_module_cache__[moduleId];  /******/ if (cachedModule !== undefined) {  /******/ return cachedModule.exports;  /******/ }  /******/ // Create a new module (and put it into the cache)  /******/ var module = __webpack_module_cache__[moduleId] = {  /******/ // no module.id needed  /******/ // no module.loaded needed  /******/ exports: {}  /******/ };  /******/  /******/ // 傳入了module,exports和require  /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);  /******/  /******/ // 返回這個 exports  /******/ return module.exports;  /******/ }  /******/  /************************************************************************/  /******/ // 用於給exports添加屬性  /******/ (() => {  /******/ __webpack_require__.d = (exports, definition) => {  /******/ for(var key in definition) {                  // 檢測definition 是否有 key 並且 export 沒有這個值的時候會去進入判斷  /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {  /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });  /******/ }  /******/ }  /******/ };  /******/ })();  /******/              //檢測 obj 具有這個值prop嗎返回一個布爾值  /******/ (() => {  /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))  /******/ })();  /******/            // 設置export有__exmodule 屬性 並且有Symbol.toStringTag的值  /******/ (() => {  /******/ __webpack_require__.r = (exports) => {  /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {  /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });  /******/ }  /******/ Object.defineProperty(exports, '__esModule', { value: true });  /******/ };  /******/ })();  /******/  /************************************************************************/  /******/  /******/ // startup  /******/ // Load entry module and return exports  /******/ // This entry module can't be inlined because the eval devtool is used.  /******/ var __webpack_exports__ = __webpack_require__("./scr/index.js");  /******/  /******/ })() ;

分析

經過一些提煉在去看

​ (() => {    //1. var __webpack_modules__ = ({});     var __webpack_module_cache__ = {}; //2.    function __webpack_require__(moduleId) {} //3. (() => {__webpack_require__.d = (exports, definition) => {})(); //4. (() => {__webpack_require__.o = (obj, prop) => ()})(); //5. (() => {__webpack_require__.r = (exports) => {})();        //6.             var __webpack_exports__ = __webpack_require__("./scr/index.js"); })()

可以將其分成兩處

(()=>{    var __webpack_modules__ = ({});        function __webpack_require__(moduleId) {}    var __webpack_exports__ = __webpack_require__("./scr/index.js"); })()

__webpack_modules__ 存放了模塊名及源碼 我們在去調用 __webpack_require__("./scr/index.js") 這個入口函數然後就能開始執行。

來看看 __webpack_modules__ 存放了些什麼 {    "./scr/common.js": ((__unused_webpack_module, exports) => {eval(/*代碼*/);}),    "./scr/es.js": ((__unused_webpack_module, exports) => {eval(/*代碼*/);}),    "./scr/index.js": ((__unused_webpack_module, exports) => {eval(/*代碼*/);}), }

__webpack_modules__ 裏面存放的是自調用函數這些函數會往裏面傳入兩個參數。我們從第一個函數開始走也就是上文的var __webpack_exports__ = __webpack_require__("./scr/index.js"); 先開始執行它 找到 __webpack_modules__ 中的 "./scr/index.js" 所對應的子調用函數開始執行 先去看一下它 eval 了那些代碼

// index.js __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */   "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony import */ var _es__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./es */ "./scr/es.js"); ​ let co = __webpack_require__(/*! ./common */ "./scr/common.js"); co.sayHello(_es__WEBPACK_IMPORTED_MODULE_0__.name); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (_es__WEBPACK_IMPORTED_MODULE_0__["default"]); ​

這段代碼是將原文件的代碼轉化成 es5 後開始執行的 它調用了 那些函數呢?

  1. webpack_require.r()
  2. webpack_require.d()
  3. webpack_require()

在原文中使用 import _, { name } from './es';let co = require('./common');去讀取模塊都被轉化成了 __webpack_require__函數去調用

我們先來看一下 這個函數 __webpack_require__

function __webpack_require__(moduleId) {  /******/ var cachedModule = __webpack_module_cache__[moduleId];  /******/ if (cachedModule !== undefined) {  /******/ return cachedModule.exports;  /******/ }  /******/ // Create a new module (and put it into the cache)  /******/ var module = __webpack_module_cache__[moduleId] = {  /******/ // no module.id needed  /******/ // no module.loaded needed  /******/ exports: {}  /******/ };  /******/  /******/ // 傳入了module,exports和require  /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);  /******/  /******/ // 返回這個 exports  /******/ return module.exports;  /******/ }

看起來它最主要的就是幹了兩件事 一個是去調用 另一個是去返回 exports 對象。

function __webpack_require__ (moduleID){ //....    var module = __webpack_module_cache__[moduleId] = {exports: {}}    // 調用    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);    // 返回exports    return module.exports }

這麼一來就通順了 __webpack_require__ 去定義了一個對象將這個對象和這個函數傳到要eval執行的裏面然後去調用就行了在eval函數執行時候碰到 __webpack_require__就會去查找 __webpack_modules__中入口文件依賴文件響應的代碼然後在執行碰到 exports 就會將值綁定到 exports中 執行完函數後返回。(可見整個函數是迭代的形式調用的)

前面還有兩個工具函數

  1. webpack_require.r()
  2. webpack_require.d()

前者用於設置 exports 對象上具有 __exmodule 屬性 後者用於給 exports 對象添加屬性

``` // 用於給exports添加屬性 (() => { webpack_require.d = (exports, definition) => { for(var key in definition) {         // 檢測definition 是否有 key 並且 export 沒有這個值的時候會去進入判斷 if(webpack_require.o(definition, key) && !webpack_require.o(exports, key)) { Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); } } }; })();

//檢測 obj 具有這個值prop嗎返回一個布爾值 (() => { webpack_require.o = (obj, prop) =>(Object.prototype.hasOwnProperty.call(obj, prop)) })();

// 設置export有__exmodule 屬性 並且有Symbol.toStringTag的值 (() => { webpack_require.r = (exports) => { if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })       }            Object.defineProperty(exports, '__esModule', { value: true }); }; })(); ```

根據這樣的代碼可以推測它的組裝代碼的流程。也可以去參考着寫一個簡單的打包器。

import() 後的代碼

首先和上述的配置但是 index.js 的代碼不同

// index.js import(/* webpackChunkName: "es" */ './es.js') .then((val => console.log(val))) //es.js export const name = "前端事務所"; export default "ESModule";

通過打包後來看一下文件夾下 index.js 和 es.js 的內容 我們這裏先只看 入口文件的代碼是什麼樣子

__webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */   "name": () => (/* binding */ name), /* harmony export */   "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); ​ //index.js 入口文件 __webpack_require__.e(/*! import() | es */ "es").then(__webpack_require__.bind(__webpack_require__, /*! ./es.js */ "./scr/es.js")).then((val => console.log(val))) ​ //es.js const name = "前端事務所"; /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("ESModule");

沒什麼好解釋的 還是和上述一樣 使用了__webpack_require__.r(__webpack_exports__);來讓 exports 具有 __exMode 屬性 然後使用 __webpack_require__.d函數對 exports 對象綁定屬性,這裏關鍵的步驟在於 __webpack_require__.e()函數,可見它還是一個 Promise 對象。我們來看一下整個函數

(() => { __webpack_require__.f = {}; __webpack_require__.e = (chunkId) => { return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => { __webpack_require__.f[key](chunkId, promises); return promises; }, [])); }; })();

調用整個可以返回一個 Promise 對象

__webpack_require__.f = {    j: function(){} } Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => { __webpack_require__.f[key](chunkId, promises);    // 相當於是調用 __webpack_require__.f.j(chunkId, [])    // 從主文件夾又可以得出 chunkID = "es" return promises; }, []));

那這裏就是將存放在 __webpack_require__.f對象的值依次取出並調用傳入(chunkID, promises) 第一次的 promises 是一個數組,之後會將上一次返回的 promises在傳入下一個的參數promises中 主要看這個調用的函數有沒有對這個數組有沒有什麼操作。

我們在來找一下關於 __webpack_require__.f的東西 這裏只看到了 __webpack_require__.f.j的函數

(function(){    var installedChunks = {"main": 0}; ​     __webpack_require__.f.j = (chunkId, promises) => { ​ var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined; ​ if(installedChunkData !== 0) {   if(installedChunkData) {   promises.push(installedChunkData[2]);   } else {   if(true) { // all chunks have JS                // installedChunkData = installedChunks = {"main": [resolve, reject]} 也就是 [resolve, reject]   var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));   // promise = [resolve, reject]                 promises.push(installedChunkData[2] = promise);                // 文件路徑 + 打包後的文件名 找到入口文件的索引(chunkId)   var url = __webpack_require__.p + __webpack_require__.u(chunkId); ​   var error = new Error();   var loadingEnded = (event) => {                   // installedChunks = {"main": 0, "es": [resolve, reject, promise]}   if(__webpack_require__.o(installedChunks, chunkId)) {   installedChunkData = installedChunks[chunkId];   if(installedChunkData !== 0) installedChunks[chunkId] = undefined;   if(installedChunkData) {   var errorType = event && (event.type === 'load' ? 'missing' : event.type);   var realSrc = event && event.target && event.target.src;   error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';   error.name = 'ChunkLoadError';   error.type = errorType;   error.request = realSrc;   installedChunkData[1](error);   }   }   };   __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);   } else installedChunks[chunkId] = 0;   }   }   };   })()

由於這裏使用到了var url = __webpack_require__.p + __webpack_require__.u(chunkId); 來分析一下這個函數是做什麼用的 (() => { var scriptUrl; if (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + ""; var document = __webpack_require__.g.document; if (!scriptUrl && document) { if (document.currentScript) scriptUrl = document.currentScript.src if (!scriptUrl) { var scripts = document.getElementsByTagName("script"); if(scripts.length) scriptUrl = scripts[scripts.length - 1].src } } if (!scriptUrl) throw new Error("Automatic publicPath is not supported in this browser"); scriptUrl = scriptUrl.replace(/#.*$/, "").replace(/?.*$/, "").replace(//[^/]+$/, "/"); __webpack_require__.p = scriptUrl; })();

返回一個查找 <\script>標籤的文件名的目錄

迴歸上文然後又調用了一個 __webpack_require__.l 的函數 以此來創建一個 <\script> 的標籤並將這個主文件夾依賴的文件存放到html中。前提是沒有這個 script 標籤的情況下 有的話會忽略。我掛載的是 es這個模塊在 html 文件中 而且是看不到的通過F12可以去查看到文件確實是被主要獲取。

我一旦掛載之後就會使用這個文件來看看這個文件的內容有那些

js (self["webpackChunkwebpack_demo_two"] = self["webpackChunkwebpack_demo_two"] || []).push([["es"],{ ​ /***/ "./scr/es.js": /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { ​ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   "name": () => (/* binding */ name),\n/* harmony export */   "default": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n//es.js\r\nconst name = "前端事務所";\r\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("ESModule");\n\n//# sourceURL=webpack://webpack-demo-two/./scr/es.js?"); ​ /***/ }) ​ }]);

這裏使用了一個 self["webpackChunkwebpack_demo_two"] 這個self是存在與瀏覽器中的並指向於 window 會添加一個屬性 webpackChunkwebpack_demo_two值為[[["es"], {"./scr/es.js": (()=>{})()}]]這樣的東西然後在主文件下又會使用這樣的代碼

var chunkLoadingGlobal = self["webpackChunkwebpack_demo_two"] = self["webpackChunkwebpack_demo_two"] || []; chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0)); chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));

可以看成是這樣

chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0)); //先調用一遍 為每一個值都傳入了一個值 0 由於第一次的數組是空的所以沒有什麼反應 chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0)) // 創建了一個函數,併為webpackJsonpCallback傳入了一個參數 a.push =  webpackJsonpCallback.bind(null,a.push.bind(chunkLoadingGlobal)) ​ // 之前是再 es 文件調用了這個push 方法 (self["webpackChunkwebpack_demo_two"] = self["webpackChunkwebpack_demo_two"] || []).push // 很明顯的傳入了 [["es"], {}] 這裏我們簡單的寫一下格式明白意思就行

由於進入到 webpackJsonpCallback 的函數再來看看這個函數做了什麼

``` var webpackJsonpCallback = (parentChunkLoadingFunction, data) => { var [chunkIds, moreModules, runtime] = data; // add "moreModules" to the modules object, // then flag all "chunkIds" as loaded and fire callback var moduleId, chunkId, i = 0;    // installedChunks = {"main": 0, "es": [resolve, reject, promise]} 這裏就通過了 if(chunkIds.some((id) => (installedChunks[id] !== 0))) {                // moreModules是一個對象 這個對象裏面包裹着自調用函數 這個函數又是生成 em 模塊的代碼 for(moduleId in moreModules) {                    // 檢測是否是它的屬性 那必須是啊 if(webpack_require.o(moreModules, moduleId)) {                        // 將 webpack_require.m["em"] = 前面的那個自調用函數 webpack_require.m[moduleId] = moreModules[moduleId]; } } if(runtime) var result = runtime(webpack_require); } if(parentChunkLoadingFunction) parentChunkLoadingFunction(data); for(;i < chunkIds.length; i++) { chunkId = chunkIds[i]; if(webpack_require.o(installedChunks, chunkId) && installedChunks[chunkId]) {                    // 開始調用 resolve()觸發Promise 開關然後 installedChunks[chunkId]0; }                 installedChunks[chunkIds[i]] = 0; }

    }

```

開始獲取 var [chunkIds, moreModules, runtime] = data;這裏的 data 可以看作是我們剛才傳入的東西[["es"], {}]

webpackJsonpCallback進入後觸發了 promise .resolve() 然後開始 執行 em 後續的代碼 // em __webpack_require__.e(/*! import() | es */ "es").then(__webpack_require__.bind(__webpack_require__, /*! ./es.js */ "./scr/es.js")).then((val => console.log(val)))

現在就開始調用了 __webpack_require__()之前再__webpack_require__.m[moduleId] = moreModules[moduleId];也就是在模塊中 __webpack_require__.m添加了 '"./scr/es.js"' 屬性且值為這個模塊自調用函數的生成代碼

__webpack_require__.m = __webpack_modules__;

最後這一節內容有些複雜 我自己看的也費勁 不過我也總算是理解了並且也明白了看源碼要有一個粗大的神經是必不可少的。很多東西都是要靠自己去猜然後聯繫最後在證明。這就是不使用調試工具的壞處,不過我們總算也熬過來了。

文字看起來也非常費勁我畫一個圖大家來研究研究。

注意: webpack版本 V5.52.1

runtimeInput_demo.png

最後

我相信能夠完整閲讀下來的人收穫一定滿滿,技術文章並不是能吃速食就行的,潛下心來安靜的閲讀完,大家對於webpack 的理解也會更上一層樓。最後也十分推薦大家能使用 vscode 的調試工具來調試一遍!

參考

webapck模塊化必讀(8千字長文!!) -【webpack系列】