icestark icon indicating copy to clipboard operation
icestark copied to clipboard

Vue微前端,子应用A 跳转 子应用B 会出现 子应用的 mount() 方法被调用了两次,分别在不同的上下文

Open h6play opened this issue 3 years ago • 10 comments

问题

  • 仅在 子应用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 并不共享

h6play avatar Feb 25 '22 09:02 h6play

  1. 「仅在 子应用A 跳转 子应用B ,则子应用B会被调用两次 mount 方法去加载」 -> 使用什么 api 跳转的
  2. 「使用 @ice/stark-app . appHistory.push 跳转的时候会导致 vue-router 的promise栈堆溢出」-> 堆栈溢出的具体 log 是啥?

@h6play

maoxiaoke avatar Feb 28 '22 02:02 maoxiaoke

  1. 「仅在 子应用A 跳转 子应用B ,则子应用B会被调用两次 mount 方法去加载」 -> 使用什么 api 跳转的
  2. 「使用 @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 avatar Feb 28 '22 03:02 h6play

@h6play 微应用方便提供一个 demo 吗

maoxiaoke avatar Feb 28 '22 03:02 maoxiaoke

@h6play 微应用方便提供一个 demo 吗

刚刚创建一个新项目,就展示出了 vue-router 那个栈堆的错误,我试试部署到线上看看重复加载的问题 https://github.com/h6play/help-icestark1.git 刚刚测试了一下,新建demo居然无法复现加载两次的情况,我找找原因 http://xx.h6love.cn/

h6play avatar Feb 28 '22 04:02 h6play

@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 调用了两次

h6play avatar Mar 01 '22 07:03 h6play

@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 avatar Mar 01 '22 08:03 h6play

@h6play 这个 window.history.replaceState 是代码主动调用的?

maoxiaoke avatar Mar 01 '22 09:03 maoxiaoke

@h6play MOUNTED 这个状态在 mount 之前是不合理的,可以增加一个中间态,避免重复 mount,欢迎 pr 哦

maoxiaoke avatar Mar 01 '22 09:03 maoxiaoke

态在 mount 之前是不合理的,可以增加一个中间态,避免重复 mount,欢迎 pr

是的,

@h6play 这个 window.history.replaceState 是代码主动调用的?

是的,为了获取 token 并删除

h6play avatar Mar 01 '22 09:03 h6play

状态在 mount 之前是不合理的,可以增加一个中间态,避免重复 mount,欢迎 p

那我回家后增加一个pr

h6play avatar Mar 01 '22 09:03 h6play