icestark
icestark copied to clipboard
Vue微前端,子应用A 跳转 子应用B 会出现 子应用的 mount() 方法被调用了两次,分别在不同的上下文
问题
- 仅在 子应用A 跳转 子应用B ,则子应用B会被调用两次 mount 方法去加载
- 使用 @ice/stark-app . appHistory.push 跳转的时候会导致 vue-router 的promise栈堆溢出
- 使用 window.location.href = xxx 跳转则正常跳转
- 两则都会引起 子应用B 被调用了两次 mount 方法
环境
- 主应用
- vue2.6.11
- @ice/stark2.7.1
- 子应用
- vue2.6.11
- @ice/stark-app1.5.0
- vue-router3.1.3
子应用分别是
- Login 登陆端
- Main 云平台
主应用注释
let nodeContainer = document.querySelector('#NodeContainer');
registerMicroApps([ .... ]);
start();
console.log('Master入口创建');
子应用注释
if (isInIcestark()) {
setLibraryName(process.env.VUE_APP_FRONTEND);
setWebpackPublicPath();
} else {
createVue();
}
export function mount(props) {
// event.emit('page', {});
console.log('Login应用mount被调用'); # Main那边则是 console.log('Main应用mount被调用');
createVue(props);
}
export function unmount() {
console.log('Login应用unmount被调用'); # Main那边则是 console.log('Main应用unmount被调用');
destroyVue();
}
let instance = null;
export function createVue(props = { container: '#app' }) {
if (instance === null) {
console.log('Login应用createVue', instance); # Main那边则是 console.log('Main应用createVue', instance);
router = createRouter();
instance = new Vue({ router, render: h => h(App) });
instance.$mount(props.container);
} else {
console.log('Login应用不处理创建', instance); # Main那边则是 console.log('Main应用不处理创建', instance);
}
}
运行日志 window.location.href 跳转
Master入口创建 app.8cd76315.js?v=20211115:6 Login应用mount被调用 app.8cd76315.js?v=20211115:6 Lgoin应用createVue null app.2f5e8c31.js:6 Master入口创建 app.b77e37d2.js:6 Main应用mount被调用 app.b77e37d2.js:6 Main应用createVue null app.b77e37d2.js:6 Main应用mount被调用 app.b77e37d2.js:6 Main应用createVue null
运行日志 @ice/stark-app . appHistory.push() 跳转
Master入口创建 app.8cd76315.js?v=20211115:6 Login应用mount被调用 app.8cd76315.js?v=20211115:6 Lgoin应用createVue null app.2f5e8c31.js:6 Master入口创建 app.b77e37d2.js:6 Main应用mount被调用 app.b77e37d2.js:6 Main应用createVue null app.b77e37d2.js:6 Main应用mount被调用 app.b77e37d2.js:6 Main应用createVue null Uncaught (in promise) RangeError: Maximum call stack size exceeded at Generator._invoke ...
后记
可以看到在调用 createVue null 输出的都为 null 也就是说 createVue 是在两个不同的上下文中调用的 instance 并不共享
- 「仅在 子应用A 跳转 子应用B ,则子应用B会被调用两次 mount 方法去加载」 -> 使用什么 api 跳转的
- 「使用 @ice/stark-app . appHistory.push 跳转的时候会导致 vue-router 的promise栈堆溢出」-> 堆栈溢出的具体 log 是啥?
@h6play
- 「仅在 子应用A 跳转 子应用B ,则子应用B会被调用两次 mount 方法去加载」 -> 使用什么 api 跳转的
- 「使用 @ice/stark-app . appHistory.push 跳转的时候会导致 vue-router 的promise栈堆溢出」-> 堆栈溢出的具体 log 是啥?
@h6play
1、跳转使用 window.location.href 或者 @ice/stark-app 的 appHistory.push() 方法 2、具体日志为
app.78d6b20b.js:6 Uncaught (in promise) RangeError: Maximum call stack size exceeded
at Generator._invoke (app.78d6b20b.js:6:23832)
at Generator.next (app.78d6b20b.js:6:23016)
at c (app.13a06ab7.js?v=20211115:6:8374)
at u (app.13a06ab7.js?v=20211115:6:8577)
at app.13a06ab7.js?v=20211115:6:8636
at new Promise ()
at app.13a06ab7.js?v=20211115:6:8517
at app.13a06ab7.js?v=20211115:6:11375
at h (vue-router.min.js?v=1643351341:1:17301)
at o (vue-router.min.js?v=1643351341:1:14135)
# 代码片段
while (1) {
var i = n.delegate;
if (i) {
var c = _(i, n);
if (c) {
if (c === v)
continue;
return c
}
}
if ("next" === n.method)
n.sent = n._sent = n.arg;
else if ("throw" === n.method) {
if (r === l)
throw r = h,
n.arg;
n.dispatchException(n.arg) # 这行抛出
} else
"return" === n.method && n.abrupt("return", n.arg);
r = d;
var u = f(e, t, n);
if ("normal" === u.type) {
if (r = n.done ? h : p,
u.arg === v)
continue;
return {
value: u.arg,
done: n.done
}
}
"throw" === u.type && (r = h,
n.method = "throw",
n.arg = u.arg)
}
@h6play 微应用方便提供一个 demo 吗
@h6play 微应用方便提供一个 demo 吗
刚刚创建一个新项目,就展示出了 vue-router 那个栈堆的错误,我试试部署到线上看看重复加载的问题 https://github.com/h6play/help-icestark1.git 刚刚测试了一下,新建demo居然无法复现加载两次的情况,我找找原因 http://xx.h6love.cn/
@maoxiaoke 我找到原因了,因为我这边主应用跳转子应用是通过 NodeUrl + ?token=xxx 的形式携带登陆凭证过去的,然后对于这些token子应用里面会获取后并删除的处理,也就是调用了 window.location.replaceState 进行替换路径处理,然后被 icestark 识别为路由跳转引起的重复加载。
但是还是引出一个问题,如果路由相同,icestark 并不会先调用unmount 进行卸载,而是直接调用 mount 方法,然后就会导致,我在 dom 中的每个 click 事件,每个 created 方法 都会被调用两次,以及渲染两次,或者 icestark 是否有专属的优化url的方式提供呢?
案例
原:http://xxx.com/main/?token=xxxxx 执行获取 token 并优化 url 的方法 window.history.replaceState 后:http://xxx.com/main/ 结果:mount() 调用了两次 结果:vue.created() 调用了两次 结果:点击 DOM 中按钮事件 onSubmit 调用了两次
@maoxiaoke
案例数据
配置:/main 指向 应用A 原来:url = http://xx.com/main/?token=xxx 处理:window.history.replaceState 方法 结果:url = http://xx.com/main/ 依旧指向该应用
icestark 代码
## 选取 icestark/start.ts
export function reroute(url: string, type: RouteType | 'init' | 'popstate'| 'hashchange') {
const { pathname, query, hash } = urlParse(url, true);
// trigger onRouteChange when url is changed
if (lastUrl !== url) {
globalConfiguration.onRouteChange(url, pathname, query, hash, type);
const unmountApps = [];
const activeApps = [];
getMicroApps().forEach((microApp: AppConfig) => {
const shouldBeActive = microApp.checkActive(url);
if (shouldBeActive) {
activeApps.push(microApp);
} else {
unmountApps.push(microApp);
}
});
// trigger onActiveApps when url is changed
globalConfiguration.onActiveApps(activeApps);
// call captured event after app mounted
Promise.all(
// call unmount apps
unmountApps.map(async (unmountApp) => {
if (unmountApp.status === MOUNTED || unmountApp.status === LOADING_ASSETS) {
globalConfiguration.onAppLeave(unmountApp);
}
await unmountMicroApp(unmountApp.name);
}).concat(activeApps.map(async (activeApp) => {
if (activeApp.status !== MOUNTED) {
globalConfiguration.onAppEnter(activeApp);
}
await createMicroApp(activeApp);
})),
).then(() => {
callCapturedEventListeners();
});
}
lastUrl = url;
}
以及 apps.ts
export async function mountMicroApp(appName: string) {
const appConfig = getAppConfig(appName);
// check current url before mount
if (appConfig && appConfig.checkActive(window.location.href) && appConfig.status !== MOUNTED) {
if (appConfig.mount) {
await appConfig.mount({ container: appConfig.container, customProps: appConfig.props }); //创建过程中调用的 window.history.replaceState 方法
}
updateAppConfig(appName, { status: MOUNTED });
}
}
原因
- 当前 active 的应用两次 hashChange 事件都未改变,但是在调用 mount 的过程中调用的 window.history.replaceState 然后产生了一个 hashChange 事件进入
- 当前 app 状态处于 NOT_MOUNTED 导致判断又重新 mount 了一次
- 思考:是否应该将 MOUNTED 状态更新放在 mount 之前,还是之后,或者增加中间态
- 一般用户都是在 mount 方法中去创建 vue 实例的,并且会在此过程中对url的参数进行更改,总会产生这样的事件
解决处理
- 因为这个是生产过程中需要使用的,目前我这样进行了处理解决这个问题
export function mount(props) {
console.log('createVue 之前');
setTimeout(() => { createVue(props); }, 0);
console.log('createVue 之后');
}
@h6play 这个 window.history.replaceState 是代码主动调用的?
@h6play MOUNTED 这个状态在 mount 之前是不合理的,可以增加一个中间态,避免重复 mount,欢迎 pr 哦
态在 mount 之前是不合理的,可以增加一个中间态,避免重复 mount,欢迎 pr
是的,
@h6play 这个 window.history.replaceState 是代码主动调用的?
是的,为了获取 token 并删除
状态在 mount 之前是不合理的,可以增加一个中间态,避免重复 mount,欢迎 p
那我回家后增加一个pr