基于 qiankun & Vue 的微前端实践

基于 qiankun & Vue 的微前端实践

前言

基于目前业务需求,需要使用微前端方案进行构建。对比了 Single-SPA 和 qiankun,最终选择 qiankun 进行实现。

2018 年 Single-SPA 诞生了, single-spa 是一个用于前端微服务化的 JavaScript 前端解决方案 (本身没有处理样式隔离,js 执行隔离)实现了路由劫持和应用加载

2019 年 qiankun 基于 Single-SPA, 提供了更加开箱即用的 API (single-spa + sandbox + import-html-entry) 做到了,技术栈无关、并且接入简单(iframe 一样简单)

微前端主应用与子应用如何构建

构建主应用

  1. 使用 vue-cli 创建主应用

  2. npm install qiankun 下载微前端方案依赖

  3. 改造主项目

    • 改造 main.js
    // 在和main.js统计目录新建application.js,然后在main.js中引入application.js
    import { registerMicroApps, start } from "qiankun";
    
    // 注册子应用
    const apps = [
      {
        name: "vueApp",
        entry: "http://localhost:10000/vue/", // 默认会加载这个HTML,解析里面的js,动态的执行 (fetch)
        container: "#vue", // 容器
        activeRule: "/vue", // 激活的路径
        props: { a: 1 }, // 传递参数
      },
    ];
    registerMicroApps(apps); // 注册
    start({ prefetch: false }); // 开启
    
    • 改造 App.vue
    <template>
        <div>
            <router-view></router-view>
            <!-- 子应用盒子 -->
            <div id="vue"></div>
        </div>
    </template>
    

构建子应用

  1. 在主应用同级目录下使用 vue-cli 创建一个子项目

  2. 改造子项目

    • 改造 main.js
    import Vue from "vue";
    import App from "./App.vue";
    import router from "./router";
    import store from "./store";
    
    // eslint-disable-next-line no-unused-vars
    let instance = null;
    
    function render(props = {}) {
      instance = new Vue({
        router,
        store,
        render: (h) => h(App),
      }).$mount("#app"); // 这里是挂在到自己的HTML中,基座会拿到这个挂在后的HTML将其插入进去
    }
    
    // 判断当前环境是否为qiankun环境
    if (window.__POWERED_BY_QIANKUN__) {
      // eslint-disable-next-line no-undef,camelcase
      __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    } else if (!window.__POWERED_BY_QIANKUN__) {
      render();
    }
    // 子应用必须导出 以下生命周期 bootstrap、mount、unmount
    export async function bootstrap(props) {}
    
    export async function mount(props) {
      render(props);
    } // props 父应用传递过来的值
    export async function unmount(props) {
      instance = null;
    }
    
    • 改造 vue.config.js
    module.exports = {
      publicPath: "/vue", // 必须设置路由根路径,便于主应用加载子应用
      configureWebpack: {
        output: {
          library: "singleVue",
          libraryTarget: "umd", // 把子应用打包成 umd 库格式
        },
        devServer: {
          port: 10000,
          headers: {
            // 必须设置跨越
            "Access-Control-Allow-Origin": "*",
          },
        },
      },
    };
    

    经过上述改造,一个简易的微前端环境就草草建成了,是不是很简单,你是不是已经跃跃欲试了?
    但是,一个基础的微前端架子建成后,我们还有一些无法绕过的问题要处理,例如:应用之间的通信、全局状态的保存…

应用路径配置

为主应用添加项目根路径

  1. 改造主应用 vue.config.js 的 publicPath

    // vue.config.js
    module.exports = {
      publicPath: "/base",
    };
    
  2. 改造主应用 application.js 的 app

const apps = [
  {
    name: "vueApp",
    entry: "http://localhost:10000/base/vue/", // 默认会加载这个HTML,解析里面的js,动态的执行 (fetch)
    container: "#vue", // 容器
    activeRule: "/base/vue", // 激活的路径
    props: pagerProps, // 传递参数
  },
];
  1. 改造子应用 vue.config.js 的 publicPath
    module.exports = {
      publicPath: "/base/vue",
    };
    

