qiankun icon indicating copy to clipboard operation
qiankun copied to clipboard

[Feature Request] 主应用多页签切换不同子应用的页面状态保持

Open gaorubin1990 opened this issue 4 years ago • 77 comments

Background

想要主应用负责导航及页面页签切换,切换页签,即切换不同子应用的页面时,container不重新覆盖。看了下singular: false适用于一个路由页面组合展示的不同子应用界面场景?能否指导一下该需求的实现思路,非常感谢

Proposal

Describe the solution you'd like, better to provide some pseudo code.

Additional context

Add any other context or screenshots about the feature request here.

gaorubin1990 avatar Mar 30 '20 06:03 gaorubin1990

我也有同样的需求。不知道用qiankun怎么实现,暂时只能用frame去做了

wisdomG avatar Mar 30 '20 10:03 wisdomG

@gaorubin1990 @wisdomG 找到方案了吗?

werts avatar Apr 02 '20 01:04 werts

同求,一直不敢下手,就是因为这个

anncwb avatar Apr 02 '20 06:04 anncwb

see https://qiankun.umijs.org/zh/faq/#%E5%A6%82%E4%BD%95%E5%90%8C%E6%97%B6%E6%BF%80%E6%B4%BB%E4%B8%A4%E4%B8%AA%E5%AD%90%E5%BA%94%E7%94%A8%EF%BC%9F

不同的子应用的 render 实现不一样就不会覆盖了

kuitos avatar Apr 02 '20 07:04 kuitos

see https://qiankun.umijs.org/zh/faq/#%E5%A6%82%E4%BD%95%E5%90%8C%E6%97%B6%E6%BF%80%E6%B4%BB%E4%B8%A4%E4%B8%AA%E5%AD%90%E5%BA%94%E7%94%A8%EF%BC%9F

不同的子应用的 render 实现不一样就不会覆盖了

我试着把react16和react15的activeRule,改成genActiveRule(/react),也改了singular为false,但访问/react16这个路由的时候,会报错 image

wisdomG avatar Apr 02 '20 07:04 wisdomG

主应用把标签信息存在了本地存储,然后子应用的container都存在了主应用一个对象里,一个应用一个key,切换路由即切换应用时切换container,子应用的unmonunt函数不销毁子应用。子应用用的keepalive缓存组件状态。子应用通过监听路由变化确定什么时候取消缓存并修改主应用的标签本地缓存信息。这样表面上是实现了,不知道有没有什么性能问题。 常见问题里同时激活两个子应用的场景感觉不太适合多页签场景, @kuitos 能否知道下具体实现该场景的思路呢

gaorubin1990 avatar Apr 05 '20 02:04 gaorubin1990

不行 第一个子应用请求js 域名错误 请求到第二个子应用的域名下了。。。。。

see365 avatar Apr 11 '20 11:04 see365

see https://qiankun.umijs.org/zh/faq/#%E5%A6%82%E4%BD%95%E5%90%8C%E6%97%B6%E6%BF%80%E6%B4%BB%E4%B8%A4%E4%B8%AA%E5%AD%90%E5%BA%94%E7%94%A8%EF%BC%9F

不同的子应用的 render 实现不一样就不会覆盖了 不行 第一个子应用请求js 域名错误 请求到第二个子应用的域名下了。。。。。

see365 avatar Apr 11 '20 11:04 see365

有没有哪位大佬解决了这个问题啊

Clive-Wang avatar Apr 14 '20 05:04 Clive-Wang

我解决这个问题了。我的主应用是JQuery(因为一些技术原因暂时保留JQuery),子应用是Vue。Tab切换,会为每个tab页创建一个div容器,我们的子应用页面(Vue)显示在容器中,相应的其他tab页被隐藏,同时切换路由。要实现tab页切换,并保持页面缓存功能,可以换个思路。Vue的keep-alive可以做页面缓存,并且Vue只有一个根实例对象,挂载在一个dom节点上,其内部状态都在实例对象上。因此,在tab切换时,我们可以将dom节点,从一个容器移动到另一个容器,同时随着路由的切换,页面实现了切换,并且keep-alive生效。 对于React也可以参考实现keep-alive。

Liaoct avatar Apr 15 '20 09:04 Liaoct

angular的有思路吗?

werts avatar Apr 15 '20 12:04 werts

能提供个例子,参考下吗?

Clive-Wang avatar Apr 16 '20 03:04 Clive-Wang

@Liaoct 有例子看下吗?我正好有这个需求

penxu avatar Apr 18 '20 02:04 penxu

我的Vue子应用可以实现Tab切换并保持页面状态,代码如下:

