Webpack5 模块加载原理

Webpack 模块加载原理

webpack 是一个模块打包器,它通过编译打包,将 ESM 和 CJS 语法转换为 webpack 加载模块的规范,这种方式在浏览器上是可以直接使用的,通俗理解就是打包成了通用的普通对象

CommonJS 规范

打包前代码

scss 复制代码
// index.js
const test = require('./test')
​
function demo() {}
​
demo()
test()
​
// test.js
function test() {}
​
module.exports = test

打包后代码,较为简单,解释在源码。

javascript 复制代码
(() => {
  // 打包后的原始模块放在这里,用于第一次读取
  var __webpack_modules__ = {
    './src/test.js': (module) => {
      function test() {}
      module.exports = test;
    },
  };
​
  // 缓存模块,用于第二次读取
  // 可能显得多余,但对于大项目来说可以做到利用空间换时间的
  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: {},
    });
​
    // Execute the module function
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
​
    // Return the exports of the module
    return module.exports;
  }
​
  var __webpack_exports__ = {};
​
  // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
  (() => {
    const test = __webpack_require__('./src/test.js');
​
    function demo() {}
​
    demo();
    test();
  })();
})();
​

ESM 规范

打包前代码

javascript 复制代码
// index.js
import test,{a} from "./test"
function demo() {
   console.log('demo',a);
   // import("./dynamicImport.js")
}
​
demo()
test()
​
// test.js
function test() {
    console.log("test");
}
​
export const a ="hello"
export default test

打包后代码,较为简单,解释在源码。

ESM 其实和 CJS 差不多,但是加载模块的时候,多了一个表明这是一个ESM模块的标志

  • 能够与其他模块加载器,或者工具交互,站在库开发者的角度,我觉得有点用的,起码得有。
  • 模块识别,一些插件,工具,能够读取到这些配置就能判断是 esm 模块还是 cjs 模块
javascript 复制代码
(() => {
  'use strict';
  var __webpack_modules__ = {
    './src/test.js': ( __unused_webpack_module,__webpack_exports__, __webpack_require__) => {
        __webpack_require__.r(__webpack_exports__);
        /* harmony export */ __webpack_require__.d(__webpack_exports__, {
        /* harmony export */ a: () => /* binding */ a,
        /* harmony export */ default: () => __WEBPACK_DEFAULT_EXPORT__,
        /* harmony export */
      });
      function test() {
        console.log('test');
      }
​
      const a = 'hello';
​
      /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = test;
    },
​
  };
​
  // 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: {},
​
    });
​
    // Execute the module function
    __webpack_modules__[moduleId](module,module.exports,__webpack_require__);
​
    // Return the exports of the module
    return module.exports;
​
  }
​
  /* webpack/runtime/define property getters */
  (() => {
    // define getter functions for harmony exports
    __webpack_require__.d = (exports, definition) => {
      for (var key in definition) {
        if (
          __webpack_require__.o(definition, key) &&
          !__webpack_require__.o(exports, key)
        ) {
          Object.defineProperty(exports, key, {
            enumerable: true,
            get: definition[key],
          });
        }
      }
    };
  })();
​
  /* webpack/runtime/hasOwnProperty shorthand */
  (() => {
    __webpack_require__.o = (obj, prop) =>
      Object.prototype.hasOwnProperty.call(obj, prop);
  })();
​
  /* webpack/runtime/make namespace object */
  (() => {
    // define __esModule on exports
    // 猜测两种好处,能够与其他模块加载器,或者工具交互,站在库开发者的角度,我觉得有点用的,起码得有。
    // 模块识别,一些插件,工具,能够读取到这些配置就能判断是 esm 模块还是 cjs 模块
    __webpack_require__.r = (exports) => {
      // console.log(  Object.prototype.toString.call(exports)) ==> "[object Module]"
      //  Object.getOwnPropertyDescriptors(exports)
      if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, {
          value: 'Module',
        });
      }
      Object.defineProperty(exports, '__esModule', { value: true });
    };
  })();
​
  var __webpack_exports__ = {a:1};
  // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
  (() => {
    __webpack_require__.r(__webpack_exports__);
    /* harmony import */ var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__('./src/test.js');
​
    function demo() {
      console.log('demo', _test__WEBPACK_IMPORTED_MODULE_0__.a);
    }
​
    demo();
    (0, _test__WEBPACK_IMPORTED_MODULE_0__['default'])();
  })();
