●●●
微前端這個詞這兩年很頻繁的出現在大家的視野中,主要是把微服務的概念引入到了前端,讓前端的多個模塊或者應用解耦,做到讓前端的子模塊獨立倉儲,獨立運行,獨立部署。
下面是微前端的概念圖。
為什麼需要微前端?
1.項目前端是單體應用即負責貌美如花還要賺錢養家,而後端使用微服務只需要負責分家家然後給前端送朵花花。前端單體承擔了所有的接口。
2.系統模塊增多,前端單體應用變得肥胖,開發效率低下,構建速度變慢。前端心想:哼,老娘就使勁的吃,瀏覽器也別想看見我,卡死你。
3.公司人員擴大,需要多個前端團隊獨立開發,獨立部署,如果都在一個倉儲中開發會帶來一系列問題,例如:老子辛苦一天寫的代碼,第二天讓別人弄沒了。
4.解決遺留系統問題,新模塊需要使用最新的框架和技術,舊系統還繼續使用。
5.單體前端帶來的測試問題。前端小姐姐分模塊測試,這時正遇到構建發布,多人運動戛然而止。
6.編不下去了。。。。。。
微前端技術選型之路
目前關注度和成熟度最高的應該就是 single-spa,但是國內也有很多團隊都有自己的微前端框架,像螞蟻金服的qiankun,phodal 的 mooa,阿里飛冰的icestark,這些都是比較出名的微前端解決方案。但是這些框架都有一定的局限性,像mooa是針對Angular 打造的主從結構的微前端框架,icestark是最近才出的方案,而qiankun官網的開發文檔就僅僅的幾十行。本文將對qiankun進行幾個方面講解。
聊一聊qiankun
沒錯,qiankun正是光天化日,朗朗乾坤的乾坤。qiankun是一個開放式微前端架構,支持當前三大主流前端框架甚至jq等其他項目無縫接入。
qiankun是基於路由配置,適用於 route-based 場景,通過將微應用關聯到一些 url 規則的方式,實現當瀏覽器 url 發生變化時,自動加載相應的微應用的功能。
qiankun在Vue項目中的使用
1、微前端構建
構建主應用
1、使用vue腳手架初始化項目或者原有Vue項目
2、npm install qiankun --save下載qiankun微前端方案依賴
3、改造主項目,很簡單,在src創建一個qiankun文件夾,創建init.js文件,文件內容:
import {
registerMicroApps, // 註冊子應用方法
setDefaultMountApp, // 設默認啟用的子應用
runAfterFirstMounted, // 有個子應用加載完畢回調
start, // 啟動qiankun
addGlobalUncaughtErrorHandler, // 添加全局未捕獲異常處理器
} from "qiankun";
function genActiveRule(routerPrefix) {//路由監聽
return location => location.pathname.startsWith(routerPrefix);
}
export default function startQiankun() {
const apps = [//子應用配置
{
name: 'admin',
entry: "//localhost:9528",
container:'#admin',//將子應用節點掛載到父應用定義id=admin的div上
activeRule: genActiveRule('/admin'),//路由前綴
props: {mag:」我是主應用」} //父應用給子應用傳參
}
]
//註冊子應用
registerMicroApps(apps, {
beforeLoad: [//子應用加載生命周期,下同
app => {
console.log("before load", app);
}
],
beforeMount: [
app => {
console.log("before mount", app);
}
],
afterUnmount: [
app => {
console.log("after unload", app);
}
]
})
// 設置默認子應用
// setDefaultMountApp('/admin');
// 第一個子應用加載完畢回調
runAfterFirstMounted((app) => {
console.log(app)
});
// 啟動微服務
start({
prefetch: 'all'//預加載子應用靜態資源
});
// 設置全局未捕獲一場處理器
addGlobalUncaughtErrorHandler(event => console.log(event));
}
4、在App.vue中加入<div id="admin"></div>即可
<template>
<div :class="classObj">
<div v-if="device === 'mobile' && sidebar.opened" @click="handleClickOutside"></div>
<sidebar v-if="!isFullModel"></sidebar>
<div :style="isFullModel?'margin-left:0px;':''">
<navbar></navbar>
<tags-view v-if="!isFullModel"></tags-view>
<div id="admin" :style="`height:calc(100vh - ${isFullModel ? 54: 84}px);`"></div>//子應用盒子
</div>
</div>
</template>
5、最後將init.js在main.js引入
import startQiankun from './qiankun/init/index'
let app = null
async function render({ appContent, loading } = {}) {
if (!app) {
await ReadConfig(Vue)//加載配置文件
app = new Vue({
router,
store,
i18n,
render: h => h(App),
}).$mount('#app')
} else {
// app.content = appContent;
// app.loading = loading;
console.log(loading, appContent)
}
}
render()
startQiankun()
構建子應用
1、使用vue腳手架初始化項目
2、在main.js中加入:
let router = null;
let instance = null;
if (window.__POWERED_BY_QIANKUN__) {
//處理資源
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
//下面的 /admin 與主應用 registerMicroApps 中的 activeRule 欄位對應
function render(props = {}) {
const {container} = props
router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? '/admin' : '/',
mode: 'history',
routes,
});
const create = async () => {
// await ReadConfig(Vue)
instance = new Vue({
router,
render: h => h(App),
}).$mount(container ? container.querySelector('#app') : '#app');
}
create()
}
if (!window.__POWERED_BY_QIANKUN__) {//判斷是否在qiankun環境下
render();
}
// 導出子應用生命周期 掛載前
export async function bootstrap(props) {
console.log('[vue] vue app bootstraped');
}
export async function mount(props) {// 導出子應用生命周期 掛載前 掛載後
render(props);
}// 導出子應用生命周期 掛載前 卸載後
export async function unmount() {
instance.$destroy();
instance = null;
router = null;
}
3、創建vue.config.js文件,需要做一些打包配置。
const path = require('path');
const packageName = require('./package').name;
function resolve(dir) {
return path.join(__dirname, dir);
}
const dev = process.env.NODE_ENV === 'development'
const port = 9528; // dev port
module.exports = {
publicPath: dev ? `//localhost:${port}` : '/',
outputDir: 'dist',
assetsDir: 'static',
filenameHashing: true,
devServer: {
hot: true,
historyApiFallback: true,
port,
overlay: {
warnings: false,
errors: true,
},
headers: {
'Access-Control-Allow-Origin': '*',
},
},
// 自定義webpack配置
configureWebpack: {
resolve: {
alias: {
'@': resolve('src'),
},
},
output: {// 把子應用打包成 umd 庫格式
library: `${packageName}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${packageName}`,
globalObject: 'this',
},
},
};
2、微前端解決的問題
自此,一個父子前端微應用搭建成功,當然這只是簡單的vue實現,真正複雜的是一個項目中每個模塊使用不同的前端框架。效果如下:
我們可以看到A模塊、B模塊和C模塊三個微應用分別運行在Vue、React和Angular的環境中,而主應用主要提供了NavBar和SideBar的界面。中心是微應用中組件顯示的界面。
我們從圖中可以看出將前端微服務化後解決了什麼問題:
1、當A模塊需要維護時,只需要動A模塊的代碼,維護完成之後對A項目進行構建發布,由於體積很小,構建發布就很快。
2、當A項目構建發布時,其他應用運行正常,避免了之前單體應用構建發布導致整個前端項目無法訪問。
3、主應用可以捕捉到子應用的運行狀態,如果子應用出現異常或者掛掉,可以對用戶進行友好提示。例如當A模塊掛掉後,用戶點擊其功能一將提示「功能正在維護中」。
4、由於子應用運行的環境不同,項目可以加入各種各樣的技術棧,解決了前端人員因為技術棧不同無法聚合的問題。
5、每個子項目擁有獨自的倉儲,代碼易維護,並且可以用到別的項目中,這樣一來,每個微前端又可以作為一個微應用提供服務。
3、微前端開發中需要解決的問題
父子應用通信
父向子通信(Shared 通信方案)
Shared 通信方案的原理就是,主應用基於 Vuex維護一個狀態池,通過 shared 實例暴露一些方法給子應用使用。同時,子應用需要單獨維護一份 shared 實例,在獨立運行時使用自身的 shared 實例,在嵌入主應用時使用主應用的 shared 實例,這樣就可以保證在使用和表現上的一致性。
1、在主應用中創建shared.js,可以將需要給子應用傳的數據通過getter和setter的方式設置。
import store from "../../store";
class Shared {
//獲取 Token
getToken() {
const state = store.state;
return state.user.token || "";
}
setToken(token) {
// 將 token 的值記錄在 store 中
store.commit('SET_TOKEN',token);
}
}
const shared = new Shared();
export default shared;
2、在init.js註冊的子應用中,將shared傳遞過去
const apps = [
{
name: 'admin',
entry: "//localhost:9528",
container:'#admin',
activeRule: genActiveRule('/admin'),
props: {
shared,//將數據以類的方式傳遞
}
}
]
3、當主應用登錄成功之後可以通過調用shared.setToken(token)設置token,然後主應用通過自身路由跳轉到主頁this.$router.push({ path: '/' })。
4、現在,我們來處理子應用需要做的工作。我們希望子應用有獨立運行的能力,所以子應用也應該實現 shared,以便在獨立運行時可以擁有兼容處理能力。代碼實現如下:
class Shared {
//獲取 Token
getToken() {
// 子應用獨立運行時,在 localStorage 中獲取 token
return sessionStorage.getItem("token") || "";
}
setToken(token) {
// 子應用獨立運行時,在 localStorage 中設置 token
sessionStorage.setItem("token", token);
}
}
class SharedModule {//通過SharedModule 來維護shared
static shared = new Shared();
static overloadShared(shared) {
SharedModule.shared = shared;
}
static getShared() {
return SharedModule.shared;
}
}
export default SharedModule;
5、接下來我們只需要在子應用的入口文件接受父應用傳來的shared,然後設置到SharedModule 中。
export async function bootstrap(props) {
const {shared = SharedModule.getShared() } = props;
SharedModule.overloadShared(shared);
}
6、然後子應用就可以在組件中通過SharedModule獲取shared然後得到token,最後發起網絡請求。
mounted () {
const shared = SharedModule.getShared();
const token = shared.getToken();
let resp =await this.dispatch(AppController.findById, {id: this.currentApp.appId})//封裝之後的網絡請求
...
}
子向父通信(emit通信)
emit通信的原理是子應用通過觸發父應用傳遞的函數來改變父應用vuex中維護的狀態,進而達到子應用想父應用的通信。這裡以子應用向父應用發送token失效,讓父應用跳轉至登錄頁的場景。
1、父應用定製提供子應用觸發token註銷的函數
import store from "../../store";
import { removeToken } from '@/utils/auth'
function logout(childThis) {
removeToken()
childThis
.$confirm('登錄信息已過期!', '提示', {
confirmButtonText: '重新登錄',
type: 'warning',
center: true,
showClose: false,
showCancelButton: false,
closeOnClickModal: false,
})
.then(() => {
store.dispatch('FedLogOut')
})
}
export {
logout
}
2、將父應用定製的函數傳遞至子應用
const apps = [
{
name: 'admin',
entry: "//localhost:9528",
container:'#admin',
activeRule: genActiveRule('/admin'),
props: {
shared,
emitFnc: childEmit,//將上述的函數傳遞子應用
}
}
]
3、子應用獲取並註冊到子應用的全局變量中
export async function bootstrap(props) {
const {emitFnc , shared = SharedModule.getShared() } = props;
SharedModule.overloadShared(shared);
Object.keys(emitFnc || {}).forEach(i => {
Vue.prototype[`$${i}`] = emitFnc[i]
});
}
4、子應用通過在組件中調用this.$logout(this)即可完成通信。
公共資源共享
項目中存在大量的公共資源,例如公共方法,公共組件,公共UI。在開發的時候不可能每個項目都複製一遍,這樣既降低了開發效率,同時項目的體積增大,構建速度變慢。所以對於微前端項目,我們需要定製一套合適的方案將公共資源抽離出來,子應用可以在運行期動態獲取到資源並加以使用,就像maven的頂級pom,將公共的jar抽離了出來。
1、這裡以公共組件為例,將定義好的公共組件放置主應用的文件夾中並導出,創建一個js文件專門作為公共組件。
import InputEditor from './input-editor/src' //自定義公共組件
import ClipButton from './clip-button/src' //自定義公共組件
const components = [InputEditor,ClipButton];
const install = function (Vue) {
components.forEach(component => {
Vue.component(component.name, component);
});
};
if (typeof window !== "undefined" && window.Vue) {
install(window.Vue);
}
export default {
install,
};
2、同樣的,將此文件作為參數傳遞至子應用。
const apps = [
{
name: 'admin',
entry: "//localhost:9528",
container:'#admin',
activeRule: genActiveRule('/admin'),
props: {
shared,
emitFnc: childEmit,
components: Components//公共組件
}
}
]
3、子應用接收到參數後,將組件註冊到自身的項目中
export async function bootstrap(props) {
const {emitFnc ,components, shared = SharedModule.getShared() } = props;
SharedModule.overloadShared(shared);
Vue.use(components) //註冊組件
Object.keys(emitFnc || {}).forEach(i => {
Vue.prototype[`$${i}`] = emitFnc[i]
});
}
4、子應用可以在任何組件中使用父應用傳遞的組件。需要注意的是,使用公共組件的名稱要和註冊組件的名稱保持一致,其他公共js類似。
通過父子間通信,我們大致可以了解到項目中如何實現鑑權,因為前端鑑權是一個難點,在微前端中鑑權方案有很多種,有興趣的小夥伴可以嘗試著去實現自身項目中的鑑權方案。
總結
qiankun,意為統一。通過 qiankun 這種技術手段,讓你能很方便的將一個巨石應用改造成一個基於微前端架構的系統,並且不再需要去關注各種過程中的技術細節,做到真正的開箱即用和生產可用。
關於作者:卜壯,普元前端開發工程師,負責Mobile 8.0項目管理平臺前端部分。熟悉ReactNative,目前正在學習Vue,大前端技術探求者。
關於EAWorld:微服務,DevOps,數據治理,移動架構原創技術分享。