在主应用的某个路由页面加载微应用

  1. 主应用注册这个路由时给 path 加一个 *注意:如果这个路由有其他子路由,需要另外注册一个路由,任然使用这个组件即可。
    const routes = [
      {
        path: "/work/*",
        name: "work",
        component: () => import("../views/work.vue"),
      },
    ];
    
  2. 微应用的 activeRule 需要包含主应用的这个路由 path
    // 存放要加载进来的系统
    const apps = [
      {
        name: "vueApp",
        entry: "http://localhost:10000/base/work/vue/", // 默认会加载这个HTML,解析里面的js,动态的执行 (fetch)
        container: "#vue", // 容器
        activeRule: "/base/work/vue", // 激活的路径
        props: pagerProps, // 传递参数
      },
    ];
    
  3. 改造子应用 vue.config.js 的 publicPath
    module.exports = {
      publicPath: "/base/work/vue",
    };
    
  4. 在 work.vue 这个组件的 mounted 周期调用 start 函数,注意不要重复调用
    import { start } from "qiankun";
    export default {
      mounted() {
        if (!window.qiankunStarted) {
          window.qiankunStarted = true;
          start();
        }
      },
    };
    

全局状态存储

主应用实现

  1. 首先我们需要在主应用中使用 Vuex 创建 store 用于管理全局状态池

    import Vue from "vue";
    import Vuex from "vuex";
    
    Vue.use(Vuex);
    
    export default new Vuex.Store({
      state: {
        token: "",
      },
      mutations: {
        SET_TOKEN: (state, token) => {
          state.token = token;
        },
      },
      getters: {
        token: (state) => state.user.token,
      },
    });
    
  2. 实现主应用的 shared 实例

    // /src/shared/shared.js
    import store from "@/store";
    
    /**
     * 用于操作全局状态池
     */
    class Shared {
      /**
       * 获取 Token
       * @returns {any}
       */
      getToken() {
        return store.getters.token || "";
      }
    
      /**
       * 设置 Token
       * @param token
       */
      setToken(token) {
        // 将 token 的值记录在 store 中
        store.commit("SET_TOKEN", token);
      }
    }
    const shared = new Shared();
    export default shared;
    
  3. shared 实例通过 props 传递给子应用

    // 在application.js中
    import shared from "@/shared/shared";
    
    const pagerProps = {
      shared, // 将全局状态传递给子应用
    };
    
    // 存放要加载进来的系统
    const apps = [
      {
        name: "vueApp",
        entry: "http://localhost:10000/base/work/vue/", // 默认会加载这个HTML,解析里面的js,动态的执行 (fetch)
        container: "#vue", // 容器
        activeRule: "/base/work/vue", // 激活的路径
        props: pagerProps, // 传递参数
      },
    ];
    

子应用实现

  1. 子应用也应该实现 shared,以便在独立运行时可以拥有兼容处理能力

    // /src/shared/index.js
    class Shared {
      /**
       * 获取 Token
       * @returns {any}
       */
      getToken() {
        return store.getters.token || "";
      }
    
      /**
       * 设置 Token
       * @param token
       */
      setToken(token) {
        // 将 token 的值记录在 store 中
        store.commit("SET_TOKEN", token);
      }
    }
    
    class SharedModule {
      static shared = new Shared();
    
      /**
       * 重载 shared
       */
      static overloadShared(shared) {
        SharedModule.shared = shared;
      }
    
      /**
       * 获取 shared 实例
       */
      static getShared() {
        return SharedModule.shared;
      }
    }
    
    export default SharedModule;
    
  2. 实现了子应用的 shared 后,我们需要在入口文件处注入 shared

    import Vue from "vue";
    import App from "./App.vue";
    import router from "./router";
    import store from "./store";
    import SharedModule from "@/shared";
    
    // Vue.config.productionTip = false
    
    // eslint-disable-next-line no-unused-vars
    let instance = null;
    
    function render(props = {}) {
      // 注入 shared 并将其挂载到Vue实例上
      if (window.__POWERED_BY_QIANKUN__) {
        const { shared = SharedModule.getShared() } = props;
        SharedModule.overloadShared(shared);
        Vue.prototype.$commStore = SharedModule.getShared();
      }
      instance = new Vue({
        router,
        store,
        render: (h) => h(App),
      }).$mount("#app"); // 这里是挂在到自己的HTML中,基座会拿到这个挂在后的HTML将其插入进去
    }
    
    if (window.__POWERED_BY_QIANKUN__) {
      // eslint-disable-next-line no-undef,camelcase
      __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    }
    if (!window.__POWERED_BY_QIANKUN__) {
      render();
    }
    
    export async function bootstrap() {}
    
    export async function mount(props) {
      render(props);
    } // props 父应用传递过来的值
    export async function unmount(props) {
      instance = null;
    }
    

