微前端中父子应用间的路由跳转是一个核心且稍微复杂的问题。因为每个应用都有自己的路由系统,我们需要一个机制来协调它们,确保用户体验流畅且状态同步。
Qiankun 提供了一些强大的能力来帮助我们处理这个问题,主要思路是:主应用控制子应用的加载和激活,子应用内部的路由由自己管理,同时通过某些机制让主应用感知到子应用的路由变化(反之亦然),从而实现协调。
这里我们将分两种主要情况来讨论:
- 主应用跳转到子应用内部路由(父 -> 子)
- 子应用内部路由跳转(子内跳转)
- 子应用跳转到主应用路由或另一个子应用路由(子 -> 父 / 子 -> 另一个子)
前提条件
-
主应用和子应用都使用 History 模式的路由: 这几乎是微前端的标配,因为它能提供干净的 URL,并且 Qiankun 能够更好地劫持和管理 URL 变化。
- Vue Router (History 模式):
js
// Vue Router 配置
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(window.__POWERED_BY_QIANKUN__ ? '/vue/' : '/'), // 注意 base 路径
routes: [ /* ... */ ],
});
- React Router (History 模式):
js
// React Router 配置
import { BrowserRouter, HashRouter } from 'react-router-dom';
// 在微前端环境下,建议使用 BrowserRouter,但要确保 base 路径
// 或者更推荐使用 memory history,让子应用内部管理路由,不直接影响主应用 URL
// 但通常我们会让子应用影响主应用 URL,所以 BrowserRouter 配合 basePath 较常用
import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Home from '../pages/Home'; // 假设你有 Home 组件
import Dashboard from '../pages/Dashboard'; // 假设你有 Dashboard 组件
import Settings from '../pages/Settings'; // 假设你有 Settings 组件
import NotFound from '../pages/NotFound'; // 假设你有 NotFound 组件
interface SubAppRouterProps {
// Qiankun 传递的 props,其中包含沙箱环境信息
// 如果在微前端环境中,Qiankun 会将子应用的 publicPath 作为 basename 传递
basename?: string;
}
const SubAppRouter: React.FC<SubAppRouterProps> = ({ basename }) => {
// console.log('React Sub App Router basename:', basename);
return (
// 使用 BrowserRouter 配合 basename
// basename 会确保所有的路由链接和导航都是相对于这个 base 路径的
<BrowserRouter basename={basename}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="*" element={<NotFound />} /> {/* 404 页面 */}
</Routes>
</BrowserRouter>
);
};
export default SubAppRouter;
子应用的 base 路径: 在子应用的路由配置中,base 路径需要与 Qiankun 中定义的 activeRule 保持一致。
- 例如:如果主应用注册 Vue 子应用的
activeRule是/vue,那么 Vue 子应用的 Vue Routerhistory模式的base路径就应该是/vue/。 - 同理,React 子应用的
activeRule是/react,React Router 的basename就应该是/react/。
1. 主应用跳转到子应用内部路由 (父 -> 子)
当你在主应用中点击一个链接,希望跳转到某个子应用的特定页面时:
方法:使用主应用控制路由跳转
-
如果使用
registerMicroApps:- Qiankun 会监听主应用的 URL 变化。当 URL 匹配到某个子应用的
activeRule时,Qiankun 会自动加载并挂载该子应用。 - 如果你希望直接跳转到子应用内部的某个路由(例如
/react/dashboard),主应用只需要改变自身的 URL 即可。 - 主应用导航:
- Qiankun 会监听主应用的 URL 变化。当 URL 匹配到某个子应用的
js
// my-qiankun-main/src/App.tsx 或其他导航组件
import { useNavigate } from 'react-router-dom'; // 如果主应用也用 React Router
function App() {
const navigate = useNavigate(); // 或使用 window.history.pushState
const goToReactDashboard = () => {
navigate('/react/dashboard'); // 使用 React Router 跳转
// 或者 window.history.pushState(null, '', '/react/dashboard');
};
return (
<nav>
<button onClick={goToReactDashboard}>Go to React App Dashboard</button>
{/* ... 其他按钮 */}
</nav>
);
}
当主应用的 URL 变为 /react/dashboard 时,Qiankun 会:
- 检测到
/react匹配规则,加载并挂载reactApp。 reactApp内部的路由(例如 React Router)会识别到其basename/react后面的/dashboard路径,并渲染dashboard页面。
当让你也可以更灵活定义全局状态,当子路由发生变化,它想要跳转的内部路径片段(例如dashboard或about),然后由全局状态中的subAppRoutes来拼接出完整的 URL。
js
// 定义一个全局状态类型(可以单独放在一个 common.ts 或 types.ts 文件中)
interface GlobalState {
currentPath: string;
navigateTo: string; // 子应用请求主应用跳转的目标路径
subAppRoutes: { // 新增:存储子应用的路由信息
[key: string]: { // key: 子应用的 name (如 'reactApp', 'vueApp')
basePath: string; // 子应用的 activeRule (如 '/react', '/vue')
defaultPath?: string; // 子应用的内部默认路径 (如 '/')
dashboardPath?: string; // 示例:特定的内部路径
// ... 更多子应用可以暴露的路径
};
};
}
// 初始状态
const initialGlobalState: GlobalState = {
currentPath: window.location.pathname,
navigateTo: '',
subAppRoutes: {}, // 初始为空,子应用挂载时注册
};
如果使用 loadMicroApp (手动加载):
- 由于
loadMicroApp不依赖activeRule自动加载,你需要手动控制加载和 URL 变化。 - 你可以在调用
loadMicroApp之后,手动更改主应用的 URL。 - 主应用导航:
js
// my-qiankun-main/src/App.tsx (手动加载方式)
// ...
const loadAndMountApp = (appName: 'react' | 'vue', entry: string, path: string) => {
// ... 省略卸载逻辑 ...
const app = loadMicroApp({ /* ... */ });
currentMicroAppRef.current = app;
setActiveApp(appName);
// 关键:加载后,手动改变主应用的 URL
window.history.pushState(null, '', path);
};
const goToReactDashboard = () => {
loadAndMountApp(
'react',
'//localhost:7100/', // 或生产环境 UMD URL
'/react/dashboard' // 目标路径
);
};
const goToVueAbout = () => {
loadAndMountApp(
'vue',
'//localhost:7200/', // 或生产环境 UMD URL
'/vue/about' // 目标路径
);
};
// ...
子应用内部路由的 base 路径设置至关重要: 确保子应用(React Router 的 basename 或 Vue Router 的 base)能够正确识别 activeRule 后面的路径。
2. 子应用内部路由跳转 (子内跳转)
子应用内部的路由跳转完全由子应用自己的路由系统管理,通常不需要 Qiankun 的额外干预。
- Vue 子应用:
js
<template>
<router-link to="/about">About Page</router-link>
</template>
<script>
import { useRouter } from 'vue-router';
export default {
setup() {
const router = useRouter();
const goToHomePage = () => {
router.push('/home'); // 内部跳转
};
return { goToHomePage };
}
}
</script>
- react 子应用:
js
// MyComponent.tsx in React sub-app
import { Link, useNavigate } from 'react-router-dom';
function MyComponent() {
const navigate = useNavigate();
const goToDashboard = () => {
navigate('/dashboard'); // 内部跳转
};
return (
<nav>
<Link to="/settings">Settings</Link>
<button onClick={goToDashboard}>Go to Dashboard</button>
</nav>
);
}
Qiankun 会自动处理 URL 同步: 当子应用内部进行路由跳转时(例如从 /react/dashboard 跳转到 /react/settings),Qiankun 会劫持 window.history 相关的 API (如 pushState, replaceState),并将这些变化同步到主应用的 URL 上。这样,即使子应用内部跳转,主应用的地址栏也会正确显示 /react/settings。
3. 子应用跳转到主应用路由或另一个子应用路由 (子 -> 父 / 子 -> 另一个子)
这是最需要注意的地方,因为子应用不能直接使用其内部路由系统跳转到主应用路由或另一个子应用的路由,否则可能会导致主应用上下文丢失或路由错误。子应用需要通过与主应用通信来请求路由跳转。
方法:使用 Qiankun 的全局状态通信 (推荐)
Qiankun 提供了 setGlobalState 和 onGlobalStateChange API,用于主子应用之间的全局状态通信。
- 在主应用中监听全局状态变化,并执行路由跳转:
js
// my-qiankun-main/src/main.tsx (如果使用 registerMicroApps 和 start)
// 或者在 App.tsx 中使用 onGlobalStateChange 监听
import { start, initGlobalState } from 'qiankun';
import { useNavigate } from 'react-router-dom'; // 如果主应用也用 React Router
// 初始化全局状态
const actions = initGlobalState({
currentPath: '/', // 初始值
// ... 其他全局状态
});
// 监听全局状态变化
actions.onGlobalStateChange((state, prev) => {
// 当子应用发送 'navigateTo' 意图时,主应用来处理路由跳转
if (state.navigateTo && state.navigateTo !== prev.navigateTo) {
console.log(`Main app received navigation request: ${state.navigateTo}`);
// 假设主应用使用 React Router
// const navigate = useNavigate(); // 注意:useNavigate 必须在 React 组件内部调用
// 所以这个逻辑通常在 App.tsx 中实现
// 或者直接使用 window.history.pushState(null, '', state.navigateTo);
}
}, true); // true 表示立即执行一次,获取初始状态
start({
// ... sandbox 配置
// 这里的 actions 可以在 loadMicroApp 或 registerMicroApps 时通过 props 传递给子应用
});
在主应用的 App.tsx 中监听全局状态:
js
// my-qiankun-main/src/App.tsx
import React, { useEffect, useState, useRef } from 'react';
import { loadMicroApp, MicroApp, initGlobalState, MicroAppStateActions } from 'qiankun';
import { useNavigate } from 'react-router-dom'; // 如果主应用使用 React Router
import './App.css';
// 确保 actions 实例只创建一次并全局可用,或者通过 context 传递
let globalActions: MicroAppStateActions;
if (!globalActions) {
globalActions = initGlobalState({ currentPath: '/' }); // 初始化
}
function App() {
const [activeApp, setActiveApp] = useState<'react' | 'vue' | null>(null);
const currentMicroAppRef = useRef<MicroApp | null>(null);
const navigate = useNavigate(); // 获取主应用的导航实例
// 监听全局状态变化来处理路由跳转
useEffect(() => {
const unsubscribe = globalActions.onGlobalStateChange((state, prev) => {
if (state.navigateTo && state.navigateTo !== prev.navigateTo) {
console.log(`Main app received navigation request: ${state.navigateTo}`);
// 使用主应用的路由跳转
navigate(state.navigateTo);
}
}, true);
return () => unsubscribe();
}, [navigate]); // 依赖 navigate 确保钩子更新
// ... loadAndMountApp 函数保持不变 ...
return (
<>
<h1>Qiankun Main App (React Vite) - Manual Loading</h1>
<nav>
<button onClick={() => loadAndMountApp('react', '//localhost:7100/', '/react')}>Load React App</button>
<button onClick={() => loadAndMountApp('vue', '//localhost:7200/', '/vue')}>Load Vue App</button>
<button onClick={() => navigate('/')}>Go to Main Home</button> {/* 主应用自己的路由 */}
</nav>
{activeApp && <p>Currently loaded: {activeApp} App</p>}
<div id="sub-app-container"></div>
</>
);
}
export default App;
- 在子应用中调用
setGlobalState请求路由跳转: 子应用需要在其mount生命周期中获取到actions对象(通过props传递),然后调用setGlobalState来触发主应用的路由跳转。
- Vue 子应用 (
my-qiankun-vue-sub-webpack/src/main.ts):
js
// ...
let instance: any = null;
let qiankunGlobalActions: any = null; // 用于存储 Qiankun 的 actions
function render(props: any = {}) {
const { container, setGlobalState } = props; // 接收 setGlobalState
// 如果 setGlobalState 存在,就保存它
if (setGlobalState) {
qiankunGlobalActions = props; // 或直接保存 setGlobalState
}
instance = createApp(App);
instance.mount(container ? container.querySelector('#app') : '#app');
}
// ... bootstrap, mount, unmount 钩子不变 ...
export async function mount(props: any) {
console.log('[Vue-Webpack] child app mounted', props);
render(props);
qiankunGlobalActions = props; // 确保在 mount 时保存 actions
}
// 示例:子应用内部的一个方法,用于跳转到主应用路由或另一个子应用路由
// 可以在 App.vue 中通过 provide/inject 或 props 传递 qiankunGlobalActions
// 或者直接在 main.ts 中暴露一个方法
(window as any).goToMainHome = () => {
if (qiankunGlobalActions && qiankunGlobalActions.setGlobalState) {
qiankunGlobalActions.setGlobalState({ navigateTo: '/' }); // 跳转到主应用首页
}
};
(window as any).goToReactApp = () => {
if (qiankunGlobalActions && qiankunGlobalActions.setGlobalState) {
qiankunGlobalActions.setGlobalState({ navigateTo: '/react/dashboard' }); // 跳转到 React 子应用的某个路径
}
};
然后在 Vue 组件中:
js
<template>
<div>
<button @click="goToMain">Go to Main App Home</button>
<button @click="goToOtherApp">Go to React App</button>
</div>
</template>
<script setup lang="ts">
// 注意:在 setup 中直接访问 window 属性
const goToMain = () => {
(window as any).goToMainHome();
};
const goToOtherApp = () => {
(window as any).goToReactApp();
};
</script>
React Webpack 子应用 (my-qiankun-react-sub-webpack/src/main.tsx):
js
// ...
let root: ReactDOM.Root | null = null;
let qiankunGlobalActions: any = null; // 用于存储 Qiankun 的 actions
function render(props: any) {
const { container } = props;
const mountNode = container ? container.querySelector('#root') : document.getElementById('root');
if (mountNode) {
if (!root) {
root = ReactDOM.createRoot(mountNode);
}
root.render(
<React.StrictMode>
<App {...props} /> {/* 将 props 传递给 App 组件 */}
</React.StrictMode>
);
}
}
export async function mount(props: any) {
console.log('[Webpack-React] child app mounted', props);
render(props);
qiankunGlobalActions = props; // 确保在 mount 时保存 actions
}
// ... unmount 钩子不变 ...
// 在 App.tsx 中使用这些 actions
// App.tsx
// function App(props: any) {
// const { setGlobalState } = props; // 从 props 接收
// const goToMainHome = () => {
// if (setGlobalState) {
// setGlobalState({ navigateTo: '/' });
// }
// };
// return (
// <div>
// <h2>React Webpack Sub App</h2>
// <button onClick={goToMainHome}>Go to Main Home</button>
// </div>
// );
// }
// export default App;
总结和最佳实践:
-
统一路由模式: 主应用和所有子应用都使用 History 模式。
-
子应用
base路径: 确保子应用的路由base路径与 Qiankun 中定义的activeRule保持一致(例如/react/和/vue/)。 -
主应用控制子应用加载:
registerMicroApps: 只需要主应用改变 URL,Qiankun 自动加载匹配的子应用。loadMicroApp: 主应用需要手动调用loadMicroApp,并在加载后手动改变 URL。
-
子应用内部路由: 由子应用自己的路由系统独立管理,Qiankun 会自动同步 URL 变化到主应用地址栏。
-
子应用请求跳转: 子应用不直接操作
window.location或主应用路由实例 。而是通过 Qiankun 提供的setGlobalState向主应用发送一个"路由跳转意图"的状态,由主应用监听并执行实际的路由跳转。
通过这种"主控分发,子知会"的模式,可以很好地协调微前端架构中的路由跳转,避免冲突和意外行为。
---------------------------------------------------------------
上面的介绍只是对于两个子应用的,但是如果子应用10几个,甚至更多在每个子应用中硬编码 qiankunGlobalActions.setGlobalState({ navigateTo: '/another-sub-app/some-path' }) 来实现跨子应用跳转,将变得非常难以维护。这会造成严重的紧耦合 和代码冗余。
理想情况下,子应用应该只关心自己的业务逻辑和路由,而不知道其他子应用的具体 URL 结构。跨子应用跳转的"路由映射"和"实际执行"应该由主应用来统一管理。
核心思想:主应用统一路由分发,子应用只发送意图
我们可以在之前的基础上进一步优化:
-
子应用发送更抽象的"跳转意图": 子应用不直接发送完整的
MapsTo路径,而是发送一个更抽象的意图,例如:- "我想去 产品管理 应用的 列表页"
- "我想去 用户中心 应用的 个人资料页"
-
主应用作为"路由网关": 主应用拥有所有子应用的路由映射表(或者能够根据某种规则动态生成),它根据子应用发送的抽象意图,查询映射表,然后构建出正确的完整 URL 并执行跳转。
具体的实现方案:
方案一:主应用维护集中式路由表 (推荐,适用于子应用数量较多)
主应用维护一个所有子应用和其内部关键页面的映射表。子应用只传递一个标识符。
1. 定义更抽象的跳转意图和主应用路由表结构:
js
// common.ts 或 types.ts
// 定义全局状态类型
interface GlobalState {
currentPath: string;
// navigateTo 仍然存在,但现在它可能只接收抽象的路由标识符,而不是完整路径
// 或者,我们可以用一个更明确的属性名,比如 targetRoute
targetRoute: {
app: string; // 目标子应用的 name (如 'productApp', 'userApp')
page?: string; // 目标子应用内部的页面标识 (如 'list', 'detail', 'profile')
params?: Record<string, any>; // 任何需要的参数 (如 { id: 123 })
} | null;
subAppRoutes: {
[key: string]: {
basePath: string; // 子应用的 activeRule (如 '/product', '/user')
// 可以在这里定义子应用内部的"友好名称"到实际路径的映射,或者由主应用统一管理
pages?: { // 示例:子应用可暴露的关键页面映射
[key: string]: string; // 页面标识符 -> 内部路径片段 (如 'list' -> '/', 'detail' -> '/detail/:id')
}
};
};
}
const initialGlobalState: GlobalState = {
currentPath: window.location.pathname,
targetRoute: null,
subAppRoutes: {},
};
// 主应用可以维护的路由映射表(不需要全局状态,可以在主应用内部)
interface MainAppRouteMap {
[appIdentifier: string]: { // 例如 'productManagement' 或 'userCenter'
basePath: string; // 对应的 activeRule
pages: {
[pageIdentifier: string]: (params?: Record<string, any>) => string; // 页面标识符 -> 生成完整路径的函数
};
};
}
// 示例主应用路由映射表(在主应用内部定义)
const mainAppRouteMap: MainAppRouteMap = {
'productManagement': {
basePath: '/product', // 对应 activeRule
pages: {
'list': () => '/',
'detail': (params) => `/detail/${params?.id || ''}`,
'create': () => '/create',
},
},
'userCenter': {
basePath: '/user', // 对应 activeRule
pages: {
'profile': () => '/profile',
'settings': () => '/settings',
},
},
'main': { // 主应用自身的路由
basePath: '/',
pages: {
'home': () => '/',
'about': () => '/about',
},
},
};
2. 主应用 (my-qiankun-main/src/App.tsx):
- 集中处理路由跳转逻辑:
- 构建完整的 URL:
js
// my-qiankun-main/src/App.tsx
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import './App.css';
import { initGlobalState, MicroAppStateActions } from 'qiankun';
import { initialGlobalState, MainAppRouteMap } from './types'; // 导入类型和初始状态
// 获取全局 actions 实例
let globalActions: MicroAppStateActions | null = null;
if (!(window as any).__QIANKUN_GLOBAL_ACTIONS__) {
(window as any).__QIANKUN_GLOBAL_ACTIONS__ = initGlobalState(initialGlobalState);
}
globalActions = (window as any).__QIANKUN_GLOBAL_ACTIONS__;
// 示例主应用内部路由映射表(可以从一个单独的文件导入)
const mainAppRouteMap: MainAppRouteMap = {
'productManagement': { // 这是一个逻辑上的应用标识符,不一定是qiankun的name
basePath: '/react', // 对应 React 子应用的 activeRule
pages: {
'list': () => '/',
'dashboard': () => '/dashboard',
'detail': (params) => `/detail/${params?.id || ''}`,
},
},
'userCenter': { // 这是一个逻辑上的应用标识符,不一定是qiankun的name
basePath: '/vue', // 对应 Vue 子应用的 activeRule
pages: {
'home': () => '/',
'about': () => '/about',
'settings': () => '/settings',
},
},
'main': { // 主应用自身的路由
basePath: '/',
pages: {
'home': () => '/',
'about': () => '/about',
},
},
};
function App() {
const navigate = useNavigate();
// 监听子应用请求的导航意图
useEffect(() => {
if (!globalActions) return;
const unsubscribe = globalActions.onGlobalStateChange((state: typeof initialGlobalState, prev: typeof initialGlobalState) => {
if (state.targetRoute && state.targetRoute !== prev.targetRoute) {
const { app, page, params } = state.targetRoute;
const appInfo = mainAppRouteMap[app];
if (appInfo && appInfo.pages[page || 'home']) { // 如果 page 不存在,默认到 home
const internalPath = appInfo.pages[page || 'home'](params); // 生成内部路径
const fullPath = `${appInfo.basePath}${internalPath.startsWith('/') ? internalPath : '/' + internalPath}`;
console.log(`主应用:根据抽象意图导航到 ${app} 的 ${page} -> ${fullPath}`);
navigate(fullPath);
} else {
console.warn(`主应用:无法识别目标应用或页面: app=${app}, page=${page}`);
}
globalActions?.setGlobalState({ targetRoute: null }); // 清除意图
}
}, true);
return () => unsubscribe();
}, [navigate]);
// 主应用导航到子应用特定页面(通过抽象意图)
const navigateToAbstractRoute = (app: string, page: string = 'home', params?: Record<string, any>) => {
const appInfo = mainAppRouteMap[app];
if (appInfo && appInfo.pages[page]) {
const internalPath = appInfo.pages[page](params);
const fullPath = `${appInfo.basePath}${internalPath.startsWith('/') ? internalPath : '/' + internalPath}`;
console.log(`主应用:导航到 ${app} 的 ${page} -> ${fullPath}`);
navigate(fullPath);
} else {
console.warn(`主应用:无法导航到 ${app} 的 ${page}。请检查 mainAppRouteMap 配置。`);
// 备用:如果配置不明确,可以导航到子应用根路径
if (app === 'productManagement') navigate('/react');
if (app === 'userCenter') navigate('/vue');
// 如果是主应用自身
if (app === 'main') navigate(mainAppRouteMap.main.pages[page]?.(params) || '/');
}
};
return (
<>
<h1>Qiankun 主应用 (React Vite) - 抽象路由</h1>
<nav>
<button onClick={() => navigateToAbstractRoute('main', 'home')}>主应用首页</button>
<button onClick={() => navigateToAbstractRoute('main', 'about')}>主应用关于</button>
<button onClick={() => navigateToAbstractRoute('productManagement', 'dashboard')}>
跳转到 产品-仪表盘
</button>
<button onClick={() => navigateToAbstractRoute('productManagement', 'detail', { id: 123 })}>
跳转到 产品-详情 (ID 123)
</button>
<button onClick={() => navigateToAbstractRoute('userCenter', 'about')}>
跳转到 用户-关于页
</button>
</nav>
<div id="sub-app-container"></div>
</>
);
}
export default App;
3. 子应用(React 和 Vue)发送抽象意图:
子应用不再拼接完整的 URL,而是通过 setGlobalState 发送一个抽象的 targetRoute 对象,其中包含目标应用的逻辑标识符和页面标识符。
- React Webpack 子应用 (
my-qiankun-react-sub-webpack/src/App.tsx):
js
// my-qiankun-react-sub-webpack/src/App.tsx
// ...
const App: React.FC<AppProps> = ({ qiankunGlobalActions }) => {
// ...
const navigateToMainHome = () => {
if (qiankunGlobalActions && qiankunGlobalActions.setGlobalState) {
qiankunGlobalActions.setGlobalState({ targetRoute: { app: 'main', page: 'home' } });
}
};
const navigateToUserCenterProfile = () => {
if (qiankunGlobalActions && qiankunGlobalActions.setGlobalState) {
qiankunGlobalActions.setGlobalState({ targetRoute: { app: 'userCenter', page: 'profile' } });
}
};
return (
<div>
{/* ... 内部导航 ... */}
<hr />
<div>
<h4>跨应用跳转(抽象意图):</h4>
<button onClick={navigateToMainHome}>跳转到主应用首页</button>
<button onClick={navigateToUserCenterProfile}>跳转到 用户中心-个人资料</button>
<button onClick={() => qiankunGlobalActions?.setGlobalState({ targetRoute: { app: 'productManagement', page: 'create' } })}>
跳转到 产品管理-创建页
</button>
</div>
</div>
);
};
export default App;
Vue CLI Webpack 子应用 (my-qiankun-vue-sub-webpack/src/App.vue):
js
<template>
<div>
<hr />
<div>
<h4>跨应用跳转(抽象意图):</h4>
<button @click="navigateToMainAbout">跳转到主应用关于页</button>
<button @click="navigateToProductDashboard">跳转到 产品管理-仪表盘</button>
<button @click="navigateToProductDetail">跳转到 产品管理-详情 (ID 456)</button>
</div>
</div>
</template>
<script setup lang="ts">
import { inject } from 'vue';
import type { MicroAppStateActions } from 'qiankun';
const qiankunActions = inject<MicroAppStateActions>('qiankunActions');
const navigateToMainAbout = () => {
if (qiankunActions && qiankunActions.setGlobalState) {
qiankunActions.setGlobalState({ targetRoute: { app: 'main', page: 'about' } });
}
};
const navigateToProductDashboard = () => {
if (qiankunActions && qiankunActions.setGlobalState) {
qiankunActions.setGlobalState({ targetRoute: { app: 'productManagement', page: 'dashboard' } });
}
};
const navigateToProductDetail = () => {
if (qiankunActions && qiankunActions.setGlobalState) {
qiankunActions.setGlobalState({ targetRoute: { app: 'productManagement', page: 'detail', params: { id: 456 } } });
}
};
</script>
这种方案的巨大优势:
- 极度解耦: 子应用完全不知道其他子应用的具体 URL 结构。它只知道一个逻辑名称(如
productManagement)和一个页面标识符(如dashboard)。 - 主应用统一管理: 所有的路由映射和拼接逻辑都集中在主应用中。当子应用路由结构变化时,只需要更新主应用中的
mainAppRouteMap。 - 易于扩展: 增加新的子应用或子应用页面,只需要在
mainAppRouteMap中添加对应配置,子应用发送的意图保持抽象。 - 避免硬编码: 大量减少了子应用中的硬编码路径,大大提升了可维护性。
- 参数传递: 可以方便地通过
params传递路由参数。
这种方式是处理大量微前端路由跳转的最佳实践之一,将路由的"决策权"和"执行权"集中到主应用,而子应用只需"发出请求"。
上面的路由网关方案,即主应用维护集中式路由表,子应用发送抽象跳转意图 的方案,是基于 registerMicroApps 和 start() 模式下最理想的实践。
为什么它更适合 registerMicroApps 模式?
- URL 驱动:
registerMicroApps模式的核心就是 URL 驱动 。Qiankun 监听主应用的 URL 变化,并根据activeRule自动加载和卸载子应用。 - 主应用作为"URL 事实拥有者": 在这种模式下,主应用的
window.location.href始终是"真实"的 URL。当子应用内部发生路由变化时,Qiankun 会劫持pushState/replaceState并同步到主应用的 URL 上。反之,当主应用改变 URL 时,Qiankun 会根据activeRule自动切换子应用。 - 无缝衔接: 当主应用通过
Maps(fullPath)改变 URL 时,Qiankun 会自动检测到这个 URL 变化。如果这个fullPath匹配到某个子应用的activeRule,Qiankun 就会加载并挂载这个子应用。子应用内部的路由系统(例如 React Router 或 Vue Router)会立即根据basename和fullPath的剩余部分来渲染对应的内部组件。整个过程非常流畅和自动化。
如果基于 loadMicroApp 模式呢?
理论上,你也可以将这个"路由网关"的思路应用到 loadMicroApp 模式,但会带来额外的复杂性:
- 手动加载与 URL 驱动的冲突:
loadMicroApp是命令式的,它不依赖 URL 自动加载。这意味着,当主应用计算出fullPath后,它不仅要执行Maps(fullPath),还需要手动调用loadMicroApp来加载对应的子应用。这会导致逻辑上的重复和可能的竞争条件。 - 卸载逻辑: 你需要更复杂的逻辑来判断何时卸载当前子应用,何时加载目标子应用。而在
registerMicroApps模式下,Qiankun 会根据activeRule自动处理卸载和加载。 - URL 变化感知: 虽然你可以手动改变 URL,但
loadMicroApp模式下,你失去了registerMicroApps带来的"URL 驱动"的便捷性。你需要自己管理 URL 和子应用状态的同步。
总结:
路由网关的核心目标是解耦子应用之间的直接依赖,并由主应用统一管理导航流程。这个模式与 registerMicroApps 这种以 URL 变化为驱动的微前端管理方式是高度契合和互补的。
因此,上述的路由网关方案是基于 registerMicroApps 模式下,实现大规模微前端路由管理的标准和推荐做法。
----------------------------------------------------------
虽然基于 loadMicroApp 的路由跳转 在复杂性和管理上会比 registerMicroApps 模式更复杂一些,因为它不再依赖 Qiankun 自动的 activeRule 匹配和生命周期管理,但它在某些特定场景下(例如非 URL 驱动的模块化加载、弹窗式微应用等)仍然有其价值。
基于 loadMicroApp 的父子应用路由跳转思路
在 loadMicroApp 模式下,你拥有对微应用生命周期的完全控制权。 这意味着:
- 加载和卸载是手动触发的。
- URL 变化与微应用的加载/卸载解耦(不再自动化)。 你需要自己同步它们。
- 路由网关的逻辑依然适用,但其执行方式会改变。
核心思路:
-
主应用仍然作为"路由网关"和"应用管理器": 它拥有所有子应用的路由映射表,并根据用户意图(或子应用请求)来决定要加载哪个子应用,并跳转到哪个 URL。
-
子应用发送抽象意图: 和
registerMicroApps模式一样,子应用通过setGlobalState发送抽象的跳转意图(目标应用名 + 页面标识 + 参数)。 -
主应用监听意图,并执行"两步走"操作:
- 步骤一: 根据意图,计算出完整的 URL 路径。
- 步骤二: 手动调用
loadMicroApp加载/切换到目标子应用,同时手动调用window.history.pushState来更新主应用的 URL。 这两步必须协同进行。
-
管理
MicroApp实例: 由于是手动加载,主应用需要维护当前激活的MicroApp实例,以便在加载新子应用前正确地unmount旧子应用。
具体代码示例 (loadMicroApp 模式)
我们将修改主应用 (my-qiankun-main) 的 src/App.tsx 文件。子应用的代码(包括 main.tsx/ts 中生命周期函数的导出、App.tsx/vue 中通过 qiankunGlobalActions 发送抽象意图)与 registerMicroApps 模式下是完全相同的,因为子应用不需要知道主应用是用哪种方式加载它的。
1. 定义全局状态和主应用路由表 (与 registerMicroApps 模式相同)
js
// common.ts 或 types.ts
interface GlobalState {
currentPath: string;
targetRoute: {
app: string;
page?: string;
params?: Record<string, any>;
} | null;
subAppRoutes: {
[key: string]: {
basePath: string;
pages?: {
[key: string]: (params?: Record<string, any>) => string;
};
};
};
}
const initialGlobalState: GlobalState = {
currentPath: window.location.pathname,
targetRoute: null,
subAppRoutes: {},
};
interface MainAppRouteMap {
[appIdentifier: string]: {
basePath: string;
pages: {
[pageIdentifier: string]: (params?: Record<string, any>) => string;
};
// 新增:子应用的 entry URL,用于 loadMicroApp
entry: string;
qiankunName: string; // Qiankun 注册时的 name,需要与子应用 package.json name 或 vite.config.ts 的 build.lib.name 对应
};
}
2. 主应用 (my-qiankun-main/src/App.tsx):
- 我们将
MicroApp实例存储在useRef中。 loadAndMountApp函数现在将执行核心的"切换"逻辑。- 主应用不再使用
useNavigate来触发 Qiankun 自动加载,而是直接控制loadMicroApp和window.history.pushState。
js
// my-qiankun-main/src/App.tsx
import React, { useEffect, useState, useRef } from 'react';
// 在 loadMicroApp 模式下,主应用不一定需要 React Router
// 如果你的主应用本身没有复杂的路由,可以移除 useNavigate
// import { useNavigate } from 'react-router-dom'; // 如果主应用自身有复杂路由,保留
import { loadMicroApp, initGlobalState, MicroApp, MicroAppStateActions } from 'qiankun';
import { initialGlobalState, MainAppRouteMap } from './types'; // 导入类型
import './App.css';
// 获取全局 actions 实例
let globalActions: MicroAppStateActions;
if (!(window as any).__QIANKUN_GLOBAL_ACTIONS__) {
(window as any).__QIANKUN_GLOBAL_ACTIONS__ = initGlobalState(initialGlobalState);
}
globalActions = (window as any).__QIANKUN_GLOBAL_ACTIONS__;
// 示例主应用内部路由映射表(需要添加每个子应用的 entry 和 qiankunName)
const mainAppRouteMap: MainAppRouteMap = {
'productManagement': {
basePath: '/react',
entry: '//localhost:7100', // React 子应用的入口 URL
qiankunName: 'my-qiankun-react-sub-webpack', // 对应子应用的 name
pages: {
'home': () => '/',
'dashboard': () => '/dashboard',
'detail': (params) => `/detail/${params?.id || ''}`,
'create': () => '/create',
},
},
'userCenter': {
basePath: '/vue',
entry: '//localhost:7200', // Vue 子应用的入口 URL
qiankunName: 'vueApp', // 对应子应用的 name
pages: {
'home': () => '/',
'about': () => '/about',
'settings': () => '/settings',
},
},
'main': { // 主应用自身的路由,这里只是为了演示抽象跳转
basePath: '/',
entry: '', // 主应用不需要 entry
qiankunName: '', // 主应用没有 qiankunName
pages: {
'home': () => '/',
'about': () => '/about',
},
},
// ... 其他子应用
};
function App() {
// const navigate = useNavigate(); // 如果主应用本身没有复杂路由,可以移除
const currentMicroAppRef = useRef<MicroApp | null>(null); // 存储当前激活的微应用实例
const [activeSubAppName, setActiveSubAppName] = useState<string | null>(null); // 记录当前激活的子应用逻辑名
// 核心函数:加载并挂载目标子应用,同时更新 URL
const loadAndSwitchApp = async (targetAppId: string, targetInternalPath: string = '', params?: Record<string, any>) => {
const appInfo = mainAppRouteMap[targetAppId];
if (!appInfo || !appInfo.entry) {
console.error(`主应用:无法找到应用 ${targetAppId} 的配置或入口。`);
return;
}
// 1. 计算出完整的 URL 路径
const pageFn = appInfo.pages[targetInternalPath] || appInfo.pages['home']; // 默认到 home
const internalPathSegment = pageFn ? pageFn(params) : '';
const fullPath = `${appInfo.basePath}${internalPathSegment.startsWith('/') ? internalPathSegment : '/' + internalPathSegment}`;
// 2. 如果有旧的应用在运行,先卸载它
if (currentMicroAppRef.current) {
console.log(`主应用:卸载当前应用 ${activeSubAppName}`);
await currentMicroAppRef.current.unmount();
currentMicroAppRef.current = null;
}
// 3. 更新主应用 URL
console.log(`主应用:更新 URL 到 ${fullPath}`);
window.history.pushState(null, '', fullPath);
// 4. 加载并挂载新的子应用
// Qiankun 的 name 必须是子应用构建时定义的 name
const app = loadMicroApp({
name: appInfo.qiankunName, // 使用配置的 Qiankun name
entry: appInfo.entry,
container: '#sub-app-container',
props: {
qiankunGlobalActions: globalActions, // 继续传递 actions
},
});
currentMicroAppRef.current = app;
setActiveSubAppName(targetAppId);
console.log(`主应用:加载并切换到 ${targetAppId}`);
};
// 监听子应用请求的导航意图 (与 registerMicroApps 模式相同)
useEffect(() => {
if (!globalActions) return;
const unsubscribe = globalActions.onGlobalStateChange((state: typeof initialGlobalState, prev: typeof initialGlobalState) => {
if (state.targetRoute && state.targetRoute !== prev.targetRoute) {
const { app, page, params } = state.targetRoute;
console.log(`主应用:收到子应用导航请求: app=${app}, page=${page}`);
// 执行路由切换
// 注意:这里需要判断目标是否是主应用自身,如果是,则不走 loadAndSwitchApp
if (app === 'main') {
const mainPagePath = mainAppRouteMap.main.pages[page || 'home'](params);
console.log(`主应用:跳转到自身路由 ${mainPagePath}`);
// 如果主应用使用了 React Router,则用 navigate,否则用 window.history.pushState
// navigate(mainPagePath);
window.history.pushState(null, '', mainPagePath);
} else {
loadAndSwitchApp(app, page, params); // 调用核心切换函数
}
globalActions?.setGlobalState({ targetRoute: null }); // 清除意图
}
}, true);
// 组件卸载时卸载当前微应用
return () => {
unsubscribe();
if (currentMicroAppRef.current) {
currentMicroAppRef.current.unmount();
currentMicroAppRef.current = null;
}
};
}, []); // 依赖项留空,只在组件挂载和卸载时运行一次
return (
<>
<h1>Qiankun 主应用 (React Vite) - loadMicroApp 动态路由</h1>
<nav>
{/* 主应用自身的导航,直接更新 URL,不涉及子应用切换 */}
<button onClick={() => window.history.pushState(null, '', '/')}>主应用首页</button>
<button onClick={() => window.history.pushState(null, '', '/main-about')}>主应用关于</button>
{/* 动态跳转到子应用 */}
<button onClick={() => loadAndSwitchApp('productManagement', 'dashboard')}>
加载 产品-仪表盘
</button>
<button onClick={() => loadAndSwitchApp('productManagement', 'detail', { id: 123 })}>
加载 产品-详情 (ID 123)
</button>
<button onClick={() => loadAndSwitchApp('userCenter', 'about')}>
加载 用户-关于页
</button>
</nav>
{activeSubAppName && <p>当前活跃子应用: {activeSubAppName}</p>}
<div id="sub-app-container"></div>
</>
);
}
export default App;
3. 子应用发送抽象意图 (与 registerMicroApps 模式相同)
子应用的代码逻辑与之前 registerMicroApps 模式下提供的示例完全一致,它们只通过 qiankunGlobalActions.setGlobalState({ targetRoute: { app: '...', page: '...', params: {...} } }) 发送抽象意图,不关心主应用如何实现跳转。
关键差异和注意事项:
- 主动卸载与加载: 这是
loadMicroApp模式与registerMicroApps最大的不同。主应用必须在加载新子应用之前,手动调用currentMicroAppRef.current.unmount()来卸载当前活跃的子应用。 - URL 同步: 主应用需要主动调用
window.history.pushState来更新浏览器的 URL,使其与即将加载的子应用状态保持一致。 - 主应用自身的路由: 如果主应用本身也有复杂的路由,并且使用了 React Router 的
BrowserRouter,那么在主应用内部导航时,应该继续使用Maps。但当涉及到加载和切换子应用时,由于loadMicroApp不依赖BrowserRouter自身的监听,你需要同时调用loadMicroApp和window.history.pushState来确保行为一致。 在示例中,为了简化,主应用自身的导航也直接使用了window.history.pushState,这样与子应用切换时的逻辑更统一。 mainAppRouteMap的entry和qiankunName: 在loadMicroApp模式下,mainAppRouteMap不仅需要包含basePath和pages映射,还需要包含每个子应用的entryURL 和 Qiankun 期望的name,因为主应用需要这些信息来执行loadMicroApp。
这种 loadMicroApp 模式下的路由管理更加灵活,但需要主应用承担更多的责任,手动管理子应用的生命周期和 URL 同步。它更适用于那种"我点击一个按钮,就弹出一个微应用,并且这个微应用有自己的路由"的场景,而不是纯粹的 URL 驱动的页面切换。