vue3.0&qiankun2.0極速嚐鮮,微前端進階實戰!

wl-mfe

基於 vue3.0-beta 及 qiankun2.0 極速嚐鮮!微前端進階實戰項目。
項目地址:wl-mfe

微前端實戰詳細入門教程及解放方案請轉至我另一篇文章:微前端實戰看這篇就夠了 - Vue項目篇
項目地址:wl-micro-frontends [wl-qiankun] && 在線訪問

最終效果

wl-mfe

項目啓動

npm run cinit    // 使用cnpm下載依賴,推薦cinit節省下載時間
npm run init     // 或 使用npm下載依賴

npm run serve    // 運行全部項目

npm run build     // 打包全部項目

注意:如果下載報錯,報 bin/sh 找不到start命令,那你可能是mac or linux,那就進入目錄一個一個下載運行吧。
另:執行批量服務耗時較久,請耐心等待,init與build成功會在控制檯提示,serve稍加等待或刷新瀏覽器即可。

實戰詳解todo

  • 主應用基座構建
  • 子應用構建
  • 微應用間通信
  • 跨應用通信與vuex結合

主應用基座構建

主應用需要用到elementui,暫時使用vue2.0+qiankun2.0版本。vue3.0beta體驗在下面【子應用構建】章節

主應用項目主要在5個文件:utils文件夾,app.vueappRegister.jsmain.jsrender.js

前提條件

cnpm i qiankun -S

在主應用下載qiankun,注意使用2.0以上版本

改造主應用app.vue

<template>
  <div class="main-container-view">
    <el-scrollbar class="wl-scroll">
      <!-- qiankun2.0  container 模式-->
      <div id="subapp-viewport" class="app-view-box"></div>
      <!-- qiankun1.0  render 模式-->
      <div v-html="appContent" class="app-view-box"></div>
      <div v-if="loading" class="subapp-loading"></div>
    </el-scrollbar>
  </div>
</template>

<script>
  export default {
    name: "rootView",
    props: {
      loading: Boolean,
      appContent: String
    }
  };
</script>

注意這裏,qiankun2.0是根據 container字段對應的dom id來註冊子應用盒子的,因此只用qiankun2.0的話不需要考慮render注測子應用盒子的情況,下面那兩個dom和script裏的props都可以不要!只留一個<div id="subapp-viewport"></div>即可!
另外:註冊子應用時每個子應用都可以指定一個不同的container,因此如果想做每個子應用的keep-alive,則可能需要每個子應用對應一個<div id="subapp-viewport-ui"></div><div id="subapp-viewport-blog"></div>盒子

將實例化vue方法提取至render.js

import Vue from "vue"
import router from './router'
import store from './store'
import App from './App.vue'

/**
 * @name 提取vue示例化方法
 */
export function vueRender() {
  Vue.config.productionTip = false
  new Vue({
    router,
    store,
    render: h => h(App)
  }).$mount("#main-container");  
}

爲什麼要僅僅將這段代碼從main.js摘出呢?一方面是儘量清潔main.js;另一方面,就是爲了兼容qiankun1.0的render方法。
因爲qiankun1.0需要在註冊vue實例時顯式的將appContent傳入app.vue,如果你不用qiankun1.0版本,則完全不需要以下代碼:

/**
 * @description 實例化vue,並提供子應用 render函數模式的裝載能力
 * @description 如果使用qiankun2.0 版本,只需正常實例化vue即可 不需要存在此render函數
 * @param {Object} param0 
 * @description {String} appContent 子應用內容
 * @description {Boolean} loading 是否顯示加載動畫(需手動實現loading效果)
 * @param {Boolean} notCompatible true則不兼容qiankun1.0 【此參數爲示例添加,實際應用自酌】
 */
export function vueRender({ appContent, loading }, notCompatible) {
  Vue.config.productionTip = false

  // 實際上本實例只用到此if內的代碼
  // 本文件其他代碼只爲做兼容qiankun1.0 render掛載子應用的參考
  if (notCompatible) {
    new Vue({
      router,
      store,
      render: h => h(App)
    }).$mount("#main-container");
    return;
  }

  return new Vue({
    router,
    store,
    data() {
      return {
        appContent,
        loading,
      };
    },
    render(h) {
      return h(App, {
        props: {
          appContent: this.content,
          loading: this.loading
        }
      });
    }
  }).$mount('#main-container');
}

let app = null;

/**
 * @name 提供render裝載子應用方法
 * @param {Object} param0 
 * @description {String} appContent 子應用內容
 * @description {Boolean} loading 是否顯示加載動畫(需手動實現loading效果)
 */
