首先介绍下这篇文章依赖的知识和背景:
- UmiJS: 插件化的企业级前端应用框架。
- qiankun: 基于 single-spa 的企业级微前端库。
- RequireJS: AMD 模块加载库。
- 这篇文章中 UmiJS 使用
@umijs/plugin-qiankun
插件来支持微前端。
报错内容
复现案例:umi-qiankun-requirejs-issue
直接打开子项目
报错:Mismatched anonymous define() module
错误详细描述;https://requirejs.org/docs/errors.html#mismatch
在主项目中打开子项目
报错:[qiankun]: You need to export lifecycle functions in slave entry
解决方案
在 .umirc.ts
中加上如下配置:
chainWebpack(memo) {
memo.output.libraryTarget('window');
},
PS:这个解决方案在 qiankun 的文档里也提到了。
原因
output.libraryTarget
配置
webpack 普通项目直接打开时没有报错,但改为微前端作为子项目直接打开时报错了,那一定是改为微前端后打包出来的代码变了。下面对比一下打包后的代码:
微前端项目打包的开头:
function webpackUniversalModuleDefinition(root, factory) {
(if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else if(typeof exports === 'object')
exports["umi-t-umi"] = factory();
else
"umi-t-umi"] = factory();
root[window, function() {
})(return /******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
// ...
普通项目打包的开头:
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
容易看出它们的 webpack 的 output.libraryTarget
配置不同。
通过查看@umijs/plugin-qiankun
的源码发现确实这个配置被修改:
config.output
libraryTarget("umd")
.library(
.? api.pkg.name : `${api.pkg.name}-[name]`
shouldNotAddLibraryChunkName
);
只修改这个配置普通情况下不报错,引入 RequireJS 才报错,下面进一步研究下 RequireJS。
RequireJS define
直接启动子项目的报错是因为不能在 script 标签引的 JS 里用匿名 define
(RequireJS 虽然支持匿名 define
但必须通过 RequireJS 去引)。下面是一个例子:
test-module.js
// test-module.js
define(function () {
console.log("define");
return {
: "foo",
foo: "bar",
bar
};
});
test-define.html
script src="https://cdn.jsdelivr.net/npm/[email protected]/require.js"></script>
<
<!-- 不报错 -->
script>
<require(["./test-module.js"], function (module) {
console.log(module.foo);
});script>
</
script>
<require(["test-module"], function (module) {
console.log(module.foo);
});script>
</
<!-- 报错 -->
script src="./test-module.js"></script>
<
script>
<define(function () {
console.log("define");
return {
: "foo",
foo: "bar",
bar
};
});script>
</
这里还有一个有趣的现象:
RequireJS 会在加载后 4ms (nextTick) 清一下队列,对于 4ms 后 define 的模块会在下次清队列时处理。
所以下面这段代码不会报错,但如果放开调用 require 函数的注释就会报错。
script src="https://cdn.jsdelivr.net/npm/[email protected]/require.js"></script>
<
<!-- 不报错 -->
script>
<setTimeout(function () {
define([], function () {
console.log("define");
return {
: "foo",
foo: "bar",
bar
};
});
// 放开下面这行调用 require 函数的注释会报错
// require();
5);
}, script>
</
在主项目中打开子项目,qiankun 是通过 fetch 拉下来子项目 JS 然后再执行的,这时已经过了 4ms,所以 define 的模块这时还并未执行,报错的原因要继续研究下 qiankun 生命周期。
qiankun 生命周期
qiankun 在加载子项目时会去检查入口脚本是否暴露生命周期,没有检查到会报错。
检查顺序:
- 入口脚本的导出(通过看 qiankun 内部使用的 import-html-entry 的源码,
scriptExports
应该一直是undefined
); - 入口脚本最后赋值的变量;
- 入口脚本的全局上的
${appName}
属性。
qiankun 生命周期检查源码:
function getLifecyclesFromExports(
scriptExports: LifeCycles<any>,
appName: string,
global: WindowProxy,
globalLatestSetProp?: PropertyKey | null
) {if (validateExportLifecycle(scriptExports)) {
return scriptExports;
}
// fallback to sandbox latest set property if it had
if (globalLatestSetProp) {
const lifecycles = (<any>global)[globalLatestSetProp];
if (validateExportLifecycle(lifecycles)) {
return lifecycles;
}
}
if (process.env.NODE_ENV === "development") {
console.warn(
`[qiankun] lifecycle not found from ${appName} entry exports, fallback to get from window['${appName}']`
);
}
// fallback to global variable who named with ${appName} while module exports not found
const globalVariableExports = (global as any)[appName];
if (validateExportLifecycle(globalVariableExports)) {
return globalVariableExports;
}
throw new Error(
`[qiankun] You need to export lifecycle functions in ${appName} entry`
);
}
主项目中打开子项目报错的原因也可以确定了:检查到入口脚本没有暴露生命周期。
总结
遇到报错首先还是要先看文档,这样可以快速解决问题。如果问题很难定位可以考虑二分法和控制变量法将问题范围缩小,定位到问题后就容易解决了。广泛被使用的库会包含处理各种复杂情况的逻辑,结合在一起使用可能会各种奇怪,为什么这样报错了真奇怪,为什么这样不报错真奇怪,想理清这些奇怪真不太容易。