qiankun中的数据通讯方式源码分析

3,791 阅读2分钟

qiankun(乾坤)

qiankun是一个 实现微前端的一个方案(基于single-spa) 我们今天主要看下qiankun 的通讯的使用和源码的分析 可以是我们理解的更加深刻

qiankun中通讯方式的使用

  • 用法 定义全局状态,并返回通信方法,建议在主应用使用,微应用通过 props 获取通信方法。

  • 示例(来自官网) 主应用:

    import { initGlobalState, MicroAppStateActions } from 'qiankun';
    // 初始化 state
    const actions: MicroAppStateActions = initGlobalState(state);
    
    actions.onGlobalStateChange((state, prev) => {
      // state: 变更后的状态; prev 变更前的状态
      console.log(state, prev);
    });
    actions.setGlobalState(state);
    actions.offGlobalStateChange();
    

    微应用:

    // 从生命周期 mount 中获取通信方法,使用方式和 master 一致
    export function mount(props) {
    
      props.onGlobalStateChange((state, prev) => {
        // state: 变更后的状态; prev 变更前的状态
        console.log(state, prev);
      });
    
      props.setGlobalState(state);
    }
    

qiankun中通讯的源码分析

  • 设计模式 qiankun的通讯方式 是一个典型的订阅发布的设计模式
  import { cloneDeep } from 'lodash';
  
  let globalState: Record<string, any> = {};
  const deps: Record<string, OnGlobalStateChangeCallback> = {};

  function initGlobalState(state: Record<string, any> = {}) {
       if (state === globalState) {
           console.warn('[qiankun] state has not changed!');
       } else {
           const prevGlobalState = cloneDeep(globalState);
           globalState = cloneDeep(state);
           emitGlobal(globalState, prevGlobalState);
       }
       return getMicroAppStateActions(`global-${+new Date()}`, true);
   }

初始化 globalState 初始化是一个空的对象 deps 初始化是一个空的对象 用来存放订阅器 我们把初始化对象(state) 传给 initGlobalState 使用lodash 深克隆了我们传的参数 在这里 根据时间戳新建一个id并且 我们返回了getMicroAppStateActions函数的返回结果

  export function getMicroAppStateActions(id: string, isMaster?: boolean): MicroAppStateActions {
    return {
        onGlobalStateChange(callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) {
            if (!(callback instanceof Function)) {
                console.error('[qiankun] callback must be function!');
                return;
            }
            if (deps[id]) {
                console.warn(`[qiankun] '${id}' global listener already exists before this, new listener will overwrite it.`);
            }
            deps[id] = callback;
            const cloneState = cloneDeep(globalState);
            if (fireImmediately) {
                callback(cloneState, cloneState);
            }
        },

        /**
         * setGlobalState 更新 store 数据
         *
         * 1. 对输入 state 的第一层属性做校验,只有初始化时声明过的第一层(bucket)属性才会被更改
         * 2. 修改 store 并触发全局监听
         *
         * @param state
         */
        setGlobalState(state: Record<string, any> = {}) {
            if (state === globalState) {
                console.warn('[qiankun] state has not changed!');
                return false;
            }

            const changeKeys: string[] = [];
            const prevGlobalState = cloneDeep(globalState);
            globalState = cloneDeep(
                Object.keys(state).reduce((_globalState, changeKey) => {
                    if (isMaster || _globalState.hasOwnProperty(changeKey)) {
                        changeKeys.push(changeKey);
                        return Object.assign(_globalState, { [changeKey]: state[changeKey] });
                    }
                    console.warn(`[qiankun] '${changeKey}' not declared when init state!`);
                    return _globalState;
                }, globalState),
            );
            if (changeKeys.length === 0) {
                console.warn('[qiankun] state has not changed!');
                return false;
            }
            emitGlobal(globalState, prevGlobalState);
            return true;
        },

        // 注销该应用下的依赖
        offGlobalStateChange() {
        delete deps[id];
        return true;
        },
    };
  }

onGlobalStateChange 两个参数一个是callback回调函数 一个是fireImmediately 是否直接执行回调函数 我们判断了是不是 函数 和是不是已有的订阅器 我们把订阅器存放在 deps上 如果有新的订阅器 id相同的话,订阅器将会被覆盖

setGlobalState 参数是 state

  1. 对于state 会做第一层的校验 只有是初始化的有的属性才允许被修改
 setGlobalState(state: Record<string, any> = {}) {
            if (state === globalState) {
                console.warn('[qiankun] state has not changed!');
                return false;
            }

            const changeKeys: string[] = [];
            const prevGlobalState = cloneDeep(globalState);
            globalState = cloneDeep(
                Object.keys(state).reduce((_globalState, changeKey) => {
                    if (isMaster || _globalState.hasOwnProperty(changeKey)) {
                        changeKeys.push(changeKey);
                        return Object.assign(_globalState, { [changeKey]: state[changeKey] });
                    }
                    console.warn(`[qiankun] '${changeKey}' not declared when init state!`);
                    return _globalState;
                }, globalState),
            );
            if (changeKeys.length === 0) {
                console.warn('[qiankun] state has not changed!');
                return false;
            }
            emitGlobal(globalState, prevGlobalState);
            return true;
        },

我们做了一个判断它是不是主应用的 isMaster(我们在 initGlobalState 的时候穿的第二个参数) _globalState.hasOwnProperty(changeKey) 判断传入的参数 是不是初始化的时候声明的属性 如果不是控制台警告,并且不会写入 符合条件的推入到changeKeys 并且修改 _globalState 我们判断 changeKeys 的长度如果长度大于1的话我们出发 emitGlobal(globalState, prevGlobalState); 并且返回 修改成功 true

emitGlobal

function emitGlobal(state: Record<string, any>, prevState: Record<string, any>) {
  Object.keys(deps).forEach((id: string) => {
    if (deps[id] instanceof Function) {
      deps[id](cloneDeep(state), cloneDeep(prevState));
    }
  });
}

emitGlobal 我们根据传过来的state 和preState 我把订阅器一个个触发

offGlobalStateChange() {
    delete deps[id];
    return true;
},

offGlobalStateChange 卸载订阅器的钩子

在qiankun我们是如何传给子应用的 props的呢

// 在乾坤里面唯一id
const appInstanceId = `${appName}_${+new Date()}_${Math.floor(Math.random() * 1000)}`;

   // 调用 getMicroAppStateActions 返回 onGlobalStateChange setGlobalState offGlobalStateChange
  const {
    onGlobalStateChange,
    setGlobalState,
    offGlobalStateChange,
  }: Record<string, Function> = getMicroAppStateActions(appInstanceId);

   // 把qiankun 生成生命周期传递给 single-spa
  async props => mount({ ...props, container: containerGetter(), setGlobalState, onGlobalStateChange }),

   // 在 single-spa里面使用getProps获取传过来的自定义参数customProps 最后给我们子应用的钩子函数使用
  app.loadApp(getProps(app));
  const result = assign({}, customProps, {
    name,
    mountParcel: mountParcel.bind(appOrParcel),
    singleSpa,
  });

结尾

qiankun 是一个优秀的js库 他对于开发者 接入较为简单,它的js沙箱和css沙箱设计的比较巧妙 我们下次再来一起阅读它