1. 首页
  2. Vue

基于micro-app实现多页签微前端记录

之前学习了一些关于微前端的知识,现在准备进行实践。

作者:白水清风
链接:https://juejin.cn/post/7552489208805785652
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

image.png

需求

假设有一个管理系统,这个系统中包含多个模块如系统财务运营供应链等,随着时间的推移,人员变更和技术迭代,这个管理系统也是不断的升级丰富同时体积也变得越来越大,打包构建时长不断增加,系统整体风险增加,逐渐的就演变成了"巨石前端"项目。那最简单的方法就是把各模块全部独立做成系统然后实现一个单点登录方便各系统之间切换。但其实这样并不好,用户的心智负担会增加,项目的管理过于松散,以及开发复用等问题。所以还是要保持基于菜单和页签进行切换又要实现应用的拆分。

目标

把原来“巨石前端”拆成一个基座应用和多个子应用,基座应用负责布局,页面切换,向子应用提供共享信息等功能,先不考虑子应用独立运行问题。如果所有的应用要使用相同的基础依赖也可以使用 pnpm + pnpm-workspace monorepo 的方式管理,这里也先不考虑。那最主要解决的问题就两个,1、路由跳转,2、页面保活

那由于之前的学习,我发现micro-app应该是最方便实现这一目标的微前端框架,因为它具有虚拟路由系统,应用保活,以及支持Vite子应用等特性。

实践

准备项目

这里我从github上找了一个简约的管理项目,v3-admin-vite, 基于 Vue3、Vite、TypeScript、Element Plus 等技术栈。

这里我们直接clone三份下来,分别命名为base-appmicro-app1micro-app2、安装依赖、各应用创建对应的路由菜单,更改对应启动端口为3333,3334,3335,然后按照 micro-app 的文档开始安装和配置


基础配置

主应用

import microApp from "@micro-zoe/micro-app"
microApp.start()
// app1.vue
<template>
  <div>
    <h1>子应用1👇</h1>
    <micro-app name="app1" url="http://localhost:3334/" iframe keep-alive keep-router-state  router-mode="native" />
  </div>
</template>

由于我们要接入的子应用是Vite构建的,所以这里要采用 iframe 沙箱,路由模式选择nativenative模式下子应用完全基于浏览器路由系统进行渲染

这边建议把路由模式改成history模式,否则需要更加复杂的路由配置,详情参考browser-router

子应用

首先创建一个新的布局组件,用于在微前端框架下使用

// layouts/ParentView
<script lang="ts" setup>
import { useSettingsStore } from "@/pinia/stores/settings";
import { Footer } from "./components/index";
const cachedViews = ref<string[]>([]);
const visitedViews = ref<string[]>([]);
const settingsStore = useSettingsStore();
</script>

<template>
  <section class="app-main">
    <div class="app-scrollbar">
      <router-view v-slot="{ Component, route }">
        <keep-alive :include="cachedViews">
          <component
            :is="Component"
            :key="route.path"
            class="app-container-grow"
          />
        </keep-alive>
      </router-view>
      <!-- 页脚 -->
      <Footer v-if="settingsStore.showFooter" />
    </div>

    <!-- 返回顶部 -->
    <el-backtop />

    <!-- 返回顶部(固定 Header 情况下) -->
    <el-backtop target=".app-scrollbar" />
  </section>
</template>

<style lang="scss" scoped>
@import "@@/assets/styles/mixins.scss";

.app-main {
  width: 100%;
  display: flex;
}

.app-scrollbar {
  flex-grow: 1;
  overflow: auto;
  @extend %scrollbar;
  display: flex;
  flex-direction: column;
  .app-container-grow {
    flex-grow: 1;
  }
}
</style>
{
  path: "/app1",
  component: window.__MICRO_APP_ENVIRONMENT__ ? parentView : Layouts,
  redirect: "/app1/page1",
  name: "app1",
  meta: {
  title: "微应用2",
  elIcon: "Lock",
  },
  children: [
    {
      path: "page1",
      component: () => import("@/pages/app1/page1.vue"),
      name: "App2page1",
      meta: {
        title: "页面1"
      }	
    },	
    {
      path: "page2",
      component: () => import("@/pages/app1/page2.vue"),
      name: "App2page2",
      meta: {
        title: "页面2"
      }
    },
    {
      path: "page3",
      component: () => import("@/pages/app1/page3.vue"),
      name: "App2page3",
      meta: {
        title: "页面3"
      }
    } 
  ]
}

至此我们可以通过菜单渲染出子应用,但是会发现两个问题

  1. 切换菜单时没有切换到子应用的界面

  2. cachedViews没有值,不会保活