export default function render({ appContent, loading }) {
  if (!app) {
    app = vueRender({ appContent, loading });
  } else {
    app.appContent = appContent;
    app.loading = loading;
  }
}

此處是給兼容qiankun1.0 registerMicroApps方法render字段一種方案,事實上升級到2.0完全無壓力,因此建議不需要留下臃腫的render方法。

將註冊子應用的邏輯抽離到appRegister.js

下面用了一個方法將qiankun需要用到的方法全部包裝起來,以便後續將註冊子應用放到獲取後端註冊表數據後執行。

/**
 * @name 啓用qiankun微前端應用
 * @param {*} list 
 * @param {*} defaultApp 
 */
const useQianKun = (list, defaultApp) => {
  /**
  * @name 註冊子應用
  * @param {Array} list subApps
  */
  registerMicroApps(
    [
       {
        name: 'subapp-ui', // 子應用app name 推薦與子應用的package的name一致
        entry: '//localhost:6751', // 子應用的入口地址,就是你子應用運行起來的地址
        container: '#yourContainer', // 掛載子應用內容的dom節點 `# + dom id`【見上面app.vue】
        activeRule: '/ui', // 子應用的路由前綴
      },
    ],
    {
      beforeLoad: [
        app => {
          console.log('[LifeCycle] before load %c%s', 'color: green;', app.name);
        },
      ],
      beforeMount: [
        app => {
          console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name);
        },
      ],
      afterUnmount: [
        app => {
          console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name);
        },
      ],
    },
  )

  /**
   * @name 設置默認進入的子應用
   * @param {String} 需要進入的子應用路由前綴
   */
  setDefaultMountApp('ui');
  /**
   * @name 啓動微前端
   */
  start();
  /**
   * @name 微前端啓動進入第一個子應用後回調函數
   */
  runAfterFirstMounted(() => {
    console.log('[MainApp] first app mounted');
  });
}

結合請求後端註冊表,並給子應用分發路由及數據改造後的完整代碼:

import { registerMicroApps, runAfterFirstMounted, setDefaultMountApp, start, initGlobalState } from "qiankun";
import store from "./store";
/**
 * @name 導入render函數兼容qiakun1.0裝載子應用方法,如果使用2.0container裝載則不需要此方法,此處留着註釋代碼提供兼容qiankun1.0的示例
 * @description 此處留下注釋代碼僅爲提供兼容qiankun1.0示例
 */
// import render from './render';
/**
 * @name 導入接口獲取子應用註冊表
 */
import { getAppConfigsApi } from "./api/app-configs"
/**
 * @name 導入消息組件
 */
import { wlMessage } from './plugins/element';
/**
 * @name 導入想傳遞給子應用的方法,其他類型的數據皆可按此方式傳遞
 * @description emit建議主要爲提供子應用調用主應用方法的途徑
 */
import emits from "./utils/emit"
/**
 * @name 導入qiankun應用間通信機制appStore
 */
import appStore from './utils/app-store'
/**
 * @name 聲明子應用掛載dom,如果不需要做keep-alive,則只需要一個dom即可;
 */
const appContainer = "#subapp-viewport";
/**
 * @name 聲明要傳遞給子應用的信息
 * @param data 主應要傳遞給子應用的數據類信息
 * @param emits 主應要傳遞給子應用的方法類信息
 * @param utils 主應要傳遞給子應用的工具類信息(只是一種方案)
 * @param components 主應要傳遞給子應用的組件類信息(只是一種方案)
 */
let props = {
  data: store.getters,
  emits
}

/**
 * @name 請求獲取子應用註冊表並註冊啓動微前端
 */
getAppConfigsApi().then(({ data }) => {
  // 驗證請求錯誤
  if (data.code !== 200) {
    wlMessage({
      type: 'error',
      message: "請求錯誤"
    })
    return;
  }
  // 驗證數據有效性
  let _res = data.data || [];
  if (_res.length === 0) {
    wlMessage({
      type: 'error',
      message: "沒有可以註冊的子應用數據"
    })
    return;
  }
  // 處理菜單並存入主應用Store
  store.dispatch('menu/setMenu', _res);
  // 處理子應用註冊表數據。詳細數據見 master mock
  let apps = []; // 子應用數組盒子
  let defaultApp = null; // 默認註冊應用
  let isDev = process.env.NODE_ENV === 'development'; // 根據開發環境|線上環境加載不同entry
  _res.forEach(i => {
    apps.push({
      name: i.module, // 子應用名
      entry: isDev ? i.devEntry : i.depEntry, // 根據環境註冊生產環境or開發環境地址
      container: appContainer,  // 綁定dom
      activeRule: i.routerBase, // 綁定子應用路由前綴
      props: { ...props, routes: i.children, routerBase: i.routerBase } // 將props及子應用路由,路由前綴由主應用下發
    })
    if (i.defaultRegister) defaultApp = i.routerBase; // 記錄默認啓動子應用
  });
  // 啓用qiankun微前端應用
  useQianKun(apps, defaultApp);
})

