• 浅析微前端qiankun的2种应用间通信方式(actions通信及shared通信、各自通信原理及实例代码)


    一、应该如何划分子应用

      在开始介绍 qiankun 的应用通信之前,我们需要先了解微前端架构如何划分子应用。在微前端架构中,我们应该按业务划分出对应的子应用,而不是通过功能模块划分子应用。这么做的原因有两个:

    1. 在微前端架构中,子应用并不是一个模块,而是一个独立的应用,我们将子应用按业务划分可以拥有更好的可维护性和解耦性。
    2. 子应用应该具备独立运行的能力,应用间频繁的通信会增加应用的复杂度和耦合度。

      综上所述,我们应该从业务的角度出发划分各个子应用,尽可能减少应用间的通信,从而简化整个应用,使得我们的微前端架构可以更加灵活可控。

      补充一下,上面是理想状态,但是理想状态我们很难达到。微前端最核心的点,是可以做到技术栈无关。比如如果你为了做到技术栈无关,想体验不同的技术,那就无所谓了,直接按模块划分即可,因为单独独立的应用,很多公司遇到这种需求也蛮少。我就是为了实践不同技术栈,所以上了微前端。

      本次将介绍两种通信方式:

    1. 第一种是 qiankun 官方提供的通信方式 - Actions 通信,适合业务划分清晰,比较简单的微前端应用,一般来说使用第一种方案就可以满足大部分的应用场景需求。
    2. 第二种是基于 redux 实现的通信方式 - Shared 通信,适合需要跟踪通信状态,子应用具备独立运行能力,较为复杂的微前端应用。

    二、Actions 通信

      我们先介绍官方提供的应用间通信方式 - Actions 通信,这种通信方式比较适合业务划分清晰,应用间通信较少的微前端应用场景。

    1、通信原理

      qiankun 内部提供了 initGlobalState 方法用于注册 MicroAppStateActions 实例用于通信,该实例有三个方法,分别是:

    • setGlobalState:设置 globalState - 设置新的值时,内部将执行 浅检查,如果检查到 globalState 发生改变则触发通知,通知到所有的 观察者 函数。
    • onGlobalStateChange:注册 观察者 函数 - 响应 globalState 变化,在 globalState 发生改变时触发该 观察者 函数。
    • offGlobalStateChange:取消 观察者 函数 - 该实例不再响应 globalState 变化。

      我们来画一张图来帮助大家理解(见下图)

      我们从上图可以看出,我们可以先注册 观察者 到观察者池中,然后通过修改 globalState 可以触发所有的 观察者 函数,从而达到组件间通信的效果。

    2、通信实例

    (1)主应用需要做的工作

    // micro-app-main/src/shared/actions.ts
    import { initGlobalState, MicroAppStateActions } from "qiankun";
    
    const initialState = { status: 'login' }; // 全局状态池给了个默认值
    const shareActions: MicroAppStateActions = initGlobalState(initialState);
    
    export default shareActions;

    (2)组件里注册观察者函数,在使用时触发全局状态池更新

      在注册 MicroAppStateActions 实例后,我们在需要通信的组件中使用该实例,并注册 观察者 函数,我们这里以登录功能为例,实现如下:

    // 1、先引入 actions
    import actions from "@/shared/actions";
    @Component
    export default class Login extends Vue {
      mounted() {
        // 2、注册一个观察者函数
        actions.onGlobalStateChange((state, prevState) => {
          console.log("主应用观察者:token 改变前的值为 ", prevState.token);
          console.log("主应用观察者:登录状态发生改变,改变后的 token 的值为 ", state.token);
        });
      }
      async login() {
        // 3、触发状态池更新:登录成功后,设置 token
        actions.setGlobalState({ token });
      }
    }

    (3)子应用需要做的工作

      我们首先来改造我们的 Vue 子应用,首先我们设置一个 Actions 实例,代码实现如下:

    // micro-app-vue/src/shared/actions.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(...args) {
        return this.actions.onGlobalStateChange(...args);
      }
      // 映射
      setGlobalState(...args) {
        return this.actions.setGlobalState(...args);
      }
    }
    const actions = new Actions();
    export default actions;
      我们创建 actions 实例后,我们需要为其注入真实 Actions。我们在入口文件 main.jsrender 函数中注入,代码实现如下:
    // micro-app-vue/src/main.js
    //...
    
    /**
     * 渲染函数
     * 主应用生命周期钩子中运行/子应用单独启动时运行
     */
    function render(props) {
      if (props) {
        // 注入 actions 实例
        actions.setActions(props);
      }
       ......
    }

      从上面的代码可以看出,挂载子应用时将会调用 render 方法,我们在 render 方法中将主应用的 actions 实例注入即可。

      最后我们在子应用的 通讯页 获取 globalState 中的 token,使用 token 来获取用户信息,最后在页面中显示用户信息。流程伪代码实现如下:

    // 引入 actions 实例
    import actions from "@/shared/actions";
    export default {
      ......
      mounted() {
        // 注册观察者函数:onGlobalStateChange 第二个参数为 true,表示立即执行一次观察者函数
        actions.onGlobalStateChange(state => {
          const { token } = state;
          // 未登录 - 返回主页
          if (!token) {
            this.$message.error("未检测到登录信息!");
            return this.$router.push("/");
          }
          // 获取用户信息
          this.getUserInfo(token);
        }, true);
      },
       ......
    };

      从上面的代码可以看到,我们在组件挂载时注册了一个 观察者 函数并立即执行,从 globalState/state 中获取 token,然后使用 token 获取用户信息,最终渲染在页面中。

    3、总结

      到这里,qiankun 基础通信 就完成了!我们在主应用中实现了登录功能,登录拿到 token 后存入 globalState 状态池中。在进入子应用时,我们使用 actions 获取 token,再使用 token 获取到用户信息,完成页面数据渲染!

      最后我们画一张图帮助大家理解这个流程(见下图):

    4、在项目里实战:在 Nuxt.js 里使用的问题

      由于在 nuxtjs 里服务器不支持浏览器相关的 api,如 actions 的 initGlobalState 里却使用了很多浏览器 api,所以会报错。如何解决呢?

      使用 插件 plugins 配合 ssr: false,使插件在客户端时才加载,这样就可以了。

    (1)主应用需要做的

    // ssr: false 插件里引入
    import shareActions from "@/utils/shareActions"
    // 注册一个观察者函数
    shareActions.onGlobalStateChange((state, prevState) => {
        console.log("主应用观察者:变更前-", state, ",变更后-", prevState)
    })
    Vue.prototype.$actions = shareActions
    // 在需要使用的地方直接这样操作即可
    this.$actions.setGlobalState({ logStatus: 'logout' })

    (2)子应用对应业务

    // 子应用里直接注册监听函数,然后做对应业务即可
    export async function mount(props: any) {
      // console.log('[vue] props from main framework', props)
      render()
      props?.onGlobalStateChange((state: any) => {
        state?.logStatus === 'logout' && store.commit('clearToken')
      }, true)
    }

    三、Shared 通信

      官方提供的 Actions 通信方案是通过全局状态池和观察者函数进行应用间通信,该通信方式适合大部分的场景。Actions 通信方案也存在一些优缺点,优点如下:

    1. 使用简单;
    2. 官方支持性高;
    3. 适合通信较少的业务场景;

      缺点如下:

    1. 子应用独立运行时,需要额外配置无 Actions 时的逻辑;
    2. 子应用需要先了解状态池的细节,再进行通信;
    3. 由于状态池无法跟踪,通信场景较多时,容易出现状态混乱、维护困难等问题;

      如果你的应用通信场景较多,希望子应用具备完全独立运行能力,希望主应用能够更好的管理子应用,那么可以考虑 Shared 通信方案。

    1、通信原理

      Shared 通信方案的原理就是,主应用基于 redux 维护一个状态池,通过 shared 实例暴露一些方法给子应用使用。同时,子应用需要单独维护一份 shared 实例,在独立运行时使用自身的 shared 实例,在嵌入主应用时使用主应用的 shared 实例,这样就可以保证在使用和表现上的一致性。

      Shared 通信方案需要自行维护状态池,这样会增加项目的复杂度。好处是可以使用市面上比较成熟的状态管理工具,如 reduxmobx,可以有更好的状态管理追踪和一些工具集。

      Shared 通信方案要求父子应用都各自维护一份属于自己的 shared 实例,同样会增加项目的复杂度。好处是子应用可以完全独立于父应用运行(不依赖状态池),子应用也能以最小的改动被嵌入到其他 第三方应用 中。

      Shared 通信方案也可以帮助主应用更好的管控子应用。子应用只可以通过 shared 实例来操作状态池,可以避免子应用对状态池随意操作引发的一系列问题。主应用的 Shared 相对于子应用来说是一个黑箱,子应用只需要了解 Shared 所暴露的 API 而无需关心实现细节

      个人总结一下:

      (1)比如我们主应用使用现有vuex状态管理,然后再提供一个 shared 实例暴露一些 api 来操作其 vuex,再把 shared 实例通过 props 传给子应用;

      (2)子应用使用现有vuex状态管理,子应用也实现自己的shared;然后在入口文件处如果主应用有传 shared 实例,则使用主应用实例,没有(即独立运行时)则使用子应用自己的 shared实例。

    2、通信实例

    (1)主应用的工作

       首先我们需要在主应用中创建 store 用于管理全局状态池,这里我们使用 redux 来实现

    // micro-app-main/src/shared/store.ts
    import { createStore } from "redux";
    
    export type State = {
      token?: string;
    };
    type Action = {
      type: string;
      payload: any;
    };
    const reducer = (state: State = {}, action: Action): State => {
      switch (action.type) {
        default:
          return state;
        // 设置 Token
        case "SET_TOKEN":
          return {
            ...state,
            token: action.payload,
          };
      }
    };
    
    const store = createStore<State, Action, unknown, unknown>(reducer);
    export default store;

      从上面可以看出,我们使用 redux 创建了一个全局状态池,并设置了一个 reducer 用于修改 token 的值。

      接下来我们需要实现主应用的 shared 实例,代码实现如下:

    // micro-app-main/src/shared/index.ts
    import store from "./store";
    class Shared {
      // 获取 Token
      public getToken(): string {
        const state = store.getState();
        return state.token || "";
      }
      // 设置 Token
      public setToken(token: string): void {
        // 将 token 的值记录在 store 中
        store.dispatch({
          type: "SET_TOKEN",
          payload: token
        });
      }
    }
    const shared = new Shared();
    export default shared;
      从上面实现可以看出,我们的 shared 实现非常简单,shared 实例包括两个方法 getTokensetToken 分别用于获取 token 和设置 token。接下来我们还需要对我们的 登录组件 进行改造,将 login 方法修改一下,修改如下:
    // micro-app-main/src/pages/login/index.vue
    async login() {
      // 使用 shared 的 setToken 方法记录 token
      shared.setToken(token);
      this.$router.push("/");
    }

      从上面可以看出,在登录成功后,我们将通过 shared.setToken 方法进行业务处理token 记录在 store 中。

      最后,我们需要shared 实例通过 props 传递给子应用,代码实现如下:

    // micro-app-main/src/micro/apps.ts
    import shared from "@/shared";
    const apps = [
      {
        name: "ReactMicroApp",
        entry: "//localhost:10100",
        container: "#frame",
        activeRule: "/react",
        // 通过 props 将 shared 传递给子应用
        props: { shared },
      },
      {
        name: "VueMicroApp",
        entry: "//localhost:10200",
        container: "#frame",
        activeRule: "/vue",
        // 通过 props 将 shared 传递给子应用
        props: { shared },
      },
    ];
    
    export default apps;

    (2)子应用的工作

      现在,我们来处理子应用需要做的工作。我们刚才提到,我们希望子应用有独立运行的能力,所以子应用也应该实现 shared,以便在独立运行时可以拥有兼容处理能力。代码实现如下:

    // micro-app-vue/src/shared/index.js
    class Shared {
      // 获取 Token
      getToken() {
        // 子应用独立运行时,在 localStorage 中获取 token
        return localStorage.getItem("token") || "";
      }
      // 设置 Token
      setToken(token) {
        // 子应用独立运行时,在 localStorage 中设置 token
        localStorage.setItem("token", token);
      }
    }
    
    class SharedModule {
      static shared = new Shared();
      // 重载 shared
      static overloadShared(shared) {
        SharedModule.shared = shared;
      }
      // 获取 shared 实例
      static getShared() {
        return SharedModule.shared;
      }
    }
    export default SharedModule;

      从上面我们可以看到两个类,我们来分析一下其用处:

    • Shared:子应用自身的 shared,子应用独立运行时将使用该 shared,子应用的 shared 使用 localStorage 来操作 token
    • SharedModule:用于管理 shared,例如重载 shared 实例、获取 shared 实例等等;

      我们实现了子应用的 shared 后,我们需要在入口文件处注入 shared,代码实现如下:

    // micro-app-vue/src/main.js
    // 渲染函数:主应用生命周期钩子中运行/子应用单独启动时运行
    function render(props = {}) {
      // 当传入的 shared 为空时,使用子应用自身的 shared
      // 当传入的 shared 不为空时,主应用传入的 shared 将会重载子应用的 shared
      const { shared = SharedModule.getShared() } = props;
      SharedModule.overloadShared(shared);
      ......
    }

      从上面可以看出,我们在 propsshared 字段不为空时,将会使用传入的 shared 重载子应用自身的 shared。这样做的话,主应用的 shared 和子应用的 shared 在使用时的表现是一致的。

      然后我们修改子应用的 通讯页,使用 shared 实例获取 token,代码实现如下:

    // 引入 SharedModule
    import SharedModule from "@/shared";
    
    export default {
      mounted() {
        const shared = SharedModule.getShared();
        // 使用 shared 获取 token
        const token = shared.getToken();
      }
    };

    3、总结

      我们从上面的案例也可以看出 Shared 通信方案的优缺点,这里也做一些简单的分析,优点有这些:

    • 可以自由选择状态管理库,更好的开发体验。 - 比如 redux 有专门配套的开发工具可以跟踪状态的变化。
    • 子应用无需了解主应用的状态池实现细节,只需要了解 shared 的函数抽象,实现一套自身的 shared 甚至空 shared 即可,可以更好的规范子应用开发。
    • 子应用无法随意污染主应用的状态池,只能通过主应用暴露的 shared 实例的特定方法操作状态池,从而避免状态池污染产生的问题。
    • 子应用将具备独立运行的能力,Shared 通信使得父子应用有了更好的解耦性。

      缺点也有两个:

    • 主应用需要单独维护一套状态池,会增加维护成本和项目复杂度;
    • 子应用需要单独维护一份 shared 实例,会增加维护成本;

      Shared 通信方式也是有利有弊,更高的维护成本带来的是应用的健壮性和可维护性。

      最后我们来画一张图对 shared 通信的原理和流程进行解析(见下图):

      个人总结一下实践应用的几个步骤:

    (1)主应用:自身 store(vuex)状态管理   ——  创建 shared 实例 对 store 进行部分 api 管理  ——  在使用的时候引入 shared 实例触发对 vuex 状态的 api 管理  ——  将 shared 实例通过 props 传给子应用。

    (2)子应用:自身 shared 实例、自身 sharedModule 管理shared实例  ——  在入口处,主应用有传 shared 实例则使用主应用 shared 实例覆载自身的实例;若未传(即独立运行)则使用自身的 shared 实例   ——   在使用的时候引入 shared 实例触发 api 管理

    (3)这样的话子应用也可以有自己的 store 状态管理,若想保证自己的状态管理和主应用的状态管理一直,则再触发 vuex 更新的时候,调用 shared 实例触发主应用 vuex 更新即可。

      到这里,两种 qiankun 应用间通信方案就分享完啦!两种通信方案都有合适的使用场景,大家可以结合自己的需要选择即可。

    来自于这篇文章的学习及在项目中的实战使用:《基于 qiankun 的微前端最佳实践 - 应用间通信篇 - https://juejin.cn/post/6844904151231496200》

  • 相关阅读:
    java内存分析 栈 堆 常量池的区别
    了解struts2 action的一些原理
    条件语句的写法
    Library中的title与Name
    样式优先级、margin
    文件夹IsShow字段为空
    Ispostback
    HierarchicalDataBoundControl 错误
    DBNull与Null
    sharepoint中的YesNo字段
  • 原文地址:https://www.cnblogs.com/goloving/p/15599561.html
Copyright © 2020-2023  润新知