微前端简单的实现主应用和子应用之间的通信(qiankun)
前面已经搭建好一个简单的微前端项目,现在再简单研究下主应用和子应用之间的通信。
在前面搭建好的项目中,使用了类似后台管理系统的样式。现在主要是研究其通信,参考 官方文档 即可。
需求
就简单的定个需求,将子应用的路由也渲染在主应用的左侧菜单下,并作为该子应用下的子菜单。
需求分析
来个仪式感,也做一个简单的需求分析吧,既然要主应用要渲染子应用的路由,并生成一个子菜单,那么在子应用挂载的时候,子应用将自己的路由信息传给主应用就行了。其实 qiankun 使用的是全局状态,子应用改变这个状态,然后可以通过监听这个改变,做我们想做的事情。
编码
主应用
在 src 下新建 actions.ts 文件(你也可以不建,直接在配置,也可以建在其他位置):
import { initGlobalState, MicroAppStateActions } from 'qiankun';
// 初始化的全局状态
const state = {
initData: '这是主应用传过来的初始数据',
reactRoutes: [],
vueRoutes: []
};
// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);
// actions.onGlobalStateChange((state, prev) => {
// // state: 变更后的状态; prev 变更前的状态
// console.log(state, prev);
// });
actions.setGlobalState(state);
// 取消监听
// actions.offGlobalStateChange();
export default actions;
现在我们就配置了一些全局的状态,接下来我们看子应用能否获取到。
react 子应用
参考官网,在子应用挂载的时候,我们可以从 props 中获取到一些有用的东西,接着前面的项目写:
src/index.tsx
import './public-path';
import React from 'react';
import ReactDOMClient, { Root } from 'react-dom/client';
import { BrowserRouter as Router } from 'react-router-dom'
import App from './App';
let root: Root | null = null;
function render(props: any) {
const { container } = props;
root = ReactDOMClient.createRoot(
container ? container.querySelector('#microReactRoot') as HTMLElement : document.getElementById('microReactRoot') as HTMLElement
);
root.render(
<React.StrictMode>
<Router basename={window.__POWERED_BY_QIANKUN__ ? '/micro-react' : '/'}>
<App />
</Router>
</React.StrictMode>
);
}
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
export async function bootstrap() {
console.log('[react16] react app bootstraped');
}
// 修改的地方 ----------------------------------- start
export async function mount(props: any) {
// console.log(props, 'RRRRRRRRRRRRRRRRRR')
// 子应用监听数据变化
props.onGlobalStateChange((state: any, prev: any) => {
// state: 变更后的状态; prev 变更前的状态
// console.log(state, prev, '++++++++++++++++++++++');
// 假设只有在没有设置主应用的数据的时候我们才去设置
// 如果已经设置了就不用重复设置了
if (!state.reactRoutes.length) {
// 改变数据,将子应用自己的路由传过去。
// 当然,这里只是验证通信,不考虑其他的
const _state = {
reactRoutes: [
{ path: '/', title: 'reactHome' },
{ path: '/about', title: 'reactAbout' }
]
}
// 设置数据
props.setGlobalState(_state);
}
}, true);
render(props);
}
// 修改的地方 ----------------------------------- end
export async function unmount(props: any) {
console.log(props);
root?.unmount();
}
放开打印的地方,我们就可以在控制台看到一些信息,接下来配置 vue 子应用。
vue2 子应用
因为 vue2 子应用有自己的路由配置文件,所以要引入使用。一般菜单就是路由 + 跳转的链接,所以我们只要有这两个信息即可。
src/main.js
import Vue from 'vue'
import App from './App.vue'
import routes from './router'
import VueRouter from 'vue-router'
import store from './store'
import './public-path'
Vue.use(VueRouter)
Vue.config.productionTip = false
let router = null
let instance = null
function render() {
!router && (router = new VueRouter({
mode: 'history',
base: window.__POWERED_BY_QIANKUN__ ? 'micro-vue' : '/',
routes
}))
!instance && (instance = new Vue({
router,
store,
render: h => h(App)
}).$mount('#microVueRoot'))
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
render()
}
export async function bootstrap(props) {
console.log('[vue] vue app bootstraped', props)
}
// 修改的地方 --------------------------------------- start
export async function mount(props) {
// console.log('[vue] props from main framework')
// 监听数据变化
// , prev
props.onGlobalStateChange((state) => {
// state: 变更后的状态; prev 变更前的状态
// console.log(state, prev, 'VVVVVVVVVVVVVVVVVVVVVV');
if (!state.vueRoutes.length) {
// 改变数据
const _state = {
vueRoutes: routes
}
// 设置数据
props.setGlobalState(_state);
}
}, true); // 第二个参数设置为 true 表示一上来就执行一次
render(props)
}
// 修改的地方 --------------------------------------- end
export async function unmount() {
instance.$destroy()
instance.$el.innerHTML = ''
instance = null
router = null
}
同样的,我们放开打印的部分,可以看到控制台输出了一些信息,这些也都包含了我们子应用的路由信息了。接下来,就是在主应用中去配置生成菜单了。
继续更改主应用
src/layouts/index.tsx
import {
DesktopOutlined,
PieChartOutlined
} from '@ant-design/icons';
import type { MenuProps } from 'antd';
import { Breadcrumb, Layout, Menu } from 'antd';
import React, { useState, useEffect } from 'react';
import { Link, useLocation } from 'react-router-dom';
import './index.css';
// 引入创建的 actions
import actions from '../actions';
const { Header, Content, Footer, Sider } = Layout;
type MenuItem = Required<MenuProps>['items'][number];
function getItem(
label: React.ReactNode,
key: React.Key,
icon?: React.ReactNode,
children?: MenuItem[],
): MenuItem {
return {
key,
icon,
children,
label,
} as MenuItem;
}
// 简单的生成菜单的函数
const setMenus = (state: any) => {
let _items = [];
const vue2Children = state.vueRoutes.map((item: any) => {
return getItem(<Link to={`/micro-vue${item.path}`}>{item.meta.title}</Link>, `/micro-vue${item.path}`)
});
const reactChildren = state.reactRoutes.map((item: any) => {
return getItem(<Link to={`/micro-react${item.path}`}>{item.title}</Link>, `/micro-react${item.path}`)
});
_items = [
getItem(<Link style={{color: '#fff'}} to="/micro-vue/">vue2 子应用</Link>, '/micro-vue', <PieChartOutlined />, vue2Children),
getItem(<Link style={{color: '#fff'}} to="/micro-react/">react 子应用</Link>, '/micro-react', <DesktopOutlined />, reactChildren)
];
return _items;
}
const LayoutIndex: React.FC = () => {
const { pathname } = useLocation();
const [collapsed, setCollapsed] = useState(false);
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
const [openKeys, setOpenKeys] = useState<string[]>([]);
const [breadcrumb, setBreadcrumb] = useState<string[]>([]);
const [menuList, setMenuList] = useState<MenuItem[]>([
getItem(<Link to="/micro-vue/">vue2 子应用</Link>, '/micro-vue', <PieChartOutlined />),
getItem(<Link to="/micro-react/">react 子应用</Link>, '/micro-react', <DesktopOutlined />)
]);
useEffect(() => {
setSelectedKeys([pathname]);
const pathArr = pathname.split('/');
// 只考虑有一个子节点的情况,没有考虑多级路由
if (pathArr.length >= 3) {
setOpenKeys(['/' + pathArr[1]]);
} else {
setOpenKeys([pathname]);
}
setBreadcrumb(pathArr);
}, [pathname]);
useEffect(() => {
// 监听变化,重新设置菜单
actions.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
// console.log(state, prev, '********************');
setMenuList(setMenus(state));
}, true);
return () => {
// 取消监听
actions.offGlobalStateChange();
};
}, []);
// useEffect(() => {
// console.log('主应用菜单变化了');
// }, [menuList]);
return (
<Layout className="container" style={{ minHeight: '100vh' }}>
<Sider collapsible collapsed={collapsed} onCollapse={value => setCollapsed(value)}>
<div className="logo" />
<Menu theme="dark" selectedKeys={selectedKeys} mode="inline" items={menuList} openKeys={openKeys} />
</Sider>
<Layout className="site-layout">
<Header className="site-layout-background" style={{ padding: 0 }} />
<Content style={{ margin: '0 16px' }}>
<Breadcrumb style={{ margin: '16px 0' }}>
{
breadcrumb.map(item => <Breadcrumb.Item key={item}>{item}</Breadcrumb.Item>)
}
</Breadcrumb>
<div className="site-layout-background" style={{ padding: 24, minHeight: 360 }}>
{/* 微应用容器 */}
<div id="reactContainer"></div>
<div id="vueContainer"></div>
</div>
</Content>
<Footer style={{ textAlign: 'center' }}>微前端 Demo 主应用</Footer>
</Layout>
</Layout>
);
};
export default LayoutIndex;
至此,就完成了应用的简单通信。当然如果你要用于生产环境,也可以参考网上其他更利于生产环境的资料去实现你项目的需求,这里只是简单学习研究一下。