菜单切换

由于我们使用了 keep-alive的功能,在打开该应用其他菜单时,并不会触发新的渲染,所以菜单切换后还是原来的界面。此时我们需要手动进行路由导航。

进行手动导航有两种方式,1、主应用控制子应用跳转;2、子应用内部使用vue-router路由跳转

第一种情况有主应用控制的方式,需要再子应用挂载后和卸载前,负责会出现这样的警告导航失败,请确保子应用渲染后再调用此方法。 使用第二种更加的稳定,我们只需要通知子应用导航到哪个路由路径即可

主应用

<script setup lang="ts">
import microApp from "@micro-zoe/micro-app";
import { useRoute } from "vue-router";
import { useTagsViewStore } from "@/pinia/stores/tags-view";

const route = useRoute();
const tagsViewStore = useTagsViewStore();

onActivated(() => {
  microApp.setData("app1", {
    currentPath: fullPath.value,
    cachedViews: tagsViewStore.cachedViews,
    visitedViews: tagsViewStore.visitedViews,
    type: "onActivated",
  });
});

onDeactivated(() => {
  microApp.setData("app1", {
    currentPath: route.fullPath,
    cachedViews: tagsViewStore.cachedViews,
    visitedViews: tagsViewStore.visitedViews,
    type: "onDeactivated",
  });
});
</script>

子应用

// ParentView
// 监听数据变化,初始化时如果有数据则主动触发一次
function dataListener(data: any) {
  if (data) {
    cachedViews.value = data.cachedViews.filter((item: string) => item.startsWith("App1"))
    visitedViews.value = data.visitedViews.filter((item: any) => item.name.startsWith("App1"))
    if (data.type === "onActivated") {
      router.push({ path: data.currentPath })
    } else if (data.type === "onDeactivated" && cachedViews.value.length === 0 && visitedViews.value.length === 0) {
      router.push("/")
    }
  } else {
    cachedViews.value = []
  }
}

至此我们就可以导航到具体的界面的

页面保活

micro-app的keep-alive是应用级别的,它只会保留当前正在活动的页面状态,如果想要缓存具体的页面或组件,需要使用子应用框架的能力,如:vue的keep-alive。

我们只需要监听主应用的消息,更新 cachedViews即可

子应用

cachedViews.value = data.cachedViews.filter((item: string) => item.startsWith("App1"))

当我们关掉子应用的所有页面的时候,也需要触发一次导航到首页或空页面。这是因为如果我们不这么做的话,我们关闭的子应用最后一个页面是我们下一次打开的第一个页面,就会保留着上一次的缓存无法被清除。常见的系统页签关闭之后,下次再打开都是新的,不使用缓存的。

如果我们是在当前激活的子应用时关闭当前应用的页签,以上是没有问题的,但如果此时我们激活的是子应用2,子应用1是保活状态且存在页签,此时我们去关闭子应用1标签,子应用1由于是保活状态是无法收到信息的,也就会保留着上一次的缓存无法被清除。此时我们需要通过主应用监听激活的页签,如果某个应用没有激活的页签,则卸载该应用

// layouts
const tagsViewStore = useTagsViewStore()
watch(() => tagsViewStore.visitedViews, () => {
  const allApp = microApp.getAllApps()
  const activeApp = microApp.getActiveApps({ excludeHiddenApp: true })
  const activePathArr = [...new Set(tagsViewStore.visitedViews.map(item => item.meta?.appName))]
  allApp.forEach((appName) => {
    if (!activeApp.includes(appName) && !activePathArr.includes(appName)) {
      //  clearAliveState 如果子应用是keep-alive应用,则卸载并清空状态
      microApp.unmountApp(appName, { destroy: false, clearAliveState: true }).then(() => {
        console.log("子应用卸载成功")
      })
    }
  })
}, { immediate: true, deep: true })

结语

以上就是一个基于micro-app的多页签,页面保活的方案。只是简单跑了一下,并没有发现更加深入的问题,但至少证明Vue3+Vite应该是没有问题的。 在此之前,我也基于qiankun去做了一下,基本思路是同一时间仅加载一个子应用,离开该应用时将vnode和路由进行缓存,激活时再使用缓存的vnode挂载及路由导航。但是这个只支持Vue2。在Vue3中,createApp无法这样使用。在Vue3中要实现页面缓存,只能存DOM,在激活的时候把上次的DOMappendChild到容器中,相当于手动控制显示隐藏,这样的做法不够好,所以就没有记录。

最后,如果你有很好的方法并且乐意分享的话,在下洗耳恭听。共勉


TOP