/**
 * @name 啓用qiankun微前端應用
 * @param {*} list 
 * @param {*} defaultApp 
 */
const useQianKun = (list, defaultApp) => {
  /**
  * @name 註冊子應用
  * @param {Array} list subApps
  */
  registerMicroApps(
    list,
    {
      beforeLoad: [
        app => {
          console.log('[LifeCycle] before load %c%s', 'color: green;', app.name);
        },
      ],
      beforeMount: [
        app => {
          console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name);
        },
      ],
      afterUnmount: [
        app => {
          console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name);
        },
      ],
    },
  )

  /**
   * @name 設置默認進入的子應用
   * @param {String} 需要進入的子應用路由前綴
   */
  setDefaultMountApp(defaultApp);

  /**
   * @name 啓動微前端
   */
  start();

  /**
   * @name 微前端啓動進入第一個子應用後回調函數
   */
  runAfterFirstMounted(() => {
    console.log('[MainApp] first app mounted');
  });
}

/**
 * @name 啓動qiankun應用間通信機制
 */
appStore(initGlobalState);

註冊應用間通信機制 utils文件夾

上面註冊子應用時,我們看到代碼裏有傳給子應用的props和一個appStore通信函數。

  1. 關於props,看過我上個文章的朋友都知道我將props分爲那幾個模塊,實際上,我真正用到的可能就是主應用請求獲取下來的routesrouterbase下發給子應用。
  2. 關於appStore方法,我是將官方通信機制提取至utils文件夾下的app-store.js文件,並和vuex相結合。代碼如下:
import store from "@/store";

/**
 * @name 啓動qiankun應用間通信機制
 * @param {Function} initGlobalState 官方通信函數
 * @description 注意:主應用是從qiankun中導出的initGlobalState方法,
 * @description 注意:子應用是附加在props上的onGlobalStateChange, setGlobalState方法(只用主應用註冊了通信纔會有)
 */
const appStore = (initGlobalState) => {
  /**
   * @name 初始化數據內容
   */
  const { onGlobalStateChange, setGlobalState } = initGlobalState({
    msg: '來自master初始化的消息',
  });

  /**
   * @name 監聽數據變動
   * @param {Function} 監聽到數據發生改變後的回調函數
   * @des 將監聽到的數據存入vuex
   */
  onGlobalStateChange((value, prev) => { 
    console.log('[onGlobalStateChange - master]:', value, prev);
    store.dispatch('appstore/setMsg', value.msg)
  });

  /**
   * @name 改變數據並向所有應用廣播
   */
  setGlobalState({
    ignore: 'master',
    msg: '來自master動態設定的消息',
  });
}

export default appStore;

【注意:如未在主應用註冊通信,則在子應用也獲取不到通信方法】

改造main.js

終於我們來到了最後一步,主應用一切改造完成之後,我們將其引入到main.js並執行:

/**
 * @name 統一註冊外部插件、樣式、服務等
 */
import './install'

/**
 * @name 微前端基座主應用vue實例化
 * @description 爲了兼容 qiankun1.0 的render函數裝載子應用能力
 * @description 2.0版本正常實例化vue即可,不需要此render函數
 * @description qiankun registerMicroApps方法 render用到,如果使用container裝載子應用,無需此render函數
 * @deprecated 本示例只針對 qiankun2.0 因此只留下註釋後的代碼在此提醒各位讀者如何兼容qiankun1.0
 */
/* import render from './render';
render({ loading: true }) */
import { vueRender } from './render'
vueRender({}, true)

/**
 * @name 註冊微應用並啓動微前端
 */
import './appRegister'

子應用構建

子應用使用vue3.0beta嚐鮮,大部分時間都用在找3.0的api上,還有許多未解決的問題,比如往vue實例上掛載方法,手動註銷vue是啥api,怎麼註冊插件比如elementUI等,後續會慢慢補充。
這裏使用vue3.0beta實現demo效果已經沒問題!

vuecli初始化項目並升級至vue3.0beta

默認你已經裝了vuecli3.0以上版本

vue crate subapp-ui

