jingzhiMo.github.io icon indicating copy to clipboard operation
jingzhiMo.github.io copied to clipboard

浅析 webpack 5 module federation 加载构成

Open jingzhiMo opened this issue 3 years ago • 0 comments

前言

这篇文章主要是调研 module federation的时候. 对 webpack 异步加载代码分割文件与加载远端组件的流程简述.前半部分是流程与部分代码分析, 后半部分是webpack代码的注释笔记.

介绍

Webpack 5 新增一个 module federation 的特性, 详情可以看 官方文档. 这个特性大概的作用是:

多个独立的构建可以组成一个应用程序,这些独立的构建之间不应该存在依赖关系,因此可以单独开发和部署它们。

这通常被称作微前端,但并不仅限于此。

例如在 A 应用上运行的 组件 FOO , B 应用也会有类似的需求, 需要复用组件 FOO , 通常我们可能会把 FOO 组件抽离到一个 npm 包, 然后发布到 npm 平台, 那么在 A, B 两个应用都进行引入对应的 npm 包, 后续的维护,会独立在 npm 包中进行发布. 如果 npm 包发生更新, 那么对应 A, B 两个应用都进行重新打包. 这一种维护组件的模式,会有点在“编译时”的味道.

module federation 的插件主要针对应用“运行时”. 就如上面的例子, A 应用可以通过 webpack 配置 ModuleFederationPlugin的插件, 在构建A应用的时候, 把 FOO 组件顺便打包为一个远端组件包,通常为 remoteEntry.js. B 应用想使用 FOO 组件, 则通过在入口 html 文件中, 引入 A 应用地址下的remoteEntry.js文件(这一步通常也由ModuleFederationPlugin 完成 ), 并通过 import 关键词引入即可. 后续FOO组件的发布与维护,都在 A 应用进行处理. A 发布新的包, B应用可以不重新构建代码,就可以引用最新的代码. 简单如下图所示:

image

但是要有一个前提条件,A, B两个应用都是使用 webpack 5 来打包, 否则无法识别,

调研例子

看到这个远端加载组件的方法还挺有趣,于是乎想看一下具体的实现.后面会贴一些源代码的实现.后续的所有例子,都是基于 module-federation-examples/basic-host-remote 例子来调研.

分析

我们先了解一下 webpack 打包出来的产物的两个概念:

  1. chunk

  2. module

chunk 是文件级别, 利用 Code Splitting 分割的代码,每个文件都是一个 chunk, 通常一个 chunk 中包含一个或者多个 module. 而实际webpack代码运行的时候, 是根据 module id 进行定位.

文件源码结构分析

这里对三种文件类型文件源码结构进行分析:

  1. webpack 打包出来的主文件
  2. Code Splitting 分割的 chunk 文件
  3. remoteEntry 文件 (由 ModuleFederationPlugin 插件生成的文件)

会先从普通 Code Splitting 的文件加载说起, 然后对 remoteEntry 类文件进行分析.

主文件的结构如下:

(() => {
  // 定义变量
	var __webpack_modules__ = {}
	var __webpack_module_cache__ = {}
	
	function __webpack_require__() {}
  
  // 往 __webpack_require__ 对象挂载各种数据、函数等
  __webpack_require__.m = __webpack_modules__
  __webpack_require__.n = function() {}
  // ...
  
  // 定义 jsonp 的回调函数,后面会说到:
  function webpackJsonpCallback() {}
  
  // 对指定对象数组的 push 方法进行劫持, 后面会说到
  var chunkLoadingGlobal = self["webpackChunk_basic_host_remote_app1"] = self["webpackChunk_basic_host_remote_app1"] || [];
	chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
	chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
  
  // 加载并执行对应模块
  Promise.all([
    __webpack_require__.e(558), 
    __webpack_require__.e(165)
  ]).then(__webpack_require__.bind(__webpack_require__, 165))
})()

从主文件结构开始分析, 在 Promise.all函数执行之前, 都是一些变量与函数的定义, 正式启动是调用 __webpack_require__.e 接着是 __webpack_require__ 函数的执行.

__webpack_require__.e 这种挂载的函数或者变量很多,前缀很长...后面会用__.e 来代替

