微前端乾坤(qiankun)的后台标签页功能实现

2,212 阅读3分钟

本文章仅记录我的开发历程,想要干货的请参考我下面的链接,本文可以略过了

react主应用增加tabs标签的逻辑

这里可以参考上面第一个链接,但是由于文章里的页面缓存实现是在父应用里实现的多例子应用,即每个页面都是一个单独的实例,页面多的情况下应该会有性能问题。所以只抄袭借鉴了这个文章里的tabs标签页的操作逻辑,页面缓存功能另寻出路。

子应用增加页面缓存

参考上面第二个链接,实现思路为在子应用unmount时,不调用vue或react的卸载事件,将rootDom append到document.body里面,并用样式隐藏,子应用重新mount时,将隐藏的rootDom显示

vue子应用

入口改造

let rootDom: Element | undefined;

async function render(props: any) {
  console.log("render");
  const { container } = props;
  const root = createApp(VueApp);
  root.use(createPinia());
  root.use(router);
  root.provide("appStore", props.appStore);
  rootDom = container ? container.querySelector("#root") : document.getElementById("root");
  root.mount(rootDom!);
}

renderWithQiankun({
  update() {},
  mount(props) {
    console.log("mount", props);
    props?.setLoading(false);
    if (!rootDom) {
      render(props);
    } else {
      rootDom!.setAttribute("display", "block");
      props.container?.parentNode?.appendChild(rootDom!);
    }
  },
  bootstrap() {
    console.log("bootstrap");
  },
  unmount(props) {
    console.log("unmount", props);
    rootDom!.setAttribute("display", "none");
    document.body.appendChild<Element>(rootDom!);
  },
});

keep-alive实现

//layout.vue 
const cacheNameList = []
<template>
  <RouterView v-slot="{ Component }">
     <KeepAlive :include="cacheNameList" :max="10">
       <component :is="Component"></component>
     </KeepAlive>
  </RouterView>
</template>

react子应用

入口改造

//请参考vue的

由于react没有原生的KeepAlive组件,使用的是react-activation来实现页面缓存功能,如果需要实现类似vue的maxAPI,可以参考这里

// layout.tsx
import { KeepAlive, useAliveController } from "react-activation";
const MAX_CACHE = 10;
const keys: Set<string> = new Set();

const LayoutInner = () => {
  const routes = useLocation();
  const controller = useAliveController();

  const pushKeys = (key: string) => {
    const cachePath = keys.has(key);
    if (cachePath) {
      keys.delete(key);
      keys.add(key);
    } else {
      keys.add(key);
      if (keys.size > MAX_CACHE) {
        const deleteKey = keys.values().next().value;
        keys.delete(deleteKey);
        controller.drop(deleteKey);
      }
    }
  };
  const dom = <Outlet />;

  if (canCache)) {
    pushKeys(routes.pathname);
    return (
      <KeepAlive id={routes.pathname} name={routes.pathname}>
        {dom}
      </KeepAlive>
    );
  }
  return dom;
};

遇到的问题

  • vue子应用回退异常:原因是vue-router依赖了history.state,具体的原因请看这里
  • 场景:从子级页面增加一项数据,回到一级页面,由于做了页面缓存,新增的数据并不会出现在页面上,是一个合理的bug。合理的解决方案:参考ProTable - 高级表格 - ProComponents (ant.design)revalidateOnFocus,在窗口聚焦时重新请求接口
  • refreshTab刷新tab:原贴里使用了改变key的方式rerender页面,但是我们由于有页面缓存,页面加载时的接口等不会重新请求,还没有想到一个同时兼容vue和react的方案,暂时使用window.location.reload()代替
  • ……

2024.02.28更新:关于vue-router 的问题后续

我的临时解决方案

import qs from "query-string";
import { qiankunWindow } from "vite-plugin-qiankun/dist/helper";
import { useRouter as vueUseRouter, type RouteLocationPathRaw } from "vue-router";

export function push(subApp: string) {
  const url = location.origin + subApp;
  //console.log(location, history, url);
  history.pushState(null, subApp, url);
}

type RouteLocationRaw = string | RouteLocationPathRaw;

const BASE_PATH = "";

const router = {
  push: (to: RouteLocationRaw) => {
    const isObject = typeof to === "object";
    const toPath = isObject ? to.path : to;
    let path: string = BASE_PATH + toPath;
    if (!toPath.startsWith("/")) {
      path = window.location.pathname.split("/").slice(0, -1).join("/") + "/" + toPath;
    }
    const toUrl = qs.stringifyUrl({ url: path, query: isObject ? to.query : {} });
    push(toUrl);
  },
  back: () => {
    history.back();
  },
};

const useRouter = qiankunWindow.__POWERED_BY_QIANKUN__
  ? () => {
      return router;
    }
  : vueUseRouter;

export { useRouter };