cd subapp-ui

// 在此之前都是正常創建項目,到這裏執行下面命令會以插件的形式將項目升級至3.0
vue add vue-next 

在這裏不單獨贅述vue3.0beta的特性,對此網上有許多文章。我們在實踐我們微前端的需求實際應用中取逐漸解開它的神祕面紗!

改造子應用vue.config.js文件

注意設置publicPath、端口號與註冊子應用時一致
注意開發時開啓headers跨域頭信息
注意output按照規定格式打包

const { name } = require("./package");
const port = 6751; // dev port
const dev = process.env.NODE_ENV === "development";

module.exports = {
  publicPath: dev ? `//localhost:${port}` : "/",
  filenameHashing: true,
  devServer: {
    hot: true,
    disableHostCheck: true,
    port,
    overlay: {
      warnings: false,
      errors: true
    },
    headers: {
      "Access-Control-Allow-Origin": "*"
    }
  },
  // 自定義webpack配置
  configureWebpack: {
    output: {
      // 把子應用打包成 umd 庫格式
      library: `${name}-[name]`,
      libraryTarget: "umd",
      jsonpFunction: `webpackJsonp_${name}`
    }
  }
};

在main.js同級添加 public-path.js

if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

在main.js同級添加 life-cycle.js 統一設置子應用生命週期邏輯

我在這裏區分微前端環境和單獨運行的加載機制,並引入官方通信方法

注意:3.0beta的實例化方法爲 createApp,並且註冊路由是通過連續use的方法,詳見下放代碼:
注意:3.0的router實例化方法爲 createRouter, 注意history模式通過createWebHistory方法實現,並且此方法接受一個參數表示路由前綴
注意:3.0的vuex倒是變化不大,但暫未弄明白3.0的mapGetters,mapActions的使用方法

import { createApp } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import App from "./App.vue";
import store from "./store";
import selfRoutes from "./router/routes";

/**
 * @name 導入自定義路由匹配方法
 */
import routeMatch from "./router/routes-match";
/**
 * @name 導入官方通信方法
 */
import appStore from "./utils/app-store";

const __qiankun__ = window.__POWERED_BY_QIANKUN__;
let router = null;
let instance = null;

/**
 * @name 導出生命週期函數
 */
const lifeCycle = () => {
  return {
    /**
     * @name 微應用初始化
     * @param {Object} props 主應用下發的props
     * @description  bootstrap 只會在微應用初始化的時候調用一次,下次微應用重新進入時會直接調用 mount 鉤子,不會再重複觸發
     * @description 通常我們可以在這裏做一些全局變量的初始化,比如不會在 unmount 階段被銷燬的應用級別的緩存等
     */
    async bootstrap(props) {
      console.log('props:', props)
      /* props.emits.forEach(i => {
        Vue.prototype[`$${i.name}`] = i;
      }); */
    },
    /**
     * @name 實例化微應用
     * @param {Object} props 主應用下發的props
     * @description 應用每次進入都會調用 mount 方法,通常我們在這裏觸發應用的渲染方法
     */
    async mount(props) {
      // 註冊應用間通信
      appStore(props);
      // 註冊微應用實例化函數
      render(props);
    },
    /**
     * @name 微應用卸載/切出
     */
    async unmount() {
      instance.$destroy?.();
      instance = null;
      router = null;
    },
    /**
     * @name 手動加載微應用觸發的生命週期
     * @param {Object} props 主應用下發的props
     * @description 可選生命週期鉤子,僅使用 loadMicroApp 方式手動加載微應用時生效
     */
    async update(props) {
      console.log("update props", props);
    }
  };
};

/**
 * @name 子應用實例化函數
 * @param {Object} props param0 qiankun將用戶添加信息和自帶信息整合,通過props傳給子應用
 * @description {Array} routes 主應用請求獲取註冊表後,從服務端拿到路由數據
 * @description {String} 子應用路由前綴 主應用請求獲取註冊表後,從服務端拿到路由數據
 */
const render = ({ routes, routerBase, container } = {}) => {
  router = createRouter({
    history: createWebHistory(__qiankun__ ? routerBase : "/"),
    routes: __qiankun__ ? routeMatch(routes, routerBase) : selfRoutes
  });
  instance = createApp(App).use(router).use(store).mount(container ? container.querySelector("#app") : "#app");
};

export { lifeCycle, render };

在 utils/app-store.js 編寫應用間的通信邏輯處理

import store from "@/store";
import { DataType } from "wl-core"

/**
 * @name 聲明一個常量準備將props內的部分內容儲存起來
 */