__.e函数,是一个统一异步加载 chunk 的入口,

__webpack_require__ 是一个对 module 进行引入的工具函数, 如果第一次执行,还会对该 module 进行执行, 返回内容挂载在 exports 中.

所以主文件的处理方式就是: 加载 chunk id 为558, 165 的 chunk 文件, 然后执行 module id 为 165 的 module.

那么问题来了, moduleId: 165 是位于 异步加载的 chunkId: 165 中, 怎样可以让局部变量 __webpack_require__来加载呢?

Code Splitting文件结构如下:

(self["webpackChunk_basic_host_remote_app1"] = self["webpackChunk_basic_host_remote_app1"] || []).push(
	[165], // 这是这个文件对应的 chunkId
  // 以下这两个是 moduleId, 对应的模块内容的函数
  {
    165: () => {},
    408: () => {}
  }
)

被代码分割出来的文件比较简单, 直接往 webpackChunk_basic_host_remote_app1 的全局变量数组push 两个变量[165, { 165: '', 408: 'xx'}], 该类型文件并没有一些调用的函数.

这里的关键点, 在于主文件对变量 webpackChunk_basic_host_remote_app1 的 push 函数进行了劫持:

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

当该全局变量数组执行 push 函数的之后, 并不是执行正常数组把数据添加到数组, 而是执行 webpackJsonpCallback函数, 这个函数的作用是负责把对应 chunk 标识为已加载成功, 并把加载到的 chunk 中的 module, 逐个注册到 __.m 变量中, 这样子后续主文件的代码,就能够拿到异步加载的 module. 简要如下图:

image

remoteEntry 文件结构如下:

// 定义全局变量, 这个全局变量是在 ModuleFederationPlugin 插件中定义
var app2; app2 = (() => {
  // 这个函数的内容,与主文件类似, 定义一部分变量
  var __webpack_modules__ = {
    677: () => {
      // ... 还有其他代码
      __webpack_require__.d(exports, {
        // get 方法是用于, 根据组件名称获取相关组件内容
        get: () => get,
        // init 方法是用于初始化当前分离打包组件需要依赖包的版本这部分
        init: () => init
      });
    }
  }
	var __webpack_module_cache__ = {}
	
	function __webpack_require__() {}
  
  // 后续就不详细展开,也有定义 jsonp 的回调函数等
  function webpackJsonpCallback() {}
  
  // 与主文件不一致的地方, 给全局变量返回对应的数据
  return __webpack_require__(677);
})()

module federation 插件打包出来的 remoteEntry 的文件结构,与普通代码分割的 chunk 文件不一致, 所以不能用类似的方法进行加载, remoteEntry 类文件加载会相对复杂一点点.

这里需要看一下 __.e 函数的作用, 之前说过, __.e 函数是用于加载异步的 chunk, 而实际上, 加载异步的 chunk 会有几种类型, 这几种类型分别都有独自的处理方法, 分别是:

  • __.f.j 处理普通代码分割的 chunk
  • __.f.remotes 处理需要从远端获取的组件
  • __.f.consumes 处理加载 remoteEntry 相关 chunk

__.e 只是一个壳, 每次执行,都会依次把这三个函数执行一遍. 这三种处理方法,通常如何辨别, 一个 chunk id 过来,是否符合当前处理的类型?

假设一个 chunkId: 165, 的文件需要加载, 这种文件只是一种普通的代码分割的 chunk, __.f.remotes & __.f.consumes 这两个函数不需要实际上发起请求. 针对这种情况, 处理函数通常会维护一份 chunkMapping, 通过判断 chunkId 在 chunkMapping 来确认继续, 例如

// __.f.consumes
const chunkMapping = {
  160: xx
}

对于__.f.consumes 函数来说,只有 chunkId: 160, 是有效的, 对于其他无效 chunkId, 会跳过实际处理环节.