let instance = null;
let router = null;

function render() {
  // 这里必须要new一个新的路由实例,否则无法响应URL的变化。
  router = new VueRouter({
    mode: "hash",
    base: process.env.BASE_URL,
    routes
  });
  if (
    window.__POWERED_BY_QIANKUN__ &&
    window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__
  ) {
    const cachedInstance = window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__;

    // 从最初的Vue实例上获得_vnode
    const cachedNode =
      // (cachedInstance.cachedInstance && cachedInstance.cachedInstance._vnode) ||
      cachedInstance._vnode;

    // 让当前路由在最初的Vue实例上可用
    router.apps.push(...cachedInstance.$router.apps);
    // keepAlive可用
    cachedNode.data.keepAlive = true;

    instance = new Vue({
      router,
      store,
      render: () => cachedNode
    });

    // 缓存最初的Vue实例
    instance.cachedInstance = cachedInstance;

    const { path } = router.currentRoute;
    const { path: oldPath } = cachedInstance.$router.currentRoute;
    // 当前路由和上一次卸载时不一致,则切换至新路由
    if (path !== oldPath) {
      cachedInstance.$router.push(path);
    }
    instance.$mount("#app");
  } else {
    // 正常实例化
    instance = new Vue({
      router,
      store,
      render: h => h(App)
    }).$mount("#app");
  }
}

if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap() {
  console.log("[vue] vue app bootstraped");
}

export async function mount(props) {
  console.log("[vue] props from main framework", props);
  render(props);
}

export async function unmount() {
  console.log("[vue] vue app unmount");
  window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__ =
    instance.cachedInstance || instance;
  // 不销毁实例
  // instance.$destroy();
  // instance = null;
  router = null;
}

Liaoct avatar Apr 20 '20 08:04 Liaoct

window.CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE = instance.cachedInstance || instance;

子应用实例都挂载在window,那多个子应用不是会被覆盖吗?

penxu avatar Apr 21 '20 04:04 penxu

window.CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE = instance.cachedInstance || instance;

子应用实例都挂载在window,那多个子应用不是会被覆盖吗?

qiankun为每个子应用创建了一个沙箱环境,子应用的window实际上是window.proxy。另外,你也可以不挂载到window,实例也会一直存在的,加载过的子应用不会再次加载。

Liaoct avatar Apr 21 '20 05:04 Liaoct

const { path } = router.currentRoute;
const { path: oldPath } = cachedInstance.$router.currentRoute;
// 当前路由和上一次卸载时不一致,则切换至新路由
if (path !== oldPath) {
  cachedInstance.$router.push(path);
}

当Router导航未完成就进行路由修正可能会引发路由不匹配的问题。修改方式如下:

router.onReady(() => {
      const { path } = router.currentRoute;
      const { path: oldPath } = cachedInstance.$router.currentRoute;
      // 当前路由和上一次卸载时不一致,则切换至新路由
      if (path !== oldPath) {
        cachedInstance.$router.push(path);
      }
});

Liaoct avatar Apr 21 '20 06:04 Liaoct

window.CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE = instance.cachedInstance || instance;

子应用实例都挂载在window,那多个子应用不是会被覆盖吗?

qiankun为每个子应用创建了一个沙箱环境,子应用的window实际上是window.proxy。另外,你也可以不挂载到window,实例也会一直存在的,加载过的子应用不会再次加载。

那这样,父应用要想获取子应用的一下属性,比如路由name来表示window.title,有什么办法吗?

penxu avatar Apr 21 '20 07:04 penxu

我的Vue子应用可以实现Tab切换并保持页面状态,代码如下:

let instance = null;
let router = null;

function render() {
  // 这里必须要new一个新的路由实例,否则无法响应URL的变化。
  router = new VueRouter({
    mode: "hash",
    base: process.env.BASE_URL,
    routes
  });
  if (
    window.__POWERED_BY_QIANKUN__ &&
    window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__
  ) {
    const cachedInstance = window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__;

    // 从最初的Vue实例上获得_vnode
    const cachedNode =
      // (cachedInstance.cachedInstance && cachedInstance.cachedInstance._vnode) ||
      cachedInstance._vnode;

    // 让当前路由在最初的Vue实例上可用
    router.apps.push(...cachedInstance.$router.apps);
    // keepAlive可用
    cachedNode.data.keepAlive = true;

    instance = new Vue({
      router,
      store,
      render: () => cachedNode
    });

    // 缓存最初的Vue实例
    instance.cachedInstance = cachedInstance;

    const { path } = router.currentRoute;
    const { path: oldPath } = cachedInstance.$router.currentRoute;
    // 当前路由和上一次卸载时不一致,则切换至新路由
    if (path !== oldPath) {
      cachedInstance.$router.push(path);
    }
    instance.$mount("#app");
  } else {
    // 正常实例化
    instance = new Vue({
      router,
      store,
      render: h => h(App)
    }).$mount("#app");
  }
}

