隨著項目的發展,前端SPA應用的規模不斷加大、業務代碼耦合、編譯慢,導致日常的維護難度日益增加。同時前端技術的發展迅猛,導致功能擴展吃力,重構成本高,穩定性低。因此前端微服務應運而生。 ...
一、前言
隨著項目的發展,前端SPA應用的規模不斷加大、業務代碼耦合、編譯慢,導致日常的維護難度日益增加。同時前端技術的發展迅猛,導致功能擴展吃力,重構成本高,穩定性低。因此前端微服務應運而生。
前端微服務優勢
1.複雜度可控: 業務模塊解耦,避免代碼過大,保持較低的複雜度,便於維護與開發效率。
2.獨立部署: 模塊部署,減少模塊影響範圍,單個模塊發生錯誤,不影響全局,提升項目穩定性。
3.技術選型靈活: 在同一項目下可以使用市面上所有前端技術棧,也包括未來的前端技術棧。
4.擴展性,提升業務動態擴展的可能,避免資源浪費
微前端服務結構
技術對比和選型:
選型 | 靜態資源預載入 | 子應用保活 | iframe | js沙箱 | css沙箱 | 接入成本 | 地址 |
---|---|---|---|---|---|---|---|
EMP | √ | √ | × | × | × | 低 | https://github.com/efoxTeam/emp |
Qiankun | √ | × | × | √ | √ | 中低 | https://qiankun.umijs.org/zh/ |
無界 | √ | √ | √ | √ | √ | 中低 | https://wujie-micro.github.io/doc/ |
micro-app | √ | × | × | √ | √ | 中低 | https://zeroing.jd.com/micro-app/ |
通過對比多種技術對項目的支持情況和項目接入的成本,我們最終選型無界。
二、wujie簡單用法(以主應用使用vue框架為例)
主應用是vue框架可直接使用wujie-vue,react框架可直接使用wujie-react,先安裝對應的插件哦
主應用改造:
// 引入無界,根據框架不同版本不同,引入不同的版本
import { setupApp, bus, preloadApp, startApp } from 'wujie-vue2'
// 設置子應用預設參數
setupApp({
name: '子應用id(唯一值)',
url: "子應用地址",
exec: true,
el: "容器",
sync: true
})
// 預載入
preloadApp({ name: "唯一id"});
// 啟動子應用
startApp({ name: "唯一id"});
子應用改造:
1、跨域
子應用如果支持跨域,則不用修改
原因:存在請求子應用資源跨域
方案:因前端應用基本是前後端分離,使用proxy代理。只需配置在子應用配置允許跨域即可
// 本地配置
server: {
host: '127.0.0.1', // 本地啟動如果主子應用沒處在同一個ip下,也存在跨域的問題,需要配置
headers: {
'Access-Control-Allow-Credentials': true,
'Access-Control-Allow-Origin': '*', // 如資源沒有攜帶 cookie,需設置此屬性
'Access-Control-Allow-Headers': 'X-Requested-With,Content-Type',
'Access-Control-Allow-Methods': '*'
}
}
// nginx 配置
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Origin "*";
add_header Access-Control-Allow-Headers 'X-Requested-With,Content-Type';
add_header Access-Control-Allow-Methods "*";
2、運行模式選擇
無界有三種運行模式:單例模式、保活模式、重建模式
(1)、保活模式(長存頁面)
釋義:類似於vue的keep-alive性質(子應用實例和webcomponent不銷毀,狀態、路由都不丟失,只做熱webcomponent的熱插拔),子應用不想做生命周期改造,子應用切換又不想有白屏時間,可以採用保活模式。主應用上有多個入口跳轉到子應用的不同頁面,不能採用保活模式,因為無法改變子應用路由。
配置:只需要在主應用載入子應用的時候,配置參數添加alive:true
效果:預載入+保活模式=頁面數據請求和渲染提前完成,實現瞬間打開效果
(2)、單例模式
釋義:子應用頁面切走,會調用window.__WUJIE_UNMOUNT銷毀子應用當前實例。子應用頁面如果切換回來,會調用window.__WUJIE_MOUNT渲染子應用新的子應用實例。過程相當於:銷毀當前應用實例 => 同步新路由 => 創建新應用實例
配置:只需要在主應用載入子應用的時候,配置參數添加alive:false
改造生命周期
// window.__POWERED_BY_WUJIE__用來判斷子應用是否在無界的環境中
if (window.__POWERED_BY_WUJIE__) {
let instance;
// 將子應用的實例和路由進線創建和掛載
window.__WUJIE_MOUNT = () => {
const router = new VueRouter({ routes });
instance = new Vue({ router, render: (h) => h(App) }).$mount("#app");
};
// 實例銷毀
window.__WUJIE_UNMOUNT = () => {
instance.$destroy();
};
} else {
// 子應用單獨啟動
new Vue({ router: new VueRouter({ routes }), render: (h) => h(App) }).$mount("#app");
}
(3)、重建模式
釋義:每次頁面切換銷毀子應用webcomponent+js的iframe。
配置:只需要在主應用載入子應用的時候,配置參數添加alive:false
無生命周期改造
備註:非webpack打包的老項目,子應用切換可能出現白屏,應儘可能使用保活模式降低白屏時間
三、載入模塊(主應用配置)
子應用基礎信息管理
// subList.js 數據可在配置頁面動態配置
const subList = [
{
"name":"subVueApp1",
"exec":true,// false只會預載入子應用的資源,true時預執行子應用代碼
"alive": true,
"show":true,// 是否引入
"url":{
"pre":"http://xxx1-pre.com",
"gray":"http://xxx1-gray.com",
"prod":"http://xxx1.com"
}
},
{
"name":"subVueApp2",
"exec":false,// false只會預載入子應用的資源,true時預執行子應用代碼
"alive": false,
"show":true,// 是否引入
"url":{
"pre":"http://xxx2-pre.com",
"gray":"http://xxx2-gray.com",
"prod":"http://xxx2.com"
}
}
]
export default subList;
// hostMap.js
import subList from './subList'
const env = process.env.mode || 'pre'
// 子應用map結構
const subMap = {}
const subArr = []
// 轉換子應用
export const hostMap = () => {
subList.forEach(v => {
const {url, ...other} = v
const info = {
...other,
url: url[env]
}
subMap[v.name] = info
subArr.push(info)
})
return subArr
}
// 獲取子應用配置信息
export const getSubMap = name => {
return subMap[name].show ? subMap[name] : {}
}
子應用註冊預載入和啟動
// setupApp.js
import WujieVue from 'wujie-vue2';
import {hostMap} from './hostMap';
const { setupApp, preloadApp } = WujieVue
const setUpApp = Vue => {
Vue.use(WujieVue)
hostMap().forEach(v => {
setupApp(v)
preloadApp(v.name)
})
}
export default setUpApp;
// main.js
import Vue from 'vue'
import setUpApp from'@/microConfig/setupApp'
setUpApp(Vue)
配置公共函數
全子應用共用的生命周期函數,可用於執行多個子應用間相同的邏輯操作函數共同處理
// lifecycle.js
const lifecycles = {
beforeLoad: (appWindow) => console.log(`${appWindow.__WUJIE.id} beforeLoad 生命周期`),
beforeMount: (appWindow) => console.log(`${appWindow.__WUJIE.id} beforeMount 生命周期`),
afterMount: (appWindow) => console.log(`${appWindow.__WUJIE.id} afterMount 生命周期`),
beforeUnmount: (appWindow) => console.log(`${appWindow.__WUJIE.id} beforeUnmount 生命周期`),
afterUnmount: (appWindow) => console.log(`${appWindow.__WUJIE.id} afterUnmount 生命周期`),
activated: (appWindow) => console.log(`${appWindow.__WUJIE.id} activated 生命周期`),
deactivated: (appWindow) => console.log(`${appWindow.__WUJIE.id} deactivated 生命周期`),
loadError: (url, e) => console.log(`${url} 載入失敗`, e),
};
export default lifecycles;
// subCommon.js
// 跳轉到主應用指定頁面
const toJumpMasterApp = (location, query) => {
this.$router.replace(location, query);
const url = new URL(window.location.href);
url.search = query
// 手動的掛載url查詢參數
window.history.replaceState(null, '', url.href);
}
// 跳轉到子應用的頁面
const toJumpSubApp = (appName, query) => {
this.$router.push({path: appName}, query)
}
export default {
toJumpMasterApp,
toJumpSubApp
}
// setupApp.js
import lifecycles from './lifecycles';
import subCommon from './subCommon';
const setUpApp = Vue => {
....
hostMap().forEach(v => {
setupApp({
...v,
...lifecycles,
props: subCommon
})
preloadApp(v.name)
})
}
主應用載入子應用頁面
// 子應用頁面載入
// app1.vue
<template>
<WujieVue
:key="update"
width="100%"
height="100%"
:name="name"
:url="appUrl"
:sync="subVueApp1Info.sync"
:alive="subVueApp1Info.alive"
:props="{ data: dataProps ,method:{propsMethod}}"
></WujieVue>
</template>
<script>
import wujieVue from "wujie-vue2";
import {getSubMap} from '../../hostMap';
const name = 'subVueApp1'
export default {
data() {
return {
dataProps: [],
subVueApp1Info: getSubMap(name)
}
},
computed: {
appUrl() {
// return getSubMap('subVueApp1').url
return this.subVueApp1Info.url + this.$route.params.path
}
},
watch: {
// 如果子應用是保活模式,可以採用通信的方式告知路由變化
"$route.params.path": {
handler: function () {
wujieVue.bus.$emit("vue-router-change", `/${this.$route.params.path}`);
},
immediate: true,
},
},
methods: {
propsMethod() {}
}
}
</script>
四、子應用配置
無界的插件體系主要是方便用戶在運行時去修改子應用代碼從而避免去改動倉庫代碼
// plugins.js
const plugins = {
'subVueApp1': [{
htmlLoader:code => {
return code;
},
cssAfterLoaders: [
// 在載入html所有樣式之後添加一個外聯樣式
{ src:'https://xxx/xxx.css' },
// 在載入html所有樣式之後添加一個內聯樣式
{ content:'img{height: 300px}' }
],
jsAfterLoaders: [
{ src:'http://xxx/xxx.js' },
// 插入一個內聯腳本本
{ content:`
window.$wujie.bus.$on('routeChange', path => {
console.log(path, window, self, global, location)
})`
},
// 執行一個回調
{
callback(appWindow) {
console.log(appWindow.__WUJIE.id);
}
}
]
}],
'subVueApp2': [{
htmlLoader: code=> {
return code;
}
}]
};
export default plugins;
// setupApp.js
import plugins from './plugins';
const setUpApp = Vue => {
......
hostMap().forEach(v => {
setupApp({
...v,
plugins: plugins[element.name]
})
......
})
}
五、數據傳輸和消息通信
數據交互方式
1,通過props進行傳
2、通過window進線傳達
3,通過事件bus進行傳達
props
主應用通過data傳參給子應用, 子應用通過methods方法傳參給主應用
// 主應用
<WujieVue name="xxx" url="xxx" :props="{ data: xxx, methods: xxx }"></WujieVue>
// 子應用
const props = window.$wujie?.props; // {data: xxx, methods: xxx}
window
利用子應用運行在主應用的iframe
類似iframe的傳參和調用
// 主應用獲取子應用的全局變數數據
window.document.querySelector("iframe[name=子應用id]").contentWindow.xxx;
//子應用獲取主應用的全局變數數據
window.parent.xxx;
eventBus
去中心化的通信方案,方便。類似於組件間的通信
主應用
// 使用 wujie-vue
import WujieVue from"wujie-vue";
const{ bus }= WujieVue;
// 主應用監聽事件
bus.$on("事件名字",function(arg1,arg2, ...){});
// 主應用發送事件
bus.$emit("事件名字", arg1, arg2,...);
// 主應用取消事件監聽
bus.$off("事件名字",function(arg1,arg2, ...){});
子應用
// 子應用監聽事件
window.$wujie?.bus.$on("事件名字",function(arg1,arg2, ...){});
// 子應用發送事件
window.$wujie?.bus.$emit("事件名字", arg1, arg2,...);
// 子應用取消事件監聽
window.$wujie?.bus.$off("事件名字",function(arg1,arg2, ...){});
規範主子應用傳遞規則
規則:子應用名+事件名
主應用向子應用傳參
// 主應用傳參
bus.$emit('matser', options) // 主應用向所有子應用傳參
bus.$emit('vite:getOptions', options) // 主應用向指定子應用傳參
//子應用監聽主應用事件
window?.$wujie?.bus.$on("master", (options) => {
console.log(options)
});
//子應用監聽主應用特定通知子應用事件
window?.$wujie?.bus.$on("vite:getOptions", (options) => {
console.log(options)
});
六、路由
以 vue 主應用為例,子應用 A 的 name 為 A, 主應用 A 頁面的路徑為/pathA,子應用 B 的 name 為 B,主應用 B 頁面的路徑為/pathB為例
主應用統一props傳入跳轉函數
jump (location) {
this.$router.push(location);
}
1、主應用history路由
子應用 B 為非保活應用
1、子應用A 只能跳轉到子應用 B 的主應用的預設路由
function handleJump(){
window.$wujie?.props.jump({ path:"/pathB"});
}
2、子應用A 只能跳轉到子應用B 應用的指定路由(非預設路由)
// 子應用A點擊跳轉處理函數, 子應用B需開啟路由同步
function handleJump(){
window.$wujie?.props.jump({ path:"/pathB", query:{ B:"/test"}});
}
子應用 B 為保活應用
子應用A 只能跳轉到子應用 B 的主應用的路由
可寫入主應用的插件中,主應用插件根據不同的應用,引入不同方法
// 子應用 A 點擊跳轉處理函數
function handleJump() {
window.$wujie?.bus.$emit("routeChange", "/test");
}
// 子應用 B 監聽並跳轉
window.$wujie?.bus.$on("routeChange", (path) => this.$router.push({ path }));
2、主應用hash路由
子應用 B 為非保活應用
1、子應用A 只能跳轉到子應用 B 的主應用的預設路由
同子應用B為非保活應用,子應用A跳轉到子應用 B 的主應用的預設路由
2、子應用A 只能跳轉到子應用B 應用的指定路由(非預設路由)
主應用
jump(location,query){
// 跳轉到主應用B頁面
this.$router.push(location);
const url=new URL(window.location.href);
url.search=query
// 手動的掛載url查詢參數
window.history.replaceState(null,"",url.href);
}
// 子應用 B 開啟路由同步能力
// 子應用A
function handleJump() {
window.$wujie?.props.jump({ path: "/pathB" } , `?B=${window.encodeURIComponent("/test")}`});
}
子應用 B 為保活應用
同子應用B為保活應用,子應用A跳轉到子應用 B 路由
// bus.js
// 在 xxx-sub 路由下子應用將激活路由同步給主應用,主應用跳轉對應路由高亮菜單欄
bus.$on('sub-route-change', (name, path) => {
const mainName = `${name}-sub`;
const mainPath = `/${name}-sub${path}`;
const currentName = router.currentRoute.name;
const currentPath = router.currentRoute.path;
if (mainName === currentName && mainPath !== currentPath) {
router.push({ path: mainPath });
}
});
七、部署
前端單頁面的部署,不管怎麼自動化,工具怎麼變. 都是把打包好的靜態文件,放到伺服器的正確位置下。所以支持項目的獨立部署和混合部署。
作者:京東物流 張燕燕 劉海鼎
內容來源:京東雲開發者社區