webpack5的Runtime代码浅析

2,053 阅读9分钟

阅读须知:本篇内容涵盖了非常多的代码不建议粗略的阅读它应该去亲自的调试代码仔细的去看,我想这样才能有所收获,本文就是带着大家明白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可以去查看到文件确实是被主要获取。

我一旦挂载之后就会使用这个文件来看看这个文件的内容有那些

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