​
})();
​
​

按需加载

按需加载,也叫异步加载、动态导入,即只在有需要的时候才去下载相应的资源文件。

在 webpack 中可以使用 importrequire.ensure 来引入需要动态导入的代码,例如下面这个示例:

打包前代码

scss 复制代码
function demo() {
   console.log('demo');
}
​
demo()
// webpack 会将 dynamicImport 打包到独立的 chunk  当加载到这个模块的时候,就会自动加载
// 如果这个模块还在其他模块会如用,webpack 会引用缓存
import("./dynamicImport.js")
​
​

打包后代码

javascript 复制代码
 (() => {
  // 1
  var __webpack_modules__ = ({});
  // The module cache
  // 2
  var __webpack_module_cache__ = {};
​
  // The require function
  function __webpack_require__(moduleId) {
    debugger
    // 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: {}
    };
​
    // Execute the module function
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
​
    // Return the exports of the module
    return module.exports;
  }
​
  // expose the modules object (__webpack_modules__)
  // 3
  __webpack_require__.m = __webpack_modules__;
​
  /* webpack/runtime/define property getters */
  // 4
  (() => {
    // define getter functions for harmony exports
    __webpack_require__.d = (exports, definition) => {
      for(var key in definition) {
        if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
          Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
        }
      }
    };
  })();
​
  /* webpack/runtime/ensure chunk */
  // 5
  (() => {
    __webpack_require__.f = {};
    // This file contains only the entry chunk.
    // The chunk loading function for additional chunks
    __webpack_require__.e = (chunkId) => {
      return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
        __webpack_require__.f[key](chunkId, promises);
        return promises;
      }, []));
    };
  })();
​
  /* webpack/runtime/get javascript chunk filename */
  // 6
  (() => {
    // This function allow to reference async chunks
    __webpack_require__.u = (chunkId) => {
      // return url for filenames based on template
      return "static/js/" + chunkId + "." + "52b72416" + ".chunk.js";
    };
  })();
​
  /* webpack/runtime/global */
  // 7
  (() => {
    __webpack_require__.g = (function() {
      if (typeof globalThis === 'object') return globalThis;
      try {
        return this || new Function('return this')();
      } catch (e) {
        if (typeof window === 'object') return window;
      }
    })();
  })();
​
  /* webpack/runtime/hasOwnProperty shorthand */
  // 8
  (() => {
    __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
  })();
​
  /* webpack/runtime/load script */
  // 9
  (() => {
    var inProgress = {};
    var dataWebpackPrefix = "webpack-demo:";
    // loadScript function to load a script via script tag
    __webpack_require__.l = (url, done, key, chunkId) => {
      if(inProgress[url]) { inProgress[url].push(done); return; }
      var script, needAttach;
      if(key !== undefined) {
        var scripts = document.getElementsByTagName("script");
        for(var i = 0; i < scripts.length; i++) {
          var s = scripts[i];
          if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; }
        }
      }
      if(!script) {
        needAttach = true;
        script = document.createElement('script');
​
        script.charset = 'utf-8';
        script.timeout = 120;
        if (__webpack_require__.nc) {
          script.setAttribute("nonce", __webpack_require__.nc);
        }
        script.setAttribute("data-webpack", dataWebpackPrefix + key);
​
        script.src = url;
      }
      inProgress[url] = [done];
      var onScriptComplete = (prev, event) => {
        // avoid mem leaks in IE.
        script.onerror = script.onload = null;
        clearTimeout(timeout);
        var doneFns = inProgress[url];
        delete inProgress[url];
        // script.parentNode && script.parentNode.removeChild(script);
        doneFns && doneFns.forEach((fn) => (fn(event)));
        if(prev) return prev(event);
      }
      var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
      debugger
      script.onerror = onScriptComplete.bind(null, script.onerror);
      script.onload = onScriptComplete.bind(null, script.onload);
      needAttach && document.head.appendChild(script);
    };
  })();
​
  /* webpack/runtime/make namespace object */
  // 10
  (() => {
    // define __esModule on exports
    __webpack_require__.r = (exports) => {
      if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
      }
      Object.defineProperty(exports, '__esModule', { value: true });
    };
  })();
