微前端简单的实现主应用和子应用之间的通信(qiankun)

997 阅读3分钟

微前端简单的实现主应用和子应用之间的通信(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;

至此,就完成了应用的简单通信。当然如果你要用于生产环境,也可以参考网上其他更利于生产环境的资料去实现你项目的需求,这里只是简单学习研究一下。

最终效果

GIF 2022-7-25 17-24-43.gif