解决微前端应用间模块冲突
提示
本文内容与 webpack 运行时机制关联较大,不太了解的小伙伴可以先看下我去年写的相关文章:webpack输出文件分析以及编写一个loader
背景
在微前端开发阶段,子应用会经常发生模块加载失败的错误: can't read property call of undefined
导致应用崩溃。由于微前端子应用是通过 systemjs
动态加载的 umd 模块,导致很难 debug 该问题,因此这个问题困扰了我们许久。
点击报错对应的 js 文件,抛出错误的代码如下:
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId));
复制代码
这行代码位于 __webpack_require__
函数中,从函数名可以隐约感觉到,这是 webpack 加载模块失败导致的问题,后面解决后也印证了确实是加载模块失败导致的程序崩溃。
但是该怎么 debug 找到问题根源并解决呢?
Debug 过程
一开始我们通过修改项目的一些配置,逐渐定位问题点,发现是在异步 dynamic 加载模块时会导致这个错误,如果禁用动态加载,直接 import 模块,就不会报模块加载失败的错误,程序完全正常。
因此做了一些尝试,在动态加载 import('xxx/yyy')
模块时,加了一个设置 webpack 异步 chunkId 的配置:import(/* webpackChunkName: "xxxyyy" */ 'xxx/yyy')
,程序看起来正常运行了。
但我们仍然没找到问题根源在哪,而且其他微前端应用也陆陆续续出现了这个问题,并且配置了 webpackChunkName
依然不好使。
于是我们只能硬着头皮一行一行 debug,由于 webpack 的 runtime 代码是服务自动插入的,无法插入调试代码,只能在浏览器打开 devServer 下的入口文件一行一行看。
好在去年我写过一篇关于 webpack 运行时代码分析的文章,对 webpack runtime 的代码运行机制有所了解,并且根据之前的经验,感觉错误应该是发生在加载异步 chunk 导致的,因此我们把注意力放在了 webpack 加载异步 chunk 的实现上,相关的代码如下:
// webpack 异步加载模块的回调函数,用于 resolve 异步的 promise
/******/ // install a JSONP callback for chunk loading
/******/ function webpackJsonpCallback(data) {
/******/ var chunkIds = data[0];
/******/ var moreModules = data[1];
/******/
/******/
/******/ // 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(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
/******/ resolves.push(installedChunks[chunkId][0]);
/******/ }
/******/ installedChunks[chunkId] = 0;
/******/ }
/******/ for(moduleId in moreModules) {
/******/ if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
/******/ modules[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
/******/ if(parentJsonpFunction) parentJsonpFunction(data);
/******/
/******/ while(resolves.length) {
/******/ resolves.shift()();
/******/ }
/******/
/******/ };
...
// webpack 异步加载模块的具体实现,动态床架 script 标签加载异步 js 文件
/******/ // This file contains only the entry chunk.
/******/ // The chunk loading function for additional chunks
/******/ __webpack_require__.e = function requireEnsure(chunkId) {
/******/ var promises = [];
/******/
/******/
/******/ // JSONP chunk loading for javascript
/******/
/******/ var installedChunkData = installedChunks[chunkId];
/******/ if(installedChunkData !== 0) { // 0 means "already installed".
/******/
/******/ // a Promise means "currently loading".
/******/ if(installedChunkData) {
/******/ promises.push(installedChunkData[2]);
/******/ } else {
/******/ // setup Promise in chunk cache
/******/ var promise = new Promise(function(resolve, reject) {
/******/ installedChunkData = installedChunks[chunkId] = [resolve, reject];
/******/ });
/******/ promises.push(installedChunkData[2] = promise);
/******/
/******/ // start chunk loading
/******/ var script = document.createElement('script');
/******/ var onScriptComplete;
/******/
/******/ script.charset = 'utf-8';
/******/ script.timeout = 120;
/******/ if (__webpack_require__.nc) {
/******/ script.setAttribute("nonce", __webpack_require__.nc);
/******/ }
/******/ script.src = jsonpScriptSrc(chunkId);
/******/
/******/ // create error before stack unwound to get useful stacktrace later
/******/ var error = new Error();
/******/ onScriptComplete = function (event) {
/******/ // avoid mem leaks in IE.
/******/ script.onerror = script.onload = null;
/******/ clearTimeout(timeout);
/******/ var chunk = installedChunks[chunkId];
/******/ if(chunk !== 0) {
/******/ if(chunk) {
/******/ 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;
/******/ chunk[1](error);
/******/ }
/******/ installedChunks[chunkId] = undefined;
/******/ }
/******/ };
/******/ var timeout = setTimeout(function(){
/******/ onScriptComplete({ type: 'timeout', target: script });
/******/ }, 120000);
/******/ script.onerror = script.onload = onScriptComplete;
/******/ document.head.appendChild(script);
/******/ }
/******/ }
/******/ return Promise.all(promises);
/******/ };
...
// jsonpArray,挂在 window 上,缓存已加载的异步 module
/******/ var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
/******/ var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
/******/ jsonpArray.push = webpackJsonpCallback;
/******/ jsonpArray = jsonpArray.slice();
/******/ for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
/******/ var parentJsonpFunction = oldJsonpFunction;
复制代码
异步 chunk 文件的内容格式如下:
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["userodel"], {
"./src/constants/constant.js":
/***/
(function(module, exports, __webpack_require__) {
"use strict";
eval("模块内容...");
}),
}]);
复制代码
webpack 在加载异步 chunk 之后,会把相关的模块存到 webpackJsonp
变量上,usermodel
是配置的 webpackChunkName
,会作为模块的标识存在 webpackJsonp
上,如果没有配置 webpackChunkName
,这个标识就是 webpack 自动生成的数字 id。
由于微前端的运行机制是通过 systemjs
加载 webpack 构建的子应用,并且微前端的基座本身也是 webpack 构建的一个单体应用,因此基座和子应用构建出的bundle 是运行在一个 window 下的,导致了应用之间的模块混乱,如下图示意:
如果基座加载了一个 id 为 '0' 的异步 chunk,首先就会动态创建一个路径类似 0.async.js
的 script 标签加载该 chunk,由于基座和子应用都已经处于运行时,因此这个异步 chunk 的加载,就会触发最后加载的应用的 webpackJsonpCallback
,导致异步 chunk 中模块的保存混乱,进而引发运行时的代码错误。
解决办法
既然已经找到了问题的根本原因,那么只要保证每一个应用的异步 chunk 加载函数名称唯一即可,不过我不太清楚这个函数名是否可配置,一开始是想写一个插件,好在组内大佬金龙哥提醒了我,在 webpack 中有个 jsonpFunction
的配置,能够配置这个函数名,配置如下:
module.exports = {
output: {
jsonpFunction: `webpackJsonp_${appId}`
}
}
复制代码
各个应用配置唯一 jsonpFunction
后,问题完美解决~