基於 vue3.0-beta 及 qiankun2.0 極速嚐鮮!微前端進階實戰項目。
項目地址:wl-mfehtml
微前端實戰詳細入門教程及解放方案請轉至我另外一篇文章:微前端實戰看這篇就夠了 - Vue項目篇。
項目地址:[wl-micro-frontends [wl-qiankun]](https://github.com/hql7/wl-mi... && 在線訪問前端
npm run cinit // 使用cnpm下載依賴,推薦cinit節省下載時間 npm run init // 或 使用npm下載依賴 npm run serve // 運行所有項目 npm run build // 打包所有項目
注意:若是下載報錯,報 bin/sh 找不到start命令,那你多是mac or linux,那就進入目錄一個一個下載運行吧。
另:執行批量服務耗時較久,請耐心等待,init與build成功會在控制檯提示,serve稍加等待或刷新瀏覽器便可。vue
主應用須要用到elementui,暫時使用vue2.0+qiankun2.0版本。vue3.0beta體驗在下面【子應用構建】章節linux
主應用項目主要在5個文件:utils
文件夾,app.vue
,appRegister.js
,main.js
,render.js
webpack
cnpm i qiankun -S
在主應用下載qiankun,注意使用2.0以上版本git
<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>
盒子github
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版本,則徹底不須要如下代碼:web
/** * @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方法。vue-router
下面用了一個方法將qiankun須要用到的方法所有包裝起來,以便後續將註冊子應用放到獲取後端註冊表數據後執行。vuex
/** * @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);
上面註冊子應用時,咱們看到代碼裏有傳給子應用的props
和一個appStore
通訊函數。
props
,看過我上個文章的朋友都知道我將props分爲那幾個模塊,實際上,我真正用到的可能就是主應用請求獲取下來的routes
和routerbase
下發給子應用。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並執行:
/** * @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效果已經沒問題!
默認你已經裝了vuecli3.0以上版本
vue crate subapp-ui cd subapp-ui // 在此以前都是正常建立項目,到這裏執行下面命令會以插件的形式將項目升級至3.0 vue add vue-next
在這裏不單獨贅述vue3.0beta的特性,對此網上有許多文章。咱們在實踐咱們微前端的需求實際應用中取逐漸解開它的神祕面紗!
注意設置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}` } } };
if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }
我在這裏區分微前端環境和單獨運行的加載機制,並引入官方通訊方法
注意: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 };
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;
這裏分別導出了setState
,appStore
兩個方法,appStore
在上面life-cycle.js
生命週期文件中註冊全局通訊使用,那麼setState
咱們又要在哪裏使用呢?咱們繼續往下看
將生命週期函數導出,並提供單獨運行邏輯
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();
這裏在views/index.vue
作實戰演練
要求:
直接上代碼:
<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 2.0
qiankun
若是你有心,能夠請做者喝杯咖啡,或者推薦一份好工做
<div> <img src="http://wlsy.oss-cn-hangzhou.aliyuncs.com/apply.jpg" height="330" width="220" /> <img src="http://wlsy.oss-cn-hangzhou.aliyuncs.com/wx.jpg" height="330" width="220" /></div>