说完 __.f相关函数的加载简要描述, 接下来说 remoteEntry 的加载过程:

  1. __.f.consumes 执行, 对module federation插件配置的 share 相关包进行版本注册(通常是一些公共基础包, 例如 react, react-dom 等). 配置 share, 可以减少重复加载基础包
  2. 加载对应的 remoteEntry.js 文件, 根据插件配置的全局变量, 获取到 remoteEntry 暴露的数据
  3. 调用 remoteEntry 暴露的 init 方法(上述: remoteEntry 文件结构中的 init 函数), 把主应用的公共基础包与 remoteEntry 基础包的版本进行对比, 根据x.y.z版本号的方式, 看双方版本是否适配. 如果适配, 加载同一份公共基础包, 否则, 各自加载.
  4. 当应用中, 有需要用到 remoteEntry 中的组件, 则会调用这一步, __.f.remotes
  5. __.f.remotes 加载对应远端组件, 调用 remoteEntry 暴露的 get 方法(上述: remoteEntry 文件结构中的 get 函数), 根据组件名称,获取到对应的组件, 挂载到 __webpack_modules__ 变量下, 后续会被 __webpack_require__ 方法所使用

下图对版本处理,做了简化,只显示加载与调用的过程

image

remoteEntry 类的文件, 也需要使用全局变量做为其中一个中介来传递数据, 与webpackJsonpCallback有一部分相同之处, 但是 remoteEntry 多了版本的判断, 这一部分其实非常复杂,上面只是简要对过程进行了分析, 远端版本控制没有做深入的讲解.

小结

webpack实现异步加载的方法都很巧妙,无论是利用劫持全局变量方法,还是通过“伪”全局变量来做数据中转.函数设计分工精细. 例如在 __.e__.f.j & __.f.remotes & __f.consumes 之间的联动, 还是底层工具方法的定义, 对日常开发思路都有比较不错的参考.后续的部分,是在调研过程中,对部分代码的分析做的一部分笔记, 也做为简要的 api 文档来查阅. 需要可以往后查看. (完)

部分代码实现

__webpack_require__ 代码实现

function __webpack_require__(moduleId) {
	// 判断该模块是否已经缓存,已缓存直接返回该模块
  if(__webpack_module_cache__[moduleId]) {
    return __webpack_module_cache__[moduleId].exports;
  }
  // 没有缓存,根据 moduleId 创建一个缓存模块
  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__);
  
  // 执行完毕之后,module 这个对象就被挂上目标模块了,
  // 因为module对象内存地址是同一个,在执行模块的时候,已被赋值
  return module.exports;
}

__webpack_require__.o

// hasOwnProperty 的简写
__webpack_require__.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop)

__webpack_require__.m

// 获取 __webpack_modules__ 对象
__webpack_require__.m = __webpack_modules__

__webpack_require__.d

// 对 webpack 的模块进行数据劫持,类似 vue 的数据劫持
// 但是直接获取模块的值的时候进行劫持,不会对 set 进行赋值
// 能够保证模块暴露(module.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_require__.g

// 返回当前的全局变量,例如 nodeJs 中的 globalThis、浏览器中的 window 变量
__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_require__.n

// 兼容获取 只进行 export default 的es模块打包,提取 export default 的模快
// 通常是针对引入的模块的获取进行劫持
// 但是这里 对 getter 的 a 变量的 get 劫持,不是十分了解
__webpack_require__.n = (module) => {
  var getter = module && module.__esModule ?
    () => module['default'] :
    () => module;
  __webpack_require__.d(getter, { a: getter });
  return getter;
};

__webpack_require__.f

// 这个对象下,挂载需要拆分打包(import() 或 require.ensure)的模块函数, 例如:
// f.j, 入口文件 entry 的依赖
// f.consumes, f.remotes module federation的依赖
__webpack_require__.f = {};

__webpack_require__.e

//  对 entry 入口文件中依赖的 chunk, 按顺序,进行加载并执行
__webpack_require__.e = (chunkId) => {
  return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
    __webpack_require__.f[key](chunkId, promises);
    return promises;
  }, []));
};

__webpack_require__.u

// 拼接 chunk 的文件名称(根据 webpack 配置的 basename?)
__webpack_require__.u = (chunkId) => {
  // return url for filenames based on template
  return "" + chunkId + ".js";
};

__webpack_require__.l

