微前端qiankun-实现主应用和子应用间通信(附带bigfish框架通信原理)

7,529 阅读8分钟

大家好,我是佑子.最近在工作中,碰到了关于微前端中如何实现主应用和子应用的问题.也查阅了相关资料.但是网络上其实五花八门的答案都有,感觉看了后不是特别清楚.所以特此来写一篇文章.一是分享给大家,让大家避免和我一样.少走弯路.二是记录下来,自己以后如果忘记了方便查阅.

关于微前端这个概念,可能有些小伙伴会比较陌生.这里先给不懂的小伙伴简单介绍一下微前端

什么是微前端

把前端做好很难,让多个团队同时开发大型前端应用,就更难了。目前有一种趋势是将前端应用拆分成更小、更易于管理的小应用。这就是所谓的微前端.简单来说,就是一个应用里面,依赖很多的子应用,而子应用又可以单独启动,形成一个应用,更好的方便管理代码.

Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. -- Micro Frontends

微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

微前端架构具备以下几个核心价值:

  • 技术栈无关
    主框架不限制接入应用的技术栈,微应用具备完全自主权

  • 独立开发、独立部署
    微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新

  • 增量升级

    在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略

  • 独立运行时
    每个微应用之间状态隔离,运行时状态不共享

微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith)后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。

目前,市面上有好多方法实现微前端.在我现在做的项目中,则是引用的qiankun这一方案.

qiankun(乾坤)

qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。

Why Not iframe

为什么不用 iframe,这几乎是所有微前端方案第一个会被 challenge 的问题。但是大部分微前端方案又不约而同放弃了 iframe 方案,自然是有原因的,并不是为了 "炫技" 或者刻意追求 "特立独行"。

如果不考虑体验问题,iframe 几乎是最完美的微前端解决方案了。

iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题。

  1. url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
  2. UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..
  3. 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
  4. 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。

其中有的问题比较好解决(问题1),有的问题我们可以睁一只眼闭一只眼(问题4),但有的问题我们则很难解决(问题3)甚至无法解决(问题2),而这些无法解决的问题恰恰又会给产品带来非常严重的体验问题, 最终导致我们舍弃了 iframe 方案。

qiankun中实现主应用子应用通信

先来举一个需要实现应用间通信的场景,如下

image.png

红色的框是主应用,绿色是子应用.当我想点击版本,进行路由切换时,同时收起侧边栏.控制侧边栏显示隐藏的代码在主应用里,点击版本进行路由切换的代码在子应用里.这里就涉及到了主应用和子应用间的通信.

主应用子应用间的通信有好多方式,这里我分两种来说,一是市场主流框架.例如react,vue.二是蚂蚁内部框架 bigfish实现主应用子应用通信

react,vue实现应用通信(一)

主应用与子应用的通讯只是用了APIinitGlobalState
initGlobalState返回一个MicroAppStateActions对象,它有三个属性:

  • onGlobalStateChange: (callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) => void, 在当前应用监听全局状态,有变更触发 callback,fireImmediately = true 立即触发 callback
  • setGlobalState: (state: Record<string, any>) => boolean, 按一级属性设置全局状态,微应用中只能修改已存在的一级属性
  • offGlobalStateChange: () => boolean,移除当前应用的状态监听,微应用 umount 时会默认调用

父应用如何做

import { initGlobalState } from 'qiankun';
const state = {
    baiduinit: window,
    abc: 456
}
// 初始化通信池
const actions = initGlobalState(state);
// 监听通讯池的变化
actions.onGlobalStateChange((state, prev) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log(state, prev);
});
复制代码

子应用如何做

1.创建action.js

// /src/qiankun/action.js
function emptyAction() {
    // 提示当前使用的是空 Action
    console.warn("Current execute action is empty!");
}

class Actions {
    // 默认值为空 Action
    actions = {
        onGlobalStateChange: emptyAction,
        setGlobalState: emptyAction,
    };

    /**
     * 设置 actions
     */
    setActions(actions) {
        this.actions = actions;
    }

    /**
     * 映射
     */
    onGlobalStateChange() {
        return this.actions.onGlobalStateChange(...arguments);
    }

    /**
     * 映射
     */
    setGlobalState() {
        return this.actions.setGlobalState(...arguments);
    }
}

const actions = new Actions();
export default actions;

复制代码

2.在main.js mount方法中接收父应用的传值props

import action from './qiankun/action'
export async function mount(props) {
    console.log('[vue] props from main framework', props);
    action.setActions(props)
    render(props);
}
复制代码

