本文将从0基于qiankun这个微前端框架,实现子应用多页签
引言
-
为什么需要子应用 Keep-Alive?
- 减少重复渲染,提升用户体验
- 保持子应用状态(如表单数据、滚动位置)
-
qiankun
默认行为:子应用卸载会销毁实例,每次切换页面都会导致子应用卸载、重新加载,会导致页面切换的过程中出现短暂白屏以及重新执行子应用资源导致的性能问题
效果预览

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

-
页面整体路由切换由基座控制,根据页面url来匹配当前激活的是哪个子应用
-
在基座声明各个菜单的URL
jsmenuData: [ { 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" }, ],
-
在基座声明几个组件,用于存放子应用的容器,这样方便我们在基座使用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>
-
对刚刚声明用来存放子应用的组件使用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>
-
子应用之间的路由切换实现方式:通过使用基座派发下来的路由跳转方法,而不是使用子应用自身的路由,在下面代码中就是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>