// 用于通过 scripts 标签加载 js 文件
// 限制加载 js 文件超时时间为 120s
// 加载 js 文件完毕之后,会删除 scripts 标签
(() => {
  var inProgress = {};
  // loadScript function to load a script via script tag
  /**
   * @params url 请求 js 文件的 url
   * @params done 请求完毕之后的回调函数
   * @params key 带有 chunk 的 id 的字符串,例如 chunk-1
  */
  __webpack_require__.l = (url, done, key) => {
    if(inProgress[url]) { inProgress[url].push(done); return; }
    var script, needAttach;
    if(key !== undefined) {
      // 通过以下循环,判断当前该 js 文件是否已经加载
      // 若已加载,则不会通过创建 scripts 标签加载 js 文件
      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") == 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", key);
      script.src = url;
    }
    inProgress[url] = [done];
    var onScriptComplete = (event) => {
      onScriptComplete = () => {
      }
      // 避免在 IE 中内存泄漏
      script.onerror = script.onload = null;
      clearTimeout(timeout);
      var doneFns = inProgress[url];
      delete inProgress[url];
      script.parentNode.removeChild(script);
      doneFns && doneFns.forEach((fn) => fn(event));
    }
    ;
    var timeout = setTimeout(() => {
      onScriptComplete({ type: 'timeout', target: script })
    }, 120000);
    script.onerror = script.onload = onScriptComplete;
    needAttach && document.head.appendChild(script);
  };
})();

__webpack_require__.r

// 通过 Object.defineProperty 来劫持 es 模块的 exports 对象
// 使得 es 模块的 __esModule 字段返回是 true 或
// es 模块的 Symbol.toStringTag 字段,返回固定值 "Module"
__webpack_require__.r = (exports) => {
   if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
     Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
   }
   Object.defineProperty(exports, '__esModule', { value: true });
 };

__webpack_require__.p

// 固定返回 webpack 设置的 publicPath
__webpack_require__.p = "http://localhost:3001/";

__webpack_require__.f.j

// 加载 chunk 文件
// 用于处理所有 chunk 文件的状态
// key 为 chunkId, value 是该 chunk 文件的状态,分别有
// * undefined 不会进行加载
// * null 为该 chunk 是 preloaded/prefetched 的类型
// * Promise 为该 chunk 还在加载当中
// * 0 为该 chunk 已经加载完毕
var installedChunks = {
  179: 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
        // 拼接需要请求的 js 文件链接
        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) => {
          // 加载 js 文件完毕之后的回调函数
          // 执行的时机,可以看 __webpack_require__.l 的函数
          if(__webpack_require__.o(installedChunks, chunkId)) {
            installedChunkData = installedChunks[chunkId];
            // 重点关注:这个时候,如果正常加载完毕的话,installedChunkData[chunkId] = 0 
            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);
      } else installedChunks[chunkId] = 0;
    }
  }
};

webpackJsonpCallback

在说,webpackJsonpCallback函数之前,先讲一下之前的函数列表。

main.js文件底部,会直接执行__webpack_require__.e函数用于启动获取主函数。

入口文件先启动的函数顺序是

__webpack_require__.e => __webpack_require__.f.j => __webpack_require__.l

来加载 js 文件。__webpack_require__.f.j 函数里面有一个 loadingEnded 的回调函数,这个函数是在 js 文件加载完之后,onload 触发的。但是,判断加载的 chunk 文件是否成功,是根据 installedChunkData 这个变量来确定的。只有 installedChunkData 的值为 0 的时候,才算成功。从函数调用顺序来看,没有看到什么时候对 installedChunkData 的值进行赋值,而这个赋值,就是在 webpackJsonpCallback来进行处理的。webpackJsonpCallback代码如下:

webpack 在全局中定义变量webpackJsonpmodule_federation_starter(webapck 5以下是:webpackJsonp 变量),该变量是一个数组,劫持了该数组的 push 方法,当有新的元素 push 到该数组,就先调用 webpackJsonpCallback 方法。

webpackJsonpCallback 方法中,主要做两件事:

  1. 把成功加载的 chunk 的标识置为:0,在__webpack_require__.f.j 中能够识别已加载成功
  2. 把成功加载的 chunk 中,含有的所有 module 添加到 __webpack_require__.m__webpack_modules__)中,其他 module 依赖就可以直接获取
