基于qiankun实现子应用菜单级keep-alive

本文将从0基于qiankun这个微前端框架,实现子应用多页签

引言

  • 为什么需要子应用 Keep-Alive?

    • 减少重复渲染,提升用户体验
    • 保持子应用状态(如表单数据、滚动位置)
  • qiankun 默认行为:子应用卸载会销毁实例,每次切换页面都会导致子应用卸载、重新加载,会导致页面切换的过程中出现短暂白屏以及重新执行子应用资源导致的性能问题

效果预览

整体思路

基座使用vue的内置keep-alive组件,缓存整个子应用,子应用根据自身技术栈,选择不同方案,vue使用keep-alive组件,react可使用react-activation 这个社区库

  1. 页面整体路由切换由基座控制,根据页面url来匹配当前激活的是哪个子应用

  2. 在基座声明各个菜单的URL

    js 复制代码
    menuData: [
      { name: "主应用的Home页面", value: "/" },
      { name: "子应用Vue2的home页面", value: "/app-vue2-demo" },
      { name: "子应用Vue2的users页面", value: "/app-vue2-demo/users" },
      { name: "子应用Vue2的about页面", value: "/app-vue2-demo/about" },
      { name: "子应用React的home页面", value: "/app-react-demo" },
      { name: "子应用React的about页面", value: "/app-react-demo/about" },
      { name: "子应用React的info页面", value: "/app-react-demo/info" },
    ],
  3. 在基座声明几个组件,用于存放子应用的容器,这样方便我们在基座使用keep-alive组件对子应用进行包裹

    js 复制代码
     <!-- AppVue2Demo.vue -->
    <template>
      <div id="app-vue2-demo"></div>
    </template>
    ​
    <script>
    import { loadMicroApp } from "qiankun";
    ​
    export default {
      name: "AppVue2Demo",
      props: ["pushState"],
      data() {
        return {
          qiankunInstance: null,
        };
      },
      mounted() {
        this.qiankunInstance = loadMicroApp(
          {
            name: "app-vue2-demo",
            entry: "//localhost:4001",
            container: "#app-vue2-demo",
            props: {
              routerBase: "/app-vue2-demo",
              mainPushState: this.pushState,
            },
          },
          {
            sandbox: {
              experimentalStyleIsolation: true,
            },
          }
        );
      },
    };
    </script>
    ​
    <style lang="scss" scoped></style>
    ​
  4. 对刚刚声明用来存放子应用的组件使用keep-alive包一下,为了方便基座其他路由的展示,这里简单的做了一个判断,点击左侧菜单或者顶部tab来切换当前显示的子应用

    js 复制代码
    <div v-show="$route.path.startsWith('/app')">
      <keep-alive>
        <component
          :is="currentComponent"
          :pushState="mainPushState"
        ></component>
      </keep-alive>
    </div>
    <!-- 下面用于展示基座其他路由 -->
      <router-view></router-view>
  5. 子应用之间的路由切换实现方式:通过使用基座派发下来的路由跳转方法,而不是使用子应用自身的路由,在下面代码中就是mainPushState方法,将该方法传递到子应用中

    js 复制代码
    // vue子应用的main.js
    // 保存基座传递下来的props
    export async function mount(props) {
      console.log("vue2 子应用 mount");
    ​
      Vue.prototype.$parentProps = props;
      render(props);
    }
    ​
    ​
    //vue子应用的about.vu
    <template>
      <div>
        <h2>About页面</h2>
        <button @click="handleRouter">跳转到react子应用的About页面</button>
      </div>
    </template>
    ​
    <script>
    export default {
      name: "About",
      data() {
        return {};
      },
      methods: {
        handleRouter() {
          this.$parentProps.mainPushState("/app-react-demo/about");
        },
      },
    };
    </script>
基座完整代码
js 复制代码
<template>
  <div id="app">
    <el-container>
      <el-container>
        <el-aside width="200px">
          <el-menu
            @select="changeMenu"
            :default-active="currentTab"
            class="el-menu-vertical-demo"
            router
          >
            <el-menu-item
              v-for="item in menuData"
              :index="item.value"
              :key="item.value"
            >
              <template #title>{{ item.name }}</template>
            </el-menu-item>
          </el-menu>
        </el-aside>
        <el-container>
          <el-header>Micro-App</el-header>
          <el-main>
            <el-tabs
              v-model="currentTab"
              closable
              type="card"
              @tab-click="changeTab"
            >
              <el-tab-pane
                v-for="item in allTabs"
                :label="item.name"
                :name="item.value"
                :key="item.value"
              >
              </el-tab-pane>
            </el-tabs>