const STORE = {};

/**
 * @name 啓動qiankun應用間通信機制
 * @param {Object} props 官方通信函數
 * @description 注意:主應用是從qiankun中導出的initGlobalState方法,
 * @description 注意:子應用是附加在props上的onGlobalStateChange, setGlobalState方法(只用主應用註冊了通信纔會有)
 */
const appStore = props => {
  /**
   * @name 監聽應用間通信,並存入store
   */
  props?.onGlobalStateChange?.(
    (value, prev) => {
      console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev)
      store.dispatch('appstore/setMsg', value.msg)
    },
    true
  );
  /**
   * @name 改變並全局廣播新消息
   */
  props?.setGlobalState?.({
    ignore: props.name,
    msg: `來自${props.name}動態設定的消息`,
  });

  /**
   * @name 將你需要的數據存起來,供下面setState方法使用
   */
  STORE.setGlobalState = props?.setGlobalState;
  STORE.name = props.name;
};

/**
 * @name 全局setState方法,修改的內容將通知所有微應用
 * @param {Object} data 按照你設定的內容格式數據 
 */
const setState = (data) => {
  if (!DataType.isObject(data)) {
    throw Error('data必須是對象格式');
  }
  STORE.setGlobalState?.({
    ignore: STORE.name,
    ...data
  })
}

export {
  setState
}
export default appStore;

這裏分別導出了setStateappStore兩個方法,appStore在上面life-cycle.js生命週期文件中註冊全局通信使用,那麼setState我們又要在哪裏使用呢?我們繼續往下看

改造子應用的main.js

將生命週期函數導出,並提供單獨運行邏輯

import "./public-path";
import { lifeCycle, render } from "./life-cycle";

/**
 * @name 導出微應用生命週期
 */
const { bootstrap, mount, unmount } = lifeCycle();
export { bootstrap, mount, unmount };

/**
 * @name 單獨環境直接實例化vue
 */
const __qiankun__ = window.__POWERED_BY_QIANKUN__;
__qiankun__ || render();

在子應用的某個.vue文件中實踐一下吧

這裏在views/index.vue做實戰演練
要求:

  1. 能使用 Vue 3.0 beta 基本特性體驗
  2. 接收全局消息,並能向其他微應用發佈消息
    直接上代碼:
<template>
  <div class="home">
    <div class="msg-box">
      <div class="msg-title">這裏是子應用:</div>
      <div class="msg-context">{{selfMsg}}</div>
    </div>
    <div class="msg-box">
      <div class="msg-title">來自其他微應用的消息:</div>
      <div class="msg-context">{{vuexMsg}}</div>
    </div>
    <div class="msg-box">
      <div class="msg-ipt-box">
        <input class="msg-ipt" type="text" v-model="formMsg" placeholder="請輸入你想廣播的話" />
      </div>
      <div class="msg-btn-box">
        <button class="msg-btn" @click="handleVuexMsgChange">發送廣播</button>
      </div>
    </div>
  </div>
</template>

<script>
import { ref, computed, getCurrentInstance } from "vue";
import { setState } from "@/utils/app-store";

export default {
  name: "Home",
  setup() {
    /**
     * @name 通過getCurrentInstance方法得到當前上下文
     */
    const { ctx } = getCurrentInstance();
    /**
     * @name 定義一個初始數據
     */
    const selfMsg = ref("subapp-ui");
    /**
     * @name 定義一個計算屬性,返回vuex中的數據
     */
    const vuexMsg = computed(() => ctx.$store.getters.msg);
    /**
     * @name 定義一個表單元素v-model綁定的變量
     */
    const formMsg = ref("");

    /**
     * @name 定義一個廣播事件
     */
    const handleVuexMsgChange = () => {
      /**
       * @name 注意:在setup內部使用定義的變量,需要用**.value取值!
       */
      setState({
        msg: formMsg.value
      });
    };

    // 注意變量和事件都要return出來
    return {
      selfMsg,
      vuexMsg,
      formMsg,
      handleVuexMsgChange
    };
  }
};
</script>

到這裏已經完成了一個簡單使用的 vue3.0 + qiankun2.0 微前端應用實踐,快來上手試試吧!
項目地址:Github;

友情鏈接

微前端 & qiankun

可能是你見過最完善的微前端解決方案
微前端的核心價值
目標是最完善的微前端解決方案 - qiankun 2.0
qiankun

vue3.0beta

Vue 3.0 全家桶搶先體驗

打賞咖啡

如果你有心,可以請作者喝杯咖啡,或者推薦一份好工作

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章