// install a JSONP callback for chunk loading
function webpackJsonpCallback(data) {
  var chunkIds = data[0];
  var moreModules = data[1];
  var runtime = data[3];
  // add "moreModules" to the modules object,
  // then flag all "chunkIds" as loaded and fire callback
  var moduleId, chunkId, i = 0, resolves = [];
  for(;i < chunkIds.length; i++) {
    chunkId = chunkIds[i];
    if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
      resolves.push(installedChunks[chunkId][0]);
    }
    // 关键点
    installedChunks[chunkId] = 0;
  }
  for(moduleId in moreModules) {
    if(__webpack_require__.o(moreModules, moduleId)) {
      // 关键点
      __webpack_require__.m[moduleId] = moreModules[moduleId];
    }
  }
  if(runtime) runtime(__webpack_require__);
  if(parentJsonpFunction) parentJsonpFunction(data);
  while(resolves.length) {
    resolves.shift()();
  }
};

// 关键点
var jsonpArray = window["webpackJsonpmodule_federation_starter"] = window["webpackJsonpmodule_federation_starter"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
var parentJsonpFunction = oldJsonpFunction;

__webpack_require__.f.remotes

// 需要被处理的 chunkId 
var chunkMapping = {
  "164": [
    164
  ]
};
var idToExternalAndNameMapping = {
  "164": [
    "default",
    "./Button",
    980
  ]
};

__webpack_require__.f.remotes = (chunkId, promises) => {
 if(__webpack_require__.o(chunkMapping, chunkId)) {
   chunkMapping[chunkId].forEach((id) => {
     var getScope = __webpack_require__.R;
     if(!getScope) getScope = [];
     var data = idToExternalAndNameMapping[id];
     if(getScope.indexOf(data) >= 0) return;
     getScope.push(data);
     if(data.p) return promises.push(data.p);
     var onError = (error) => {
       if(!error) error = new Error("Container missing");
       if(typeof error.message === "string")
         error.message += '\nwhile loading "' + data[1] + '" from ' + data[2];
       __webpack_modules__[id] = () => {
         throw error;
       }
       data.p = 0;
     };
     // 处理回调的工厂方法
     var handleFunction = (fn, arg1, arg2, d, next, first) => {
       try {
         var promise = fn(arg1, arg2);
         if(promise && promise.then) {
           var p = promise.then((result) =>
                          (next(result, d)),
                          onError
                        );
           if(first) promises.push(data.p = p); else return p;
         } else {
           return next(promise, d, first);
         }
       } catch(error) {
         onError(error);
       }
     }
     // 判断是否已经请求远端 remoteEntry 文件, 若未请求,  __webpack_require__.I 会发出请求
     var onExternal = (external, _, first) => (external
        ? handleFunction(
          __webpack_require__.I,
          data[0],
          0,
          external,
          onInitialized,
          first
        )
        : onError()
    );
    // 调用 remoteEntry 中暴露的 get 焊方法
     var onInitialized = (_, external, first) => (
      handleFunction(
        external.get,
        data[1],
        getScope,
        0,
        onFactory,
        first
      )
    );
    // 往 __webpack_modules__ 中挂载 moduleId, 后续给到 __webpack_require__ 所调用
     var onFactory = (factory) => {
      // chunk 加载中
       data.p = 1;
       __webpack_modules__[id] = (module) => {
         module.exports = factory();
       }
     };
     // 执行顺序 __webpack_require__ => remoteEntry.get => __webpack_module__.m => __webpack_require__
     handleFunction(__webpack_require__, data[2], 0, 0, onExternal, 1);
   });
 }
}
})();

__webpack_require__.f.consumes

在使用 ModuleFederationPlugin 的时候,配置 shared 依赖包的加载处理,例如配置为:

new ModuleFederationPlugin({
	// ... 其他的配置
  // 共享模块的配置 f.consumes 函数就是处理这些依赖
  shared: ["react", "react-dom"],
}),
// 定义 chunkId 需要依赖的 chunk 的关系
// 例如 801 这个 chunk 是需要把 module id 为 250, 138 的依赖进行加载
// 该关系,在 webpack 打包的时候,自动生成
var chunkMapping = {
  "591": [
    591
  ],
  "801": [
    250,
    138
  ]
};

