基於 vue3.0-beta 及 qiankun2.0 極速嚐鮮!微前端進階實戰項目。
項目地址:wl-mfejavascript
微前端實戰詳細入門教程及解放方案請轉至我另外一篇文章:微前端實戰看這篇就夠了 - Vue項目篇。
項目地址:wl-micro-frontends [wl-qiankun] && 在線訪問html
npm run yinit // 使用yarn下載依賴,推薦
npm run cinit // 使用cnpm下載依賴
npm run init // 或 使用npm下載依賴
npm run serve // 運行所有項目
yarn serve y // yarn運行所有項目
npm run build // 打包所有項目
yarn build y // 打包所有項目
npm run publish // 執行發佈腳本
複製代碼
注意:若是下載報錯,報 bin/sh 找不到start命令,那你多是mac or linux,那就進入目錄一個一個下載運行吧。
另:執行批量服務耗時較久,請耐心等待,init與build成功會在控制檯提示,serve稍加等待或刷新瀏覽器便可。前端
主應用須要用到elementui,暫時使用vue2.0+qiankun2.0版本。vue3.0beta體驗在下面【子應用構建】章節vue
主應用項目主要在5個文件:utils
文件夾,app.vue
,appRegister.js
,main.js
,render.js
java
cnpm i qiankun -S
複製代碼
在主應用下載qiankun,注意使用2.0以上版本linux
<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>
盒子webpack
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版本,則徹底不須要如下代碼:nginx
/** * @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方法。git
下面用了一個方法將qiankun須要用到的方法所有包裝起來,以便後續將註冊子應用放到獲取後端註冊表數據後執行。github
/** * @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>
複製代碼
在根目錄執行npm run publish
會執行發佈腳本,根據提示選擇要發佈到的服務器和要發佈的應用,按指示選擇後回車執行便可。 注意爲保持發佈腳本的精簡,默認你要發佈的應用已經打包出了dist目錄。
根據qiankun
的子應用註冊規則,給每一個子應用分配一個端口,nginx正常配置監聽多個端口便可。 詳細配置見_nginx
目錄下general-port.conf
有些項目應用場景及客戶要求限制,沒法根據子應用的數量無節制的開放端口,所以嘗試將主應用獨立一個端口,子應用共用一個端口的nginx配置。 詳細配置見_nginx
目錄下dual-port.conf
使用雙端口nginx配置須要對前面教程裏的配置作部分改動
registerMicroApps(
[
{
name: 'subapp-ui', // 子應用app name 推薦與子應用的package的name一致
entry: 'http://192.168.1.100:2751/ui', // 子應用的入口地址
container: '#yourContainer', // 掛載子應用內容的dom節點 `# + dom id`【見上面app.vue】
activeRule: '/ui', // 子應用的路由前綴
},
],
)
複製代碼
注意: entry由端口地址變成了端口地址+此子應用的路徑(//localhost:2751/ui)。且注意這個/ui路徑後面要講到
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
複製代碼
module.exports = {
publicPath: 'http://192.168.1.100:2751/ui'
...
}
複製代碼
const render = ({ routerBase } = {}) => {
router = new VueRouter({
base: __qiankun__ ? routerBase : "/",
mode: "history",
routes: []
});
複製代碼
至此便可經過nginx的配置實現一個端口下對全部子應用資源進行匹配轉發。
到這裏已經完成了一個簡單使用的 vue3.0 + qiankun2.0 微前端應用實踐,快來上手試試吧! 項目地址:Github;
需求場景承接雙端口配置,更近一步,有些極端發佈環境只給開放一個端口,或者禁止開放跨域要求主應用和全部子應用作成同域! 詳細配置見_nginx
目錄下single-port.conf
. (單端口思路大體如此,暫未進行測試)
registerMicroApps(
[
{
name: 'subapp-ui', // 子應用app name 推薦與子應用的package的name一致
entry: 'http://192.168.1.100:2750/ui', // 子應用的入口地
container: '#yourContainer', // 掛載子應用內容的dom節點 `# + dom id`【見上面app.vue】
activeRule: '/ui', // 子應用的路由前綴
},
],
)
複製代碼
注意: entry由端口地址變成了主應用端口地址+此子應用的路徑(//localhost:2750/ui)。注意和雙端口差異
基本和雙端口一直,惟一的區別是publicPath變成了主應用端口+子應用路徑
module.exports = {
publicPath: 'http://192.168.1.100:2750/ui'
...
}
複製代碼
至此便可經過nginx的配置實現主子應用同端口。(見:_nginx/single-port.conf)
這些問題若是發生,可經過調整nginx配置等來實現單端口運行主+子應用。由於這是被理論和實踐皆已證實的。
多是你見過最完善的微前端解決方案
微前端的核心價值
目標是最完善的微前端解決方案 - qiankun 2.0
qiankun
若是你有心,能夠請做者喝杯咖啡,或者推薦一份好工做