主、子应用间动态通信

主应用实现

::: tip 说明
qiankun 对于 props 的应用类似于 react 框架的父子组件通信,传入 data 数据供自组件使用,传入 fn 函数给子组件触发向上回调。
但是这种传参方式只是在注册子应用时传递了一次数据,而数据改变后又不能再注册一遍子应用。这个时候就需要应用间动态通信了

因此在这里我们使用 rxjs 来作为应用间通信的方案
:::

  1. 先在主应用下载并引入 rxjs;并创建我们的 呼机

    import { Subject } from "rxjs"; // 按需引入减少依赖包大小
    const pager = new Subject();
    export default pager;
    
  2. 在主应用的 application.js 引入并注册呼机,以及将呼机下发给子应用

    import { registerMicroApps } from "qiankun";
    import pager from "@/shared/pager";
    import shared from "@/shared/shared";
    
    /**
     * 用于接收子应用发送过来的消息
     */
    pager.subscribe((v) => {
      // 在主应用注册呼机监听器,这里可以监听到其他应用的广播
      console.log(`监听到子应用${v.from}发来消息:`, v);
      // 这里处理主应用监听到改变后的逻辑
    });
    
    const pagerProps = {
      shared, // 将全局状态传递给子应用
      pager, // 从主应用下发应用间通信呼机
    };
    
    // 存放要加载进来的系统
    const apps = [
      {
        name: "vueApp",
        entry: "http://localhost:10000/base/work/vue/", // 默认会加载这个HTML,解析里面的js,动态的执行 (fetch)
        container: "#vue", // 容器
        activeRule: "/base/work/vue", // 激活的路径
        props: pagerProps, // 传递参数
      },
    ];
    registerMicroApps(apps); // 注册
    

子应用实现

  1. 在子应用中注册呼机
    export async function bootstrap({ emitFnc, pager }) {
      pager.subscribe((v) => {
        // 在子应用注册呼机监听器,这里可以监听到其他应用的广播
        console.log(`监听到子应用${v.from}发来消息:`, v);
      });
      Vue.prototype.$pager = pager; // 将呼机挂载在vue实例
    }
    
  2. 在各应用中使用呼机动态传递信息
     methods: {  // 在某个应用里调用.next方法更新数据,并传播给其他应用
       callParentChange() {
         this.$pager.next({
           from: "Vue", // 应用名称
           token: "但若不见你,阳光也无趣"
         });
       }
     }
    

主应用下发 emit 函数收集子应用反馈

主应用实现

  1. 创建要下发的 emit 函数

    // /shared/childEmit
    function changeDataMsg(val) {
      // 要实现的业务
      console.log(val);
    }
    
    export { changeDataMsg };
    
  2. 下发 emit 函数

    // application.js
    import { registerMicroApps } from "qiankun";
    import pager from "@/shared/pager";
    import * as childEmit from "@/shared/childEmit"; // 导入主应用需要下发的emit函数
    import shared from "@/shared/shared";
    
    const pagerProps = {
      shared, // 将全局状态传递给子应用
      emitFnc: childEmit, // 从主应用下发emit函数来收集子应用反馈
      pager, // 从主应用下发应用间通信呼机
    };
    
    // 存放要加载进来的系统
    const apps = [
      {
        name: "vueApp",
        entry: "http://localhost:10000/base/work/vue/", // 默认会加载这个HTML,解析里面的js,动态的执行 (fetch)
        container: "#vue", // 容器
        activeRule: "/base/work/vue", // 激活的路径
        props: pagerProps, // 传递参数
      },
    ];
    registerMicroApps(apps); // 注册
    

子应用实现

处理完主应用的下发逻辑,下面再改造一下子应用的接收逻辑 在子应用 main.js 中写下这段代码:因为 bootstrap 在子应用的生命周期只会调用一次,因此我们把注册组件和挂载函数放在这里

export async function bootstrap({ emitFnc, pager }) {
  // 把mainEmit函数一一挂载
  Object.keys(emitFnc).forEach((i) => {
    Vue.prototype[i] = emitFnc[i];
  });
  Vue.prototype.$pager = pager; // 将呼机挂载在vue实例
}
相关推荐
©️2020 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页