​
  /* webpack/runtime/publicPath */
  // 11
  (() => {
    var scriptUrl;
    if (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + "";
    var document = __webpack_require__.g.document;
    if (!scriptUrl && document) {
      // 当前的入口文件的dom  <script src="./dist/static/js/main.js"></script>
      if (document.currentScript)
        scriptUrl = document.currentScript.src;
      console.log(document.currentScript.src,"document.currentScript.src");
      if (!scriptUrl) {
        var scripts = document.getElementsByTagName("script");
        if(scripts.length) {
          var i = scripts.length - 1;
          while (i > -1 && !scriptUrl) scriptUrl = scripts[i--].src;
        }
      }
    }
    // When supporting browsers where an automatic publicPath is not supported you must specify an output.publicPath manually via configuration
    // or pass an empty string ("") and set the __webpack_public_path__ variable from your code to use your own logic.
    if (!scriptUrl) throw new Error("Automatic publicPath is not supported in this browser");
    scriptUrl = scriptUrl.replace(/#.*$/, "").replace(/?.*$/, "").replace(//[^/]+$/, "/");
    __webpack_require__.p = scriptUrl + "../../";
  })();
​
  /* webpack/runtime/jsonp chunk loading */
  // 12
  (() => {
    // no baseURI
​
    // object to store loaded and loading chunks
    // undefined = chunk not loaded, null = chunk preloaded/prefetched
    // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
    var installedChunks = {
      "main": 0
    };
​
    __webpack_require__.f.j = (chunkId, promises) => {
        // JSONP chunk loading for javascript
        var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;
        if(installedChunkData !== 0) { // 0 means "already installed".
​
          // a Promise means "currently loading".
          if(installedChunkData) {
            promises.push(installedChunkData[2]);
          } else {
            if(true) { // all chunks have JS
              // setup Promise in chunk cache
              var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));
              promises.push(installedChunkData[2] = promise);
​
              // start chunk loading
              var url = __webpack_require__.p + __webpack_require__.u(chunkId);
              // create error before stack unwound to get useful stacktrace later
              var error = new Error();
              var loadingEnded = (event) => {
                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);
            }
          }
        }
    };
​
    // no prefetching
​
    // no preloaded
​
    // no HMR
​
    // no HMR manifest
​
    // no on chunks loaded
​
    // install a JSONP callback for chunk loading
    var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
      debugger
      console.log(parentChunkLoadingFunction,"parentChunkLoadingFunction");
      console.log(data,"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;
      if(chunkIds.some((id) => (installedChunks[id] !== 0))) {
        for(moduleId in moreModules) {
          if(__webpack_require__.o(moreModules, moduleId)) {
            __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]) {
          console.log(installedChunks[chunkId],chunkId,"installedChunks[chunkId][0]");
          debugger
          installedChunks[chunkId][0]();
        }
        installedChunks[chunkId] = 0;
      }
​
    }
    debugger
    // 这里是 jsonp 注意点,当初始化逻辑执行后,会注册这里的回调方法,重写self["webpackChunkwebpack_demo"]的 push 方法
    // self["webpackChunkwebpack_demo"] 创建一个[],重写里面的 push 方法,然后留给异步模块去执行这个 push 方法就可以把参数带回来
    // push 方法重写为 webpackJsonpCallback,接收到动态模块代码
    // script 标签请求 动态模块文件,回来后就会触发这里的方法,它会把 push 执行,并把动态模块代码返回。
    // 触发 webpackJsonpCallback 方法,installedChunks[chunkId][0]() 把 resolve 返回,那么就会触发 then方法,接着就会触发 __webpack_require__.e 把模块加载,并缓存起来。
    var chunkLoadingGlobal = self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || [];
    chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
    chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
  })();
​
  // 13
  var __webpack_exports__ = {};
  function demo() {
  console.log('demo');
  }
​
  // 14
  demo()
​
  // 15
  __webpack_require__.e("src_dynamicImport_js").then(__webpack_require__.bind(__webpack_require__,"./src/dynamicImport.js"))
​
​
 })()
​
/************************************************************************************/
// 以下为动态模块代码
'use strict';
debugger;
(self['webpackChunkwebpack_demo'] =
  self['webpackChunkwebpack_demo'] || []).push([
  ['src_dynamicImport_js'],
  {
    './src/dynamicImport.js': (
      __unused_webpack_module,
      __webpack_exports__,
      __webpack_require__
    ) => {
      __webpack_require__.r(__webpack_exports__);
      /* harmony export */ __webpack_require__.d(__webpack_exports__, {
        /* harmony export */ default: () => __WEBPACK_DEFAULT_EXPORT__,
        /* harmony export */
      });
      function dynamicImport() {
        console.log('dynamicImport');
      }
​
      /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ =
        dynamicImport;
    },
  },
]);
​

