likes
comments
collection
share

我花了三天彻底搞懂了webpack中这个报错

作者站长头像
站长
· 阅读数 3

文章内将使用 MF/mf 指代 Webpack Module Federation,并假设知晓基本的 MF 使用

MF官方文档

一、页面偶发报错:"Container initialization failed as it has already been initialized with a different share scope"

1.1 问题背景

我司是大量依赖 MF 的能力,包括:应用加载、模块(组件)共享以及 shared library(共享依赖包)

某天测试同学提了一个bug过来说门户页面偶发组件显示异常。

门户大概长这样,橙色部分就是用户自行配置的显示模块,他可能来自多个应用

我花了三天彻底搞懂了webpack中这个报错

1.2 解决问题

首先报错信息参考性不大,因为涉及到 MF 的一些内部术语,这种情况我们先使用科学上网搜索报错信息

很快啊,就我们找到了 github上关于此问题的讨论,作者给出了两个解决方案

  1. try catch 报错代码
  2. 添加标识,避免多次初始化同一个容器

我花了三天彻底搞懂了webpack中这个报错

我花了三天彻底搞懂了webpack中这个报错

这里我们采用了第一种方案,对 loadComponent 添加try catch

function loadComponent(scope, module) {
  return async () => {
    // Initializes the shared scope. Fills it with known provided modules from this build and all remotes
    await __webpack_init_sharing__('default');
    const container = window[scope]; // or get the container somewhere else
    // Initialize the container, it may provide shared modules
    try {
      await container.init(__webpack_share_scopes__.default);
    } catch(err) {
        // 忽略
    }
   
    const factory = await window[scope].get(module);
    const Module = factory();
    return Module;
  };
}

loadComponent('abtests', 'test123');

很顺利啊,问题解决了。bug成功关闭

1.3 bug解决了,但是问题却没有

bug是关掉了,但是我满脑子一堆❓

  • 为什么不可以重复初始化?其他很多地方也是重复调用 loadComponent 都没问题
  • 为什么就这个页面会报错,还是偶发
  • 加了 try catch 有什么潜在风险

这些对于一个有技术洁癖的人来说就像是:你告诉一个强迫症患者,治好你的病只需要两步

  1. 每天按时起床

二、追根溯源

想要真正知道为什么报这个错误,就必须深入了解 MF 里面的概念

首先我们先看报错信息。翻译成中文:容器初始化失败,因为它已经使用不同的共享范围进行了初始化

这里有3个关键词:

容器(Container)

容器初始化(initialization)

share scope(共享范围)

2.1 容器(Container)的一生

从官方文档我们可以知道,容器就是:任意单独构建、遵循公开对特定模块的异步访问能力的应用

而其中 公开对特定模块异步访问能力 具体步骤如下:

  1. 加载模块(异步)
  2. 评估(evaluating)模块(同步)

大白话就是:你想要使用任意一个容器的模块,需要先加载这个容器中模块对应的资源,然后解析模块

而任意容器本身可以充当 Host,也可以作为 Remote。即:可以加载其他容器,也可以被其他容器加载。

我花了三天彻底搞懂了webpack中这个报错

道理讲完了,我们看看代码。从官方加载MF模块代码示例代码中我们可以看到,其实是存在两次初始化

我花了三天彻底搞懂了webpack中这个报错

第一次初始化说的是 初始化共享范围,第二次初始化才是 容器初始化

你可以看到这里的两次初始化都是魔术字符串:

__webpack_init_sharing__

__webpack_share_scopes__

这是人类能看懂的代码吗??

我花了三天彻底搞懂了webpack中这个报错

没办法,只能使用我的终极绝招了:

看源码,跑dome 调试

这个办法我尝试了很多次,百试百灵

2.2 实践才是检验知识的唯一标准

首先我们clone webpack,直接搜索那两个魔术字符串。这种 magic string 最后一定会被编译成具体方法的,果然顺利找到