​
            <!-- 这里展示子应用 -->
            <div v-show="$route.path.startsWith('/app')">
              <keep-alive>
                <component
                  :is="currentComponent"
                  :pushState="mainPushState"
                ></component>
              </keep-alive>
            </div>
​
            <!-- 下面用于展示基座其他路由 -->
            <keep-alive>
              <router-view></router-view>
            </keep-alive>
          </el-main>
        </el-container>
      </el-container>
    </el-container>
  </div>
</template>
​
<script>
import AppVue2Demo from "@/views/AppVue2Demo.vue";
import AppReactDemo from "@/views/AppReactDemo.vue";
export default {
  name: "App",
  data() {
    return {
      currentComponent: null,
      menuData: [
        { name: "主应用的Home页面", value: "/" },
        { name: "子应用Vue2的home页面", value: "/app-vue2-demo" },
        { name: "子应用Vue2的users页面", value: "/app-vue2-demo/users" },
        { name: "子应用Vue2的about页面", value: "/app-vue2-demo/about" },
        { name: "子应用React的home页面", value: "/app-react-demo" },
        { name: "子应用React的about页面", value: "/app-react-demo/about" },
        { name: "子应用React的info页面", value: "/app-react-demo/info" },
      ],
      components: [
        {
          path: "/app-vue2-demo",
          component: AppVue2Demo,
        },
        {
          path: "/app-react-demo",
          component: AppReactDemo,
        },
      ],
      currentTab: "/",
      allTabs: [],
      microApps: [
        {
          routerBase: "/app-vue2-demo",
        },
        {
          routerBase: "/app-react-demo",
        },
      ],
    };
  },
  methods: {
    mainPushState(path) {
      this.$router.push(path);
      this.changeMenu(path);
    },
    changeMenu(indexPath) {
      this.currentComponent = this.components.find((item) =>
        indexPath.includes(item.path)
      )?.component;
      if (this.currentTab === indexPath && this.currentTab !== "/") return;
​
      //判断是否已经存在tab
      const existTab = this.allTabs.find((item) => item.value === indexPath);
​
      if (existTab) {
        this.currentTab = existTab.value;
      } else {
        //添加到tabs中
        const selectMenu = this.menuData.find(
          (item) => item.value === indexPath
        );
        if (selectMenu) {
          this.allTabs.push(selectMenu);
          this.currentTab = selectMenu.value;
        }
      }
    },
    changeTab(tab) {
      if (tab.name === this.$route.path) return;
      this.currentComponent = this.components.find((item) =>
        tab.name.includes(item.path)
      )?.component;
      this.$router.push(this.currentTab);
    },
    initTab() {
      let { fullPath } = this.$route;
      this.changeMenu(fullPath);
    },
  },
  mounted() {
    this.initTab();
  },
};
</script>
​

本文暂未实现子应用销毁逻辑,后续补充

相关推荐
SunTecTec3 分钟前
Flink Docker Application Mode 命令解析 - 修改命令以启用 Web UI
大数据·前端·docker·flink
软件技术NINI4 分钟前
html css js网页制作成品——HTML+CSS甜品店网页设计(4页)附源码
javascript·css·html
涵信14 分钟前
第十一节:性能优化高频题-响应式数据深度监听问题
javascript·vue.js·性能优化
codingandsleeping39 分钟前
Express入门
javascript·后端·node.js
Vaclee42 分钟前
JavaScript-基础语法
开发语言·javascript·ecmascript
拉不动的猪1 小时前
前端常见数组分析
前端·javascript·面试
小吕学编程1 小时前
ES练习册
java·前端·elasticsearch
Asthenia04121 小时前
Netty编解码器详解与实战
前端
袁煦丞2 小时前
每天省2小时!这个网盘神器让我告别云存储混乱(附内网穿透神操作)
前端·程序员·远程工作
一个专注写代码的程序媛3 小时前
vue组件间通信
前端·javascript·vue.js