微前端框架在umi体系中的项目实战

1,938 阅读6分钟

随着微前端构架日益完善,前端工程也不再局限于传统的巨石应用化开发,借鉴了后端微服务化的开发思想,前端开发也越发趋近于微前端的开发模式;那在当前最火热的react框架umi中,怎么样实现无缝对接qiankun呢?下面就来讲讲怎么样快速在umi中集成qiankun框架。

umi的强大之处,除了它的开箱即用,内部集成antd+dva等等全家桶,另一个强大之处就是它的插件体系,毫不意外的是qiankun也在其中,详见# @umijs/plugin-qiankun;基本的启用方式这里就不多做阐述了,你只需要照着官网通过简单的几步就能完美的将qiankun集成到umi中来;

这文章主要来聊聊两个问题:

  1. 如何实现动态注册(注册菜单及动态注册路由)
  2. 主子应用通信方案

如何实现动态注册

静态注册依照官网文档便可以很轻易实现,直接在config.ts文件写入主子应用配置即可,那什么情况下需要考虑动态注册的场景呢?

  1. 权限控制
  2. 当前端应用不确定时,存在动态情况

这两者都有一个共同点,就是需要依赖接口,在后端接口返回之前无法得知具体子应用及其数量,亦或是硬编码的方式带来了一系列的不好的交互体验;且说第一种场景,虽然我们可以通过静态注册的方式,然后进入到各子应用之后,各子应用根据权限判断是否具备访问权限,不具备权限则重定向至xxx页面;之所以不推荐这种写法是因为其带来两个比较致命的地方。其一每个子应用都需要各自添加拦截处理,判断是否具备访问权限,先不说代码冗余问题,权限数据是各自请求还是通过主应用透传就需要各自斟酌;其二从用户角度来讲,先进入到页面而后被判断到无权限时跳转,交互体验本身就不太好;别的不多说直接干代码: 在app.tsx里加入如下:

export async function qiankun() {
 
  const rs: any = {
    apps: [], // 动态子应用信息
    routes: [], // 动态路由,参照react-router, { name: xxx, path: xxx, icon: xxx }
    lifeCycles: {
      afterMount: (props: any) => {
        console.log(props);
      },
    },
  };
  
    const res: any = await queryMenuList(); // 动态菜单数据,或者在此根据权限过滤子应用
    if (res.statusCode === '200' && res.datas?.length) {
        const menuList = res.datas[0].children;
        const { apps, routes } = parseMenu(menuList, query);
        rs.apps = apps;
        rs.routes = routes;
    }
  return Promise.resolve(rs)
}

使用这种方式的好处,不仅仅是子应用,路由也可以在此动态注册了,所有的拦截配置完美的地收拢在了主应用上,子应用无需做任务改动。使用这种方式可以完美的解决子应用及路由的动态注册问题,那会不会有什么问题呢?答案是有的。从umi的开放性原则来看,静态配置一般维护在config.ts之中,app.tsx维护的动态的运行时配置,里边的注册信息只会在初始化的时候执行一次,之后便不会再执行了。但人的欲望总是没有止境的,突然有一天,来了一个需求说,我希望添加一个xxx按钮,点击按钮之后重新加载一遍配置。可能你会说这有何难,既然只执行一次,那重新reload一遍不就好了。巧了,这时需求又说能不能在不刷新的情况下做到动态加载呢,这下可把我难住了。文章写到这,那肯定是有解决之法的,不然抛出来这个问题是讨打来了吗?

勇敢牛牛不怕吃苦不怕累,终于也还是找到了解决之法,既然qiankun是通过插件体系集成进来,那可不可以从这个思路出发,看看初始化的时候是如何调用这个插件,并且执行到了app.tsx中的qiankun方法呢?作为一个前端老油条,上手就在qiankun()中加入一个console.trace('qiankun调用栈'),一通源码乱啃,还真找到了一个有趣的玩意:plugin.applyPlugins,最后一通瞎搞,终于含泪写下如下代码,亲测有效:

import { history, plugin, ApplyPluginsType  } from "umi";
import { setMasterOptions, getMasterOptions } from "@@/plugin-qiankun/masterOptions";

function reloadSetting() {
    const config = await plugin.applyPlugins({
      key: 'qiankun',
      type: ApplyPluginsType.modify,
      initialValue: {},
      args: {},
      async: true,
    });
    const masterOptions = { ...getMasterOptions(), ...config};
    console.log('masterOptions', masterOptions, )

    setTimeout(() => {
      setMasterOptions(masterOptions);
    }, 0)
}

但是一波未平,一波又起,执行reloadSetting方法虽然是重新执行了qiankun方法,重新加载到了最新配置,但是作为一个专业的工程师能这么草率吗,果然在一通log打印之后发现了一个问题,qiankun方法中虽然可以实现动态子应用及路由注册,但这个却是追加式的,而不是覆盖式的,每执行一次,路由体积就会增大…………完了,瞬间觉得自己卷不动了,又给自己挖了个大坑,只能遗憾留下一句,此物虽好,请谨慎用之,不知大神们有何解决之法?

主子应用通信问题

  1. useQiankunStateForSlave + useModel,详见: 访问
export function useQiankunStateForSlave() {
  const { userInfo }: any = useModel('@@qiankunStateFromMaster') || {};
  const [data, setData] = useState();
  
  // 这里也可以是一个异步请求
  fetchData().then((res) => {
      setData(res)
  })

  return {
    data, 
    setData,
    userInfo,
  };
}
  1. MicroAppWithMemoHistory
import React from 'react';
import { MicroAppWithMemoHistory } from 'umi';

import styles from './index.less';

interface Props {
  name: string;
  url: string;
  dataProps: any;
  [key: string]: any;
}

const MicroApp = (props: Props) => {
  const {
    name,
    url,
    ...restProps
  } = props;

  return (
    <div style={{height: '100%'}}>
      <MicroAppWithMemoHistory
        className={styles.microApp}
        name={name}
        url={url}
        style={{height: '100%'}}
        {...restProps}
      />
    </div>
  )
}
export default MicroApp;


使用:
<MicroApp name='name' url='url' dataProps={{}}/>

使用MicroAppWithMemoHistory的好处是,支持访问指定子应用的指定页面,即通过url指定访问哪个页面;其次就是可以让子应用渲染挂载在任意位置,比如:

const Page = (props: Props) => {
  const {
    name,
    url,
    ...restProps
  } = props;

  return (
    <div style={{height: '100%'}}>
      <div className='left'>
          // 这里是侧边栏
          <SiderBar>
      </div>
      <div className='right'>
          <h1>这里标题</h1>
          <div>
              // 这里才是子应用渲染的位置
              <MicroAppWithMemoHistory
                className={styles.microApp}
                name={name}
                url={url}
                style={{height: '100%'}}
                {...restProps}
              />
          </div>
      </div>
    </div>
  )
}
export default Page;

其它的通信方式暂时没有进行过多的尝试,不过在上边的例子受到的一些启发就是,主子应用实现透传基本原理都是暴露在一个全局model上,然后子应用更多是通过useModel消费,这样的好处是做到实时刷新,所以透传思路就可以在怎么集成到那个全局model里了,具体实现暂时没有去调研,估计又得干一波源码了。

总结

  1. umi对qiankun的支持度已经很高了,通过其插件体系,改造成本也不大
  2. 在主子应用通信上也有一套比较完备的解决方案了,通过全局model共享数据,useModel进行消费,相比iframe这种又高效了很多,也不用考虑各种跨域问题
  3. 通过个人实践发现也还是有一些问题的,比如在样式隔离上没有做到完全隔离,在多tab的场景多个子应用共存的情况下,还是会存在样式影响的,目前在隔离层上还是需要各个子应用通过css module等添加css作用域的形式来进行隔离,但是不管如何还是会越来越好的