我花了三天彻底搞懂了webpack中这个报错

我花了三天彻底搞懂了webpack中这个报错

再把dome跑起来,对照看下

我花了三天彻底搞懂了webpack中这个报错

果然没错了,而 webpack_require 这个函数是 webpack 实现的模块引用函数, 每个应用(容器)的入口文件处通过闭包的形式实现了该方法,

我花了三天彻底搞懂了webpack中这个报错

我花了三天彻底搞懂了webpack中这个报错

敲重点,可以看到 MF 的入口文件(remoteEntry.js)也是通过闭包的方式实现的 webpack_require,而我们刚才的两个魔术字符串都是挂载在这个函数上的静态属性/方法(IS)

ok,现在我们在回头再看看 loadComponent 的代码,是不是就so easy了?嗯,问题来了:

  1. loadComponent 中 第一个初始化共享范围是初始化谁的?
  2. 第二个容器初始化是初始化哪个容器?它的参数是谁的?

2.3 场景模拟

回答上面两个问题之前,我们先来一个模拟下真实场景

假设我们的门户页面会使用 loadComponent 加载商城应用的商品列表模块,它们都是 MF 应用。请回答上面两个问题的答案:

  1. 当然是初始化 门户 的共享范围对吧
  2. 必须是初始化 商城 容器,它的参数是 门户 的 共享范围

那什么是共享范围呢?官方文档没有太多解释,莫得关系,我可以自食其力!

只需要花费区区两天时间,就可以通过调试断点以及读代码领悟出来:

从用户打开浏览器页面开始,每一个 非webpack应用打开的第一个 webpack 应用,就会构建一个共享范围,一直传递给这个应用加载的所有的 MF 应用共用

我花了三天彻底搞懂了webpack中这个报错

应用A是加载的第一个webpack应用,所以它会初始化产生一个共享范围,然后会传递给 应用B/C/D/E/F... 所以共享范围就像是一个作用域链,只要在这个链条内。应用共享同一个shared scope

2.4 制造犯罪现场

基于上面的知识,那么我们怎么才能制造出 “Container initialization failed as it has already been initialized with a different share scope” 这个报错呢?

  1. 加载链路中出现非MF加载(可能性高)

我花了三天彻底搞懂了webpack中这个报错 可以看到应用D会被初始化多次,并且应用C是全新的共享范围,应用B是应用A的共享范围。所以必定报错

  1. 代码错误,某一次加载中不传递共享范围(可能性低)
  2. 其他?

三、功败垂成转柳暗花明

当我成功制造出犯罪现场时候,我以为再去复盘这个bug应该就轻而易举了。 我花了三天彻底搞懂了webpack中这个报错 万万没想到,生活又给了我一刀...

3.1 功败垂成

按照上面的结论,我反推bug产生的真正原因。发现代码 loadComponent 没问题,和官方代码一模一样。虽然存在多处拷贝,这不是啥大问题

那么就一定是加载链路有问题了,按照这个思路排查。半天过去了,发现整个加载链路都是对的。但是就是出现了多个共享范围的情况,并且还都是同一个应用(还记得门户那张图吧,里面的模块都来自同一个应用同一个模块)

如果遇到这种情况,我的建议是好好休息下。再整理整理现在所有已经掌握的信息,从头再来制定突破点。不然只会在细节处愈陷愈深,最后功败垂成

我花了三天彻底搞懂了webpack中这个报错

3.2 踏入深水区

从传递共享范围入口,通过阅读代码。应用的共享范围是挂在一个空对象的 default 属性上,这个空对象是 evaluation 阶段通过闭包跟随 webpack_require 产生的

我花了三天彻底搞懂了webpack中这个报错

那么我可以对这个空对象做手脚,通过堆栈找到幕后黑手呢!给大家分享一个小技巧:通过条件断点注入调试代码

我花了三天彻底搞懂了webpack中这个报错

