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系列】