// 定义share 依赖包进行加载的方法,与对应的版本
// 版本用来在其他 webpack 应用共享的时候,进行是否复用判断
var moduleToHandlerMapping = {
  250: () => loadStrictVersionCheckFallback("default", "react-dom", ["16",13,0], () => Promise.all([__webpack_require__.e(338), __webpack_require__.e(591)]).then(() => () => __webpack_require__(338))),
  138: () => loadStrictVersionCheckFallback("default", "react", ["16",13,0], () => __webpack_require__.e(162).then(() => () => __webpack_require__(162))),
  591: () => loadStrictVersionCheckFallback("default", "react", ["16",14,0], () => __webpack_require__.e(764).then(() => () => __webpack_require__(162)))
};

__webpack_require__.f.consumes = (chunkId, promises) => {
  // 需要判断 chunkId 是否在配置中
  if(__webpack_require__.o(chunkMapping, chunkId)) {
    chunkMapping[chunkId].forEach((id) => {
      if(__webpack_require__.o(installedModules, id)) return promises.push(installedModules[id]);
      // chunk 加载成功,加入到对应 __webpack_module__ 中,模块后续不需要重新加载
      var onFactory = (factory) => {
        installedModules[id] = 0;
        __webpack_modules__[id] = (module) => {
          delete __webpack_module_cache__[id];
          module.exports = factory();
        }
      };
      var onError = (error) => {
        delete installedModules[id];
        __webpack_modules__[id] = (module) => {
          delete __webpack_module_cache__[id];
          throw error;
        }
      };
      try {
        // 调用方法,进行加载对应的 chunk
        var promise = moduleToHandlerMapping[id]();
        if(promise.then) {
          promises.push(installedModules[id] = promise.then(onFactory).catch(onError));
        } else onFactory(promise);
      } catch(e) { onError(e); }
    });
  }
}

__webpack_require__.S

// 定义分享模块的 scope , 例如 default, default 里面会挂载依赖包的版本
__webpack_require__.S = {} // 初始化为空对象
// __webpack_require__.S = {
//  default: {
//    react: xxx,
//    react-dom: xxx
//  }
// }

__webpack_require__.I

// name 就是 scope 的 name
// __webpack_require__.I 函数就是为 scope 注册对应的依赖版本
// 注册完,挂载到 __webpack_require__.S 中
__webpack_require__.I = (name) => {
  // only runs once
  if(initPromises[name]) return initPromises[name];
  // handling circular init calls
  initPromises[name] = 1;
  // creates a new share scope if needed
  if(!__webpack_require__.o(__webpack_require__.S, name)) __webpack_require__.S[name] = {};
  // runs all init snippets from all modules reachable
  var scope = __webpack_require__.S[name];
  var warn = (msg) => typeof console !== "undefined" && console.warn && console.warn(msg);;
  
  // 为当前的包的所有版本都注册到 default 这个 scope 中
  // 并对版本进行判断
  // 通常版本号是 x.y.z 的,webpack 会把三个版本都进行注册,例如 react 16.14.0
  // 分别注册为
  // react`16, react`16`4, react`16`4`0
  // 这三个版本都挂载到 scope 下,也就是
  // __webpack_require__.S['default'] = { 
  //  react`16: { get: , factory: },
  //  react`16`14: { get: , factory: },
  //  react`16`14`0: { get: , factory: },
  // }
  var register = (name, version, factory, currentName) => {
    // ...
  };
  var initExternal = (id) => {}
  var promises = [];
  switch(name) {
    case "default": {
      register(
        "react",
        [16,14,0],
        // 定义获取当前库的方法, webpack 常用手段
        // 先通过 .e 函数来加载,然后通过 __webpack_require__ 来包装对象
        () => __webpack_require__.e(162).then(() => () => __webpack_require__(162))
      );
      register(
        "react-dom",
        [16,14,0],
        () => Promise.all([
        __webpack_require__.e(338),
        __webpack_require__.e(591)
        ]).then(() => () => __webpack_require__(338))
      );
    }
    break;
  }
  return promises.length && (initPromises[name] = Promise.all(promises).then(() => initPromises[name] = 1));
};

jingzhiMo avatar Mar 29 '21 02:03 jingzhiMo