Object.defineProperty(__webpack_require__.S, 'default',{set(val) {debugger;window.aaa = val}, get() {debugger;return window.aaa}});false

我选择劫持设置 default 属性,结果全部指向 容器初始化 的函数,看来此路不通

就在一筹莫展并思考许久之后,突然发现一个问题:之前断点发现,入口文件被反复 evaluation,是不是意味着同一个应用存在反复加载入口文件的问题?这个会不会有影响??

再次使用条件断点注入调试代码,收集 webpack_require 函数到全局变量,然后对比函数的指针,发现确实是同一个应用存在多个 webpack_require,翻看代码,是因为这个应用的开发同学每次在 loadComponent 之前都会加载一次 remoteEntry。我感觉可能找到了问题关键了!

3.3 官方误我啊,柳暗花明

按照上面的逻辑,如果每次都是一个新的 webpack_require,相当于下图

我花了三天彻底搞懂了webpack中这个报错

再仔细看图并思考过后,发现这样写并无不妥。最后只是浪费几次请求而已,事情又陷入了僵局。

享受了片刻失败的感觉,我又打起精神,再读了一遍代码,并仔细推敲每个过程。到底什么情况会出现问题??

直到我再次看到

我花了三天彻底搞懂了webpack中这个报错

难道会是走到这儿了吗?在我们这种场景下,什么情况才会走到这里面?我尝试打了断点,一次,两次,三次刷新页面。进了进了,进入断点了!!!这次我真的感觉谜题的答案要被破解了!!

通过堆栈可以看到,是 loadComponent 中

const factory = await window[scope].get(module);

这儿进来的,这是容器初始化结束后,加载模块了。正常来说绝对不可能进入断点才对,容器明明已经初始化了!

我再次读了 loadComponent 的实现,颤抖的打下了一个条件断点。没有意外,刷新页面,进入了断点。真相大白

我花了三天彻底搞懂了webpack中这个报错

你看出来了问题了吗?

没错,官方的代码存在问题。因为初始化的容器和加载模块的容器可能不是同一个了啊!可以看到容器初始化是一个异步的过程,假如恰好在初始化过程中,相同应用的入口文件又 evaluation 了一次,window[scope]上挂载的容器变成了最新的闭包产生的,而不是上一次的。相当于下面就会用这个没有初始化化的容器去加载模块,共享范围就会创建一个新的!!

正确的代码应该是:

--const factory = await window[scope].get(module);

++const factory = await container.get(module);

至此,所有问题全部解决。总共花费了我三天的时间(其实不止,周末也搭进去了)

四、总结

1、loadComponent 的正确写法应该是

function loadComponent(scope, module) {
  return async () => {
    // Initializes the share scope. This fills it with known provided modules from this build and all remotes
    await __webpack_require__.I('default');
    const container = window[scope]; // or get the container somewhere else
    // Initialize the container, it may provide shared modules
    await container.init(__webpack_require__.S.default);
    const factory = await container.get(module);
    const Module = factory();
    return Module;
  };
}

并非是官方示例有问题,其实官方也说了,容器只能初始化一次,入口文件正常情况也只会 evaluation 一次。但是说实话 MF 官方提供的文档还是比较难懂,并且加载函数本身是也是用了 LOW LEAVL API,出现这种情况并不显的意外了。只需要苦一苦开发者就好

2. 不要害怕未知

俗话说好女怕缠郎,只要舍得时间对未知进攻,一定会拿下它的!而这次的排查之旅也给我让我对 MF 有了更多的认知,现在再来回答一开始的问题就完全胸有成竹了

如果使用 try catch 包裹组件,会有什么问题?

答案是:try catch 会忽略错误,但同时也断开了共享范围链,意味着 MF 的 shared 能力失效,如果是一些单例依赖,比如 react 等,页面就会直接报错。

至于多次初始化容器会怎么样?正常来说没问题,大胆初始化就行 :)

希望这篇文章可以帮助到遇到这个问题的其他朋友~那么下班愉快了