微前端qiankun - 应用之间的通信
用户可能是在构建微前端架构,子应用之间需要共享数据或者触发事件。这时候通信机制就很重要了。在 qiankun 微前端架构中,子应用之间的通信可以通过以下几种方式实现。以下是详细的通信方法和示例代码:
1. 通过 window 全局对象通信
最简单的通信方式是通过全局变量或函数,但需注意 命名冲突 和 沙箱隔离 的问题:
主应用(Parent)main-app配置
qiankun-config.ts
// main-app/src/qiankun-config.ts
import { registerMicroApps, start } from 'qiankun';// 主应用中定义全局方法
(window as any).globalMethods = {sendMessage: (message: string) => {console.log('收到消息:', message);}
}// 注册子应用
registerMicroApps([{name: 'app1',entry: '//localhost:3001', // 子应用 1 地址container: '#micro-container',activeRule: '/app1',},{name: 'app2',entry: '//localhost:3002', // 子应用 2 地址container: '#micro-container',activeRule: '/app2',},
]);// 启动 Qiankun
start();
子应用(Child)app1使用全局方法
// app1/src/app.tsx
import React from 'react';
import './App.css';function App() {const handleClick = () => {// 子应用中调用全局方法(window as any).globalMethods.sendMessage('Hello From Sub App!');}return (<div className="App"><div><h1>子应用 1 - 产品展示</h1><p>这里是子应用 1 的内容。</p><button onClick={handleClick}>修改</button></div></div>);
}export default App;
在app1页面点击修改按钮
2. 通过事件总线(Event Bus)
利用 window
对象发布和订阅事件,实现松耦合通信(也可以使用mitt 库):
主应用(Parent)main-app作为事件总线
qiankun-config.ts
文件内容
// main-app/src/qiankun-config.ts
import { registerMicroApps, start } from 'qiankun';// 2.EVENT_BUS时间总线
(window as any).EVENT_BUS = {listeners: {},on(eventName: string, callback: any) {if (!this.listeners[eventName]) this.listeners[eventName] = [];this.listeners[eventName].push(callback);},emit(eventName: string, data: any) {(this.listeners[eventName] || []).forEach((cb: any) => cb(data));}
}// 注册子应用
registerMicroApps([{name: 'app1',entry: '//localhost:3001', // 子应用 1 地址container: '#micro-container',activeRule: '/app1',},{name: 'app2',entry: '//localhost:3002', // 子应用 2 地址container: '#micro-container',activeRule: '/app2',},
]);// 启动 Qiankun
start();
子应用(Child)app1
发布/订阅事件
子应用1发布事件
// app11/src/App.tsx
import React from 'react';
import logo from './logo.svg';
import './App.css';function App() {const handleClick = () => {// 替例:子应用1发布事件(window as any).EVENT_BUS.emit('user-login', { userId: 123 });}return (<div className="App"><div><h1>子应用 1 - 产品展示</h1><p>这里是子应用 1 的内容。</p><button onClick={handleClick}>修改</button></div></div>);
}export default App;
子应用2app2
订阅事件
// app2/src/index.tsx
import React from 'react';
import ReactDOM, { Root } from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';// 在 module scope 保存 root 实例
let root: Root | null = null;function render(props: any) {const { container } = props;// 主应用渲染的时候,会传入一个容器id进来,有的话要优先使用,没有的话,就默认子应用独立渲染const containerDom = container? container.querySelector('#root') as HTMLElement: document.querySelector('#root') as HTMLElement;root = ReactDOM.createRoot(containerDom);root.render(<React.StrictMode><App /></React.StrictMode>);
}// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();// 子应用2订阅事件
(window as any).EVENT_BUS.on('user-login', (user: any) => {console.log('收到用户登录事件:', user);
});// 如果是在乾坤里面加载,则子应用应该跳过默认的加载逻辑,让主应用来控制当前应用的加载(匹配到子应用的路由标识)
if (!(window as any).__POWERED_BY_QIANKUN__) {render({});
}/** 暂时保持空函数 */
export async function bootstrap() {console.log('app2 bootstraped');
}/*** 挂着子应用的函数入口* @param {*} props*/
export async function mount(props: any) {console.log('app2 mount with props:', props);render(props);
}/*** 卸载子应用的函数* @param {*} props*/
export async function unmount(props: any) {// const { container } = props;if (root) {root.unmount(); // 正确卸载root = null;}
}
点击app1
的修改按钮
3. 通过 props 传递数据
在注册子应用时,主应用可以通过 props 向子应用传递数据。
主应用(Parent)main-app传递数据
qiankun-config.ts
文件内容
// main-app/src/qiankun-config.ts
import { registerMicroApps, start } from 'qiankun';// 1.主应用中定义全局方法
// (window as any).globalMethods = {
// sendMessage: (message: string) => {
// console.log('收到消息:', message);
// }
// }// 2.EVENT_BUS时间总线
// (window as any).EVENT_BUS = {
// listeners: {},
// on(eventName: string, callback: any) {
// if (!this.listeners[eventName]) this.listeners[eventName] = [];
// this.listeners[eventName].push(callback);
// },
// emit(eventName: string, data: any) {
// (this.listeners[eventName] || []).forEach((cb: any) => cb(data));
// }
// }// 注册子应用
registerMicroApps([{name: 'app1',entry: '//localhost:3001', // 子应用 1 地址container: '#micro-container',activeRule: '/app1',props: { /* 可传递 Props 到子应用 */authToken: 'your_token_here',userInfo: { name: 'Alice' },}},{name: 'app2',entry: '//localhost:3002', // 子应用 2 地址container: '#micro-container',activeRule: '/app2',},
]);// 启动 Qiankun
start();
子应用(Child)app1
接收数据
在子应用入口文件中,可以直接访问 props:
/*** 挂着子应用的函数入口* @param {*} props*/
export async function mount(props: any) {console.log('app1 mount with props:', props);render(props);
}
4. postMessag
通过原生 window.postMessage
实现应用间的消息传递,适用于更灵活的场景。
主应用发送事件:
// 主应用向子应用发送消息
window.postMessage({type: 'UPDATE_THEME',payload: { theme: 'dark' },},'*' // 允许跨域
);
app1
子应用监听事件:
export async function mount() {window.addEventListener('message', (event) => {if (event.data.type === 'UPDATE_THEME') {const theme = event.data.payload.theme;console.log('子应用收到主题更新:', theme);}});
}
5.官方 initGlobalState 全局状态通信(双向)
主应用初始化状态:
通过 initGlobalState 初始化全局状态,并暴露 actions 对象用于操作状态
actions 对象,包含以下方法:
- setGlobalState:更新全局状态。
- getGlobalState:获取当前全局状态。
- onGlobalStateChange:监听全局状态的变化。
- offGlobalStateChange:取消监听全局状态的变化。
主应用监听状态变化:
通过 onGlobalStateChange
监听全局状态的变化,当状态被修改时执行回调。
主应用或子应用修改状态:
通过 setGlobalState
更新状态,触发所有监听器(包括主应用和子应用的监听)。
主应用初始化全局状态
新建 main-app/src/shared/actions.ts
:
// main-app/src/share/actions.ts
import { initGlobalState } from 'qiankun';// 主应用中注册全局事件
// 设置全局状态
const initialState = {userInfo: { name: 'Alice', age: 25 },
};
// 创建全局状态实例并导出
const actions = initGlobalState(initialState);
// 监听全局状态变化(可选)
actions.onGlobalStateChange((newState, prev) => {console.log('主应用监听到全局状态变化:', newState, prev);
});
export default actions;
注册子应用时传递状态
在 main-app/src/qiankun-config.ts
中
// main-app/src/qiankun-config.ts
import { registerMicroApps, start } from 'qiankun';import actions from './share/actions'// 1.主应用中定义全局方法
// (window as any).globalMethods = {
// sendMessage: (message: string) => {
// console.log('收到消息:', message);
// }
// }// 2.EVENT_BUS时间总线
// (window as any).EVENT_BUS = {
// listeners: {},
// on(eventName: string, callback: any) {
// if (!this.listeners[eventName]) this.listeners[eventName] = [];
// this.listeners[eventName].push(callback);
// },
// emit(eventName: string, data: any) {
// (this.listeners[eventName] || []).forEach((cb: any) => cb(data));
// }
// }// 注册子应用
registerMicroApps([{name: 'app1',entry: '//localhost:3001', // 子应用 1 地址container: '#micro-container',activeRule: '/app1',props: { /* 可传递 Props 到子应用 */authToken: 'your_token_here',userInfo: { name: 'Alice' },// 传递全局状态操作方法onGlobalStateChange: actions.onGlobalStateChange,setGlobalState: actions.setGlobalState}},{name: 'app2',entry: '//localhost:3002', // 子应用 2 地址container: '#micro-container',activeRule: '/app2',},
]);// 启动 Qiankun
start();
主组件(main-app
)触发状态更新
// main-app/src/App.tsx
import React from 'react';
import logo from './logo.svg';
import { BrowserRouter as Router, Link, Route, Routes } from 'react-router-dom';
import './App.css';import actions from './share/actions';function App() {const handleLogin = () => {// 更新全局状态(用户登录后)actions.setGlobalState({ user: { name: 'Admin' }, theme: 'dark' }); };return (<Router><div className="App"><header className="App-header"><h1>微前端容器应用</h1><button onClick={handleLogin}>login</button><nav><Link to="/app1">子应用 1</Link> | <Link to="/app2">子应用 2</Link></nav></header><main id="micro-container" style={{ minHeight: '400px' }}><Routes><Route path="/app1" element={<div>加载中...</div>} >{/* <div>加载中...</div> */}</Route><Route path="/app2" element={<div>加载中...</div>} >{/* <div>加载中...</div> */}</Route><Route path="/" element={<div>请选择一个子应用进行访问</div>} >{/* <div>请选择一个子应用进行访问</div> */}</Route></Routes></main></div></Router>);
}export default App;
子应用接入(React 18+)
入口文件导出生命周期,并且通过context
将actions
方法注入所有组件
在 app1/src/index.tsx
中:
// app1/src/index.tsx
import React, { createContext } from 'react';
import ReactDOM, { Root } from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';interface IGlobalActions {setGlobalState?: (state: any) => void;onGlobalStateChange?: (callback: Function, fireImmediately?: boolean) => void;
}
export const GlobalActionsContext = createContext<IGlobalActions>({});// 在 module scope 保存 root 实例
let root: Root | null = null;function render(props: any) {const { container } = props;// 主应用渲染的时候,会传入一个容器id进来,有的话要优先使用,没有的话,就默认子应用独立渲染const containerDom = container? container.querySelector('#root') as HTMLElement: document.querySelector('#root') as HTMLElement;root = ReactDOM.createRoot(containerDom);root.render(<React.StrictMode><GlobalActionsContext.Provider value={{setGlobalState: props.setGlobalState,onGlobalStateChange: props.onGlobalStateChange}}><App /></GlobalActionsContext.Provider></React.StrictMode>);
}// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();// 如果是在乾坤里面加载,则子应用应该跳过默认的加载逻辑,让主应用来控制当前应用的加载(匹配到子应用的路由标识)
if (!(window as any).__POWERED_BY_QIANKUN__) {render({});
}/** 暂时保持空函数 */
export async function bootstrap() {console.log('app1 bootstraped');
}/*** 挂着子应用的函数入口* @param {*} props*/
export async function mount(props: any) {console.log('app1 mount with props:', props);const { onGlobalStateChange, setGlobalState } = props;// 监听全局状态变化onGlobalStateChange && onGlobalStateChange((state: any, prev: any) => {console.log('[子应用] 收到新状态:', state);// 根据主题更新界面}, true); // 立即触发一次回调 render(props);
}/*** 卸载子应用的函数* @param {*} props*/
export async function unmount(props: any) {// const { container } = props;if (root) {root.unmount(); // 正确卸载root = null;}
}
在子组件app1
中触发修改
// app1/src/App.tsx
import './App.css';import { useContext } from 'react';
import { GlobalActionsContext } from './index';function App() {const handleClick = () => {// 子应用中调用全局方法//(window as any).globalMethods.sendMessage('Hello From Sub App!');// 替例:子应用1发布事件// (window as any).EVENT_BUS.emit('user-login', { userId: 123 });}const { setGlobalState } = useContext(GlobalActionsContext);const handleChangeTheme = (value: any) => {console.log(value)setGlobalState?.({ userInfo: value });};return (<div className="App"><div><h1>子应用 1 - 产品展示</h1><p>这里是子应用 1 的内容。</p><button onClick={() => handleChangeTheme({ name: 'wp', age: '18' })}>修改</button></div></div>);
}export default App;
子应用app1
点击修改按钮