主应用
1. 增加匹配所有子路由通配路由
ts
const routes: RouteRecordRaw[] = [
{
path: '/digital-twin/:pathMatch(.*)*', // 子路由全部以/digital-twin/ 前缀开头
name: 'DigitalTwin',
meta: {
icon: 'ic:baseline-view-in-ar',
keepAlive: true,
order: 1000,
title: $t('platform.route.digitalTwinPlatform'),
getTabTitle: createGetTabTitle('/digital-twin'),
},
component: () => import('#/views/sub-app/index.vue'),
},
];
2. 注册、启动子路由
- #/views/sub-app/index.vue
html
// #/views/sub-app/index.vue
<script lang="ts" setup>
import { updatePreferences } from '@vben/preferences';
import { MicroApp } from '#/components/micro-app';
import BasicLayout from '#/layouts/basic.vue';
import { useSubApp } from './useSubapp';
// 判断是否全屏
const route = useRoute();
const isFullScreen = computed(() => route.query?.fullScreen == '1');
// 启动子应用
useSubApp();
</script>
<template>
<!-- 注册子应用 -->
<MicroApp />
<div v-if="isFullScreen" class="sub-app">
<!-- 子应用挂载节点(全屏预览,不需要菜单和顶栏) -->
<div id="digital-twin"></div>
</div>
<BasicLayout v-else>
<template #subAppCounaner>
<div class="sub-app">
<!-- 子应用挂载节点(需要菜单和顶栏) -->
<div id="digital-twin"></div>
</div>
</template>
</BasicLayout>
</template>
<style lang="scss">
.sub-app {
height: calc(100vh - var(--vben-header-height) - var(--vben-footer-height));
overflow: hidden;
}
#digital-twin {
img {
display: inline-block;
}
}
</style>
- 注册:#/components/micro-app
ts
import { registerMicroApps } from 'qiankun';
import { useAuthStore } from '#/store/auth';
import { microAppActions } from './micro-app-actions';
export const MicroApp = defineComponent({
name: 'MicroApp',
setup() {
registerMicroApps([
{
name: 'twinplatform', // app name registered
entry: `//${window.location.hostname}:${import.meta.env.VITE_DIGITAL_TWIN_PORT}${import.meta.env.VITE_BASE}digital-twin/`,
container: '#digital-twin',
activeRule: `${import.meta.env.VITE_BASE}digital-twin`,
props: {
actions: microAppActions,
excludeAssetFilter: (assetUrl: string) => {
// 排除 Vite 插入的热更新脚本
return assetUrl.includes('@react-refresh');
},
},
},
]);
// 监听子应用,登录失效消息
microAppActions.onGlobalStateChange((state, prev) => {
if (!state.isLogin && state.isLogin !== prev.isLogin) {
useAuthStore().logout();
}
});
return () => null;
},
});
- 启动:./useSubapp
ts
import {
onBeforeRouteUpdate,
type RouteLocationNormalizedLoadedGeneric,
useRoute,
} from 'vue-router';
import { useTabbarStore } from '@vben/stores';
import { start } from 'qiankun';
export const useSubApp = () => {
const route = useRoute();
onMounted(() => {
if (!window.qiankunStarted) {
window.qiankunStarted = true;
console.log('start qiankun');
start({
sandbox: true,
});
}
updateTabTitle(route);
});
// 更新顶部tab标签
const updateTabTitle = (to: RouteLocationNormalizedLoadedGeneric) => {
const getTabTitle = to.meta.getTabTitle as (path: string) => string;
const title = getTabTitle(to.fullPath);
useTabbarStore().setTabTitle(to, title);
};
// 路由切换同步更新顶栏tab标签
onBeforeRouteUpdate((to, from) => {
if (to.fullPath == from.fullPath) return;
setTimeout(() => {
updateTabTitle(to);
});
});
};
注意: 1. 子应用挂载节点不要选择和主应用一样的节点(#app), 或者包含主应用(vue3) router-view组件的dom节点,否则会出现子应用返回主应用时,主应用路由失效,页面空白的问题
子应用
1. 安装qiankun vite插件
bash
npm i vite-plugin-qiankun -S
2. 在vite.config.ts中引用
ts
export default defineConfig(({ mode }) => {
...
return {
...
plugins: [
react(),
// 引用插件
qiankun('twinplatform', {
// 子应用名称
useDevMode: true, // 开发模式下启用
}),
]
...
}
}
3. 改造子应用入口文件
ts
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import 'antd/dist/antd.css';
import './index.less';
import { actions } from '@/utils/microAppEvent';
import { renderWithQiankun, qiankunWindow, QiankunLifeCycle, QiankunProps } from 'vite-plugin-qiankun/dist/helper';
let root: ReactDOM.Root;
const render = () => {
const el = document.getElementById('root') as HTMLElement;
root = ReactDOM.createRoot(el);
root.render(<App />);
};
const lifecycle: QiankunLifeCycle = {
mount(props: QiankunProps): void {
// 实现 mount 逻辑
actions.setActions(props.actions);
render();
},
bootstrap(): void {
// 实现 bootstrap 逻辑
console.log('react app bootstraped');
},
unmount(props): void {
// 实现 unmount 逻辑
console.log('react app unmount');
root?.unmount();
},
update(props: QiankunProps): void {
// 实现 update 逻辑
},
};
renderWithQiankun(lifecycle);
// 3. 独立运行逻辑
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
render();
}
注意:1. 必须要使用插件的,renderWithQiankun方法,否则会提示无法找到入口声明周期钩子函数
其他注意点:
主应用,子应用路由匹配、路由前缀、服务前缀
- 子应用和主应用的服务前缀要保持一致,例如著应用服务前缀为:a/b, 则子应用的服务前缀也要包含:a/b, eg:
js
// history模式
子应用地址: localhost:8666/a/b/home
主应用地址:localhost:8999/a/b/child-sub1/home
// 子应用必须包含主应用的服务前缀,否则registerMicroApps注册的子应用激活规则:activeRule: `child-sub1`,无法正常配置激活子应用
// 必须要写成:activeRule: `{主应用服务前缀}/child-sub1`
- 如果有服务前缀,需同步设置路由的baseurl:
ts
// react:
...
<BrowserRouter basename={服务前缀}>
....
</BrowserRouter>
....
// vue3
const router = createRouter({
history: `服务前缀`
// 应该添加到路由的初始路由列表。
routes,
scrollBehavior: (to, _from, savedPosition) => {
if (savedPosition) {
return savedPosition;
}
return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };
},
// 是否应该禁止尾部斜杠。
// strict: true,
});
-
主应用,子应用路由模式要保持一致,histroy或者hash
-
解决history模式刷新404
nginx
location /a/b {
try_files $uri $uri/ /a/b/index.html; # 找不到资源时,返回主应用首页
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization';
}
注意:子应用服刷新时,不能返回子应用的首页(index.html),要返回主应用的index.html,否则会出在子应用页面刷新,丢失主应用问题, 建议最好主应用和子应用部署在同一个域名下