if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap() {
  console.log("[vue] vue app bootstraped");
}

export async function mount(props) {
  console.log("[vue] props from main framework", props);
  render(props);
}

export async function unmount() {
  console.log("[vue] vue app unmount");
  window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__ =
    instance.cachedInstance || instance;
  // 不销毁实例
  // instance.$destroy();
  // instance = null;
  router = null;
}

这个加载过的子应用实例会一致存在,不会被销毁,当有些情况下需要销毁掉子应用实例,重新生成一个全新的,有什么好的方法吗

theshying avatar Apr 23 '20 01:04 theshying

我的Vue子应用可以实现Tab切换并保持页面状态,代码如下:

let instance = null;
let router = null;

function render() {
  // 这里必须要new一个新的路由实例,否则无法响应URL的变化。
  router = new VueRouter({
    mode: "hash",
    base: process.env.BASE_URL,
    routes
  });
  if (
    window.__POWERED_BY_QIANKUN__ &&
    window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__
  ) {
    const cachedInstance = window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__;

    // 从最初的Vue实例上获得_vnode
    const cachedNode =
      // (cachedInstance.cachedInstance && cachedInstance.cachedInstance._vnode) ||
      cachedInstance._vnode;

    // 让当前路由在最初的Vue实例上可用
    router.apps.push(...cachedInstance.$router.apps);
    // keepAlive可用
    cachedNode.data.keepAlive = true;

    instance = new Vue({
      router,
      store,
      render: () => cachedNode
    });

    // 缓存最初的Vue实例
    instance.cachedInstance = cachedInstance;

    const { path } = router.currentRoute;
    const { path: oldPath } = cachedInstance.$router.currentRoute;
    // 当前路由和上一次卸载时不一致,则切换至新路由
    if (path !== oldPath) {
      cachedInstance.$router.push(path);
    }
    instance.$mount("#app");
  } else {
    // 正常实例化
    instance = new Vue({
      router,
      store,
      render: h => h(App)
    }).$mount("#app");
  }
}

if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap() {
  console.log("[vue] vue app bootstraped");
}

export async function mount(props) {
  console.log("[vue] props from main framework", props);
  render(props);
}

export async function unmount() {
  console.log("[vue] vue app unmount");
  window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__ =
    instance.cachedInstance || instance;
  // 不销毁实例
  // instance.$destroy();
  // instance = null;
  router = null;
}

这个加载过的子应用实例会一致存在,不会被销毁,当有些情况下需要销毁掉子应用实例,重新生成一个全新的,有什么好的方法吗

这个条件一般都是来自主应用吧?那就是应用间通信的问题了。如果是子应用控制的,那就是在重新执行mount生命周期时加个控制参数。

Liaoct avatar Apr 23 '20 01:04 Liaoct

对于React子应用,我通过react-activation,也实现了页面缓存,暂时还没发现什么问题。Angular不太熟悉,没啥建议了。

Liaoct avatar Apr 23 '20 02:04 Liaoct

我的主子应用都是vue的,目前的场景是这样的,主应用是后台多tab的应用,每个tab都加载子应用的不同界面,现在想要达到的效果是,切换tab时缓存住子应用的界面,这点你上面的建议已经能实现,现在最重要的问题是,当我关闭了一个tab,就需要将这个tab的界面从子应用实例中的缓存中移除,不需要再缓存了,好像没有太好的解决办法

theshying avatar Apr 23 '20 03:04 theshying

对于React子应用,我通过react-activation,也实现了页面缓存,暂时还没发现什么问题。Angular不太熟悉,没啥建议了。

@Liaoct react的实现方法, 可以指点一下么

tianctrl avatar Apr 28 '20 06:04 tianctrl

react有什么解决方案么 各位

tianctrl avatar Apr 29 '20 08:04 tianctrl

抱歉各位,最近比较忙,手上几个项目跟着的,React缓存还没研究完。上次Vue的方案,还有一点问题,activateddeactivated不会触发。修改如下:

let instance = null;
let router = null;

/* eslint-disable no-underscore-dangle */