按需加载逻辑还是较为复杂,逻辑比较绕,以下是详细解析。

源码详细分析

  1. 首先初始化,大概逻辑和 cjs 、esm 大概相似,但是处理异步的逻辑。

    • 自执行函数初始化逻辑,都是围绕 webpack_require 添加变量或函数

    • 注意一下,push 方法重写

      ini 复制代码
      // 这里是 jsonp 注意点,当初始化逻辑执行后,会注册这里的回调方法,重写self["webpackChunkwebpack_demo"]的 push 方法
      // self["webpackChunkwebpack_demo"] 创建一个[],重写里面的 push 方法,然后留给异步模块去执行这个 push 方法就可以把参数带回来
      // push 方法重写为 webpackJsonpCallback,接收到动态模块代码
      // script 标签请求 动态模块文件,回来后就会触发这里的方法,它会把 push 执行,并把动态模块代码返回。
      // 触发 webpackJsonpCallback 方法,installedChunks[chunkId][0]() 把 resolve 返回,那么就会触发 then方法,接着就会触发 __webpack_require__.e 把模块加载,并缓存起来。
      var chunkLoadingGlobal = self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || [];
      chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
      chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
    arduino 复制代码
    _webpack_require__.e("src_dynamicImport_js").then(__webpack_require__.bind(__webpack_require__, /*! ./dynamicImport.js */ "./src/dynamicImport.js"))
    ​
    // 同步代码逻辑,then 后面的异步代码后面补充。
    _webpack_require__.e("src_dynamicImport_js")
    ​
    会去到 __webpack_require__.f.j 注册 promise,使模块加载完毕后,异步调用。
    接着调用 __webpack_require__.l,这里会去创建 script 标签加载模块,模块加载完 12s 后删除script
    等待动态模块加载后,会自动执行,那么就会去到 webpackJsonpCallback 的逻辑,它会把 promise resolve。
    回到 _webpack_require__.e("src_dynamicImport_js").then 的方法中,接着触发 __webpack_require__ 方法,就正常的加载模块,把模块放到缓存里面

总结

CJS 加载模块,从 __webpack_modules__ 把模块代码读取 __webpack_module_cache__

ESM 加载模块,从 __webpack_modules__ 把模块代码读取 __webpack_module_cache__ 中,并对模块类型,原型对象标识上是一个 ESM 模块

按需加载,通过 JSONP技术实现,本质是通过重写["webpackChunkwebpack_demo"].push 方法,当 script 方法 加载完毕就会执行 push 方法,接着把模块代码传递到 webpackJsonpCallback 中进行处理,把代码传递到 __webpack_require__ 中,把模块代码读取 __webpack_module_cache__ 中,并对模块类型,原型对象标识上是一个 ESM 模块

相关推荐
Domain-zhuo3 分钟前
Git和SVN有什么区别?
前端·javascript·vue.js·git·svn·webpack·node.js
雪球不会消失了8 分钟前
SpringMVC中的拦截器
java·开发语言·前端
李云龙I19 分钟前
解锁高效布局:Tab组件最佳实践指南
前端
m0_7482370523 分钟前
Monorepo pnpm 模式管理多个 web 项目
大数据·前端·elasticsearch
JinSoooo26 分钟前
pnpm monorepo 联调方案
前端·pnpm·monorepo
m0_7482449634 分钟前
【AI系统】LLVM 前端和优化层
前端·状态模式
明弟有理想34 分钟前
Chrome RCE 漏洞复现
前端·chrome·漏洞·复现
平行线也会相交35 分钟前
云图库平台(二)前端项目初始化
前端·vue.js·云图库平台
shimmer00837 分钟前
抖音小程序登录(前端通过tt.login获取code换取openId)
前端·小程序·状态模式
嘤嘤怪呆呆狗1 小时前
【开发问题记录】使用 Docker+Jenkins+Jenkins + gitee 实现自动化部署前端项目 CI/CD(centos7为例)
前端·vue.js·ci/cd·docker·gitee·自动化·jenkins