3.在需要接收父应用传入的参数的地方引用action.js

import action from '@/qiankun/action'
export default {
    name: 'Home',
    mounted() {
        // 接收state
        action.onGlobalStateChange((state) => {
            console.log(state)
        }, true);
    },
    methods:{
        changeValue(){
            // 修改state
            action.setGlobalState({abc:789})
        }
    }
}
复制代码

子应用可以修改通讯池,修改完会被主应用监听到。

react,vue实现应用通信(二)

该方法我在bigfish框架中用的,react和vue应该也行 原理只是将方法挂在到window上,多一个全局函数

第二种方法相对简单,直接在主应用的main.js这个文件中,将方法挂在到window上面,这样子应用就可以通过window拿到想要的属性和方法了

首先,我们先将上面那个例子的控制侧边栏的状态,保存到redux中,实现组件通信

export const namespace = 'collapsableSider';
type State = {
  collapsed: boolean;
};
export default {
  namespace,
  state: {
    collapsed: false,
  },

  reducers: {
    setCollapsed(state: State, { payload }) {
      return {
        ...state,
        collapsed: payload,
      };
    },
  },
};

然后,在主应用的main.js文件中,将修改侧边栏的方法.挂在到全局函数上

window.sdk = {
  setCollapsed: function (value) {
    getDvaApp()._store.dispatch({
      type: 'collapsableSider/setCollapsed',
      payload: value,
    });
  },
};

接下来,最简单的一步.子应用哪里需要用到,直接访问window即可

  // 进入详情页隐藏侧边栏
  useEffect(() => {
    if (window?.win?.sdk?.setCollapsed) {
      window.win.sdk.setCollapsed(true)
    }
    return (() => {
      if (window?.win?.sdk?.setCollapsed) {
        window.win.sdk.setCollapsed(false)
      }

    })
  }, [])

到这里,应用通信就完成了.当然,因为我们这是微前端项目,子应用可以单独启动.所以要做一个判断.避免如果单独启动子应用.没有对应的函数时候报错问题.

bigfish实现应用通信

配合 useModel 使用(推荐)

需确保已安装 @umijs/plugin-model 或 @umijs/preset-react

  1. 主应用使用下面任一方式透传数据:
  1. 如果你用的 MicroApp 组件模式消费微应用,那么数据传递的方式就跟普通的 react 组件通信是一样的,直接通过 props 传递即可:

    function MyPage() {
      const [name, setName] = useState(null);
      return (
        <MicroApp name={name} onNameChange={(newName) => setName(newName)} />
      );
    }
    
  1. 如果你用的 路由绑定式 消费微应用,那么你需要在 src/app.ts 里导出一个 useQiankunStateForSlave 函数,函数的返回值将作为 props 传递给微应用,如:

    // src/app.ts
    export function useQiankunStateForSlave() {
      const [masterState, setMasterState] = useState({});
    
      return {
        masterState,
        setMasterState,
      };
    }
    
  1. 微应用中会自动生成一个全局 model,可以在任意组件中获取主应用透传的 props 的值。

    import { useModel } from '@alipay/bigfish';
    
    function MyPage() {
      const masterProps = useModel('@@qiankunStateFromMaster');
      return <div>{JSON.stringify(masterProps)}</div>;
    }
    

    或者可以通过高阶组件 connectMaster 来获取主应用透传的 props

    import { connectMaster } from '@alipay/bigfish';
    
    function MyPage(props) {
      return <div>{JSON.stringify(props)}</div>;
    }
    
    export default connectMaster(MyPage);
    
  1. 和 &lt;MicroApp /&gt; 的方式一同使用时,会额外向子应用传递一个 setLoading 的属性,在子应用中合适的时机执行 masterProps.setLoading(false),可以标记微模块的整体 loading 为完成状态。

基于 props 传递

类似 react 中组件间通信的方案

  1. 主应用中配置 apps 时以 props 将数据传递下去

    // src/app.js
    
    export const qiankun = fetch('/config').then((config) => {
      return {
        apps: [
          {
            name: 'app1',
            entry: '//localhost:2222',
            props: {
              onClick: (event) => console.log(event),
              name: 'xx',
              age: 1,
            },
          },
        ],
      };
    });
    
  1. 子应用在生命周期钩子中获取 props 消费数据

到这里,qiankun中应用通信已经结束了.如果有什么不懂的话有错误的.欢迎在评论区指出

参考资料:

bigfish.antfin-inc.com/doc/plugin-… juejin.cn/post/704929…