function render() {
  // 这里必须要new一个新的路由实例,否则无法响应URL的变化。
  router = new VueRouter({
    mode: 'hash',
    base: process.env.BASE_URL,
    routes
  });

  if (window.__POWERED_BY_QIANKUN__ && window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__) {
    const cachedInstance = window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__;

    // 从最初的Vue实例上获得_vnode
    const cachedNode =
      // (cachedInstance.cachedInstance && cachedInstance.cachedInstance._vnode) ||
      cachedInstance._vnode;

    // 让当前路由在最初的Vue实例上可用
    router.apps.push(...cachedInstance.$router.apps);

    instance = new Vue({
      router,
      store,
      i18n,
      render: () => cachedNode
    });

    // 缓存最初的Vue实例
    instance.cachedInstance = cachedInstance;

    router.onReady(() => {
      const { path } = router.currentRoute;
      const { path: oldPath } = cachedInstance.$router.currentRoute;
      // 当前路由和上一次卸载时不一致,则切换至新路由
      if (path !== oldPath) {
        cachedInstance.$router.push(path);
      }
    });
    instance.$mount('#app');
  } else {
    // 正常实例化
    instance = new Vue({
      router,
      store,
      i18n,
      render: h => h(App)
    }).$mount('#app');
  }
}

if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap() {
  console.log('[vue] vue app bootstraped');
}

export async function mount(props) {
  console.log('[vue] props from main framework', props);
  render();
}

export async function unmount() {
  console.log('[vue] vue app unmount');
  const cachedInstance = instance.cachedInstance || instance;
  window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__ = cachedInstance;
  const cachedNode = cachedInstance._vnode;
  // 让keep-alive可用
  cachedNode.data.keepAlive = true;
  // 卸载当前实例,缓存的实例由于keep-alive生效,将不会真正被销毁,从而触发activated与deactivated
  instance.$destroy();
  router = null;
}

Liaoct avatar May 08 '20 05:05 Liaoct

抱歉各位,最近比较忙,手上几个项目跟着的,React缓存还没研究完。上次Vue的方案,还有一点问题,activateddeactivated不会触发。修改如下:

let instance = null;
let router = null;

/* eslint-disable no-underscore-dangle */

function render() {
  // 这里必须要new一个新的路由实例,否则无法响应URL的变化。
  router = new VueRouter({
    mode: 'hash',
    base: process.env.BASE_URL,
    routes
  });

  if (window.__POWERED_BY_QIANKUN__ && window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__) {
    const cachedInstance = window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__;

    // 从最初的Vue实例上获得_vnode
    const cachedNode =
      // (cachedInstance.cachedInstance && cachedInstance.cachedInstance._vnode) ||
      cachedInstance._vnode;

    // 让当前路由在最初的Vue实例上可用
    router.apps.push(...cachedInstance.$router.apps);

    instance = new Vue({
      router,
      store,
      i18n,
      render: () => cachedNode
    });

    // 缓存最初的Vue实例
    instance.cachedInstance = cachedInstance;

    router.onReady(() => {
      const { path } = router.currentRoute;
      const { path: oldPath } = cachedInstance.$router.currentRoute;
      // 当前路由和上一次卸载时不一致,则切换至新路由
      if (path !== oldPath) {
        cachedInstance.$router.push(path);
      }
    });
    instance.$mount('#app');
  } else {
    // 正常实例化
    instance = new Vue({
      router,
      store,
      i18n,
      render: h => h(App)
    }).$mount('#app');
  }
}

if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap() {
  console.log('[vue] vue app bootstraped');
}

export async function mount(props) {
  console.log('[vue] props from main framework', props);
  render();
}

export async function unmount() {
  console.log('[vue] vue app unmount');
  const cachedInstance = instance.cachedInstance || instance;
  window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__ = cachedInstance;
  const cachedNode = cachedInstance._vnode;
  // 让keep-alive可用
  cachedNode.data.keepAlive = true;
  // 卸载当前实例,缓存的实例由于keep-alive生效,将不会真正被销毁,从而触发activated与deactivated
  instance.$destroy();
  router = null;
}

这个方案好像也会有问题,场景是这样到,子应用有两个路由a,b,目前从子应用路由a去到主应用路由后,回到子应用路由a,a切换到路由b会出现一个问题,子应用到内容还是a的。然后我发现这个问题只有子应用真正执行了instance.$destroy()后才不会有这种内容不变的情况。请教一下,要做到可以缓存而且不会出现以上的情况应该怎么处理?

zjhr avatar Jun 09 '20 04:06 zjhr

抱歉各位,最近比较忙,手上几个项目跟着的,React缓存还没研究完。上次Vue的方案,还有一点问题,activateddeactivated不会触发。修改如下:

let instance = null;
let router = null;

/* eslint-disable no-underscore-dangle */

function render() {
  // 这里必须要new一个新的路由实例,否则无法响应URL的变化。
  router = new VueRouter({
    mode: 'hash',
    base: process.env.BASE_URL,
    routes
  });

  if (window.__POWERED_BY_QIANKUN__ && window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__) {
    const cachedInstance = window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__;

    // 从最初的Vue实例上获得_vnode
    const cachedNode =
      // (cachedInstance.cachedInstance && cachedInstance.cachedInstance._vnode) ||
      cachedInstance._vnode;

    // 让当前路由在最初的Vue实例上可用
    router.apps.push(...cachedInstance.$router.apps);

    instance = new Vue({
      router,
      store,
      i18n,
      render: () => cachedNode
    });

    // 缓存最初的Vue实例
    instance.cachedInstance = cachedInstance;

    router.onReady(() => {
      const { path } = router.currentRoute;
      const { path: oldPath } = cachedInstance.$router.currentRoute;
      // 当前路由和上一次卸载时不一致,则切换至新路由
      if (path !== oldPath) {
        cachedInstance.$router.push(path);
      }
    });
    instance.$mount('#app');
  } else {
    // 正常实例化
    instance = new Vue({
      router,
      store,
      i18n,
      render: h => h(App)
    }).$mount('#app');
  }
}

if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap() {
  console.log('[vue] vue app bootstraped');
}

export async function mount(props) {
  console.log('[vue] props from main framework', props);
  render();
}

export async function unmount() {
  console.log('[vue] vue app unmount');
  const cachedInstance = instance.cachedInstance || instance;
  window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__ = cachedInstance;
  const cachedNode = cachedInstance._vnode;
  // 让keep-alive可用
  cachedNode.data.keepAlive = true;
  // 卸载当前实例,缓存的实例由于keep-alive生效,将不会真正被销毁,从而触发activated与deactivated
  instance.$destroy();
  router = null;
}

这个方案好像也会有问题,场景是这样到,子应用有两个路由a,b,目前从子应用路由a去到主应用路由后,回到子应用路由a,a切换到路由b会出现一个问题,子应用到内容还是a的。然后我发现这个问题只有子应用真正执行了instance.$destroy()后才不会有这种内容不变的情况。请教一下,要做到可以缓存而且不会出现以上的情况应该怎么处理?

我修复了一下 暂时没问题(既保存了状态,也可以正常触发路由更新以及生命周期)

// unmount-function
const cachedInstance = instance.cachedInstance || instance
  /**
   * 1. 本地缓存cachedInstance._vnode
   * 2. 测试activated && deactivated生命周期
   */
  if (!cachedInstance._vnode.data.keepAlive) cachedInstance._vnode.data.keepAlive = true
  cachedInstance.catchRoute = {
    apps: [...instance.$router.apps]
  }
  instance.$destroy()
  router = null
  instance.$router.apps = []
...
// render-function
router.apps.push(...cachedInstance.catchRoute.apps)

dengBox avatar Jun 11 '20 06:06 dengBox

楼上给出了一个可用的方案。 我最新的方法如下:

export async function unmount() {
  console.log('[vue] vue app unmount');
  const cachedInstance = instance.cachedInstance || instance;
  window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__ = cachedInstance;
  const cachedNode = cachedInstance._vnode;
  cachedNode.data.keepAlive = true;
  cachedNode.data.hook.destroy(cachedNode);
  if (instance.cachedInstance) {
    instance.$destroy();
    instance = null;
  }
  router = null;
}

这两种方法都可以实现keep-alive生命周期正常执行。

但是存在一个小问题: 子应用有a,b路由,先进入a,再进入b,再切换到主应用(切出该子应用,此时子应用的路由实例停留在路由b),再切换回子应用a。此时的过程,先激活路由b(因为原本停留在b),再切换至a。

如果在生命周期里做了请求等操作,可能引起不必要的操作。

Liaoct avatar Jun 15 '20 09:06 Liaoct

使用 loadMicroApp 方法手动控制微应用的 mount unmount 应该就可以了

kuitos avatar Jun 16 '20 13:06 kuitos

子应用有a,b路由,先进入a,再进入b,再切换到主应用(切出该子应用,此时子应用的路由实例停留在路由b),再切换回子应用a。此时的过程,先激活路由b(因为原本停留在b),再切换至a。

我从主应用切换回a后,页面还是b的内容,没有跳转到a

Jonny-china avatar Jun 26 '20 13:06 Jonny-china