前端微服務無界實踐

来源:https://www.cnblogs.com/jingdongkeji/archive/2023/05/25/17431780.html
-Advertisement-
Play Games

隨著項目的發展,前端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 });
      }
  });

七、部署

前端單頁面的部署,不管怎麼自動化,工具怎麼變. 都是把打包好的靜態文件,放到伺服器的正確位置下。所以支持項目的獨立部署和混合部署。

作者:京東物流 張燕燕 劉海鼎

內容來源:京東雲開發者社區


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 《花木百科花木大全[圖]ACCESS資料庫》資料庫是採集全X花木網的圖文數據,資料很詳細,欄位包含種名、學名、別名、花期、生態性狀、觀賞性分類、科、屬、分佈地區、形態特征、生長習性、主要病蟲害、園林用途、主要功能、園林品種推薦、其他等。 因為網站源花木的圖片有限,所以有圖片的花木只有1千多條;並且需 ...
  • 其實互聯網上關於謎語和燈謎的資料仍然是挺多的,但是要想數據量以萬來計算並且是接近10萬的量來看的話,就只能是《近8萬條謎語燈謎大全ACCESS資料庫》了。而且《近8萬條謎語燈謎大全ACCESS資料庫》的數據表欄位中也包含分類欄位,可以根據分類欄位有針對性的給出謎語。 分類情況包含:字謎、成語、人名、 ...
  • 雖然已經有《7千多兒童故事網ACCESS\EXCEL資料庫》這種記錄數的童話故事類數據,但是遇到了好採集的就總想採集下來,後續有時間或有需求可以再做合併等操作。 分類情況統計為: 兒童故事:兒童小故事(1895)、睡前故事(1229)、益智故事(233)、哲理故事(177)。民間故事:世界上下五千年 ...
  • 原本我以為《3萬5千英語句子英語例句大全ACCESS資料庫》例句已經夠多了,沒想到今天遇到一個10萬條英語單詞例句的數據,非常適合與單詞詞典進行關聯學習,例句多了單詞的用法以及句子的掌握都更有效率,例句多了單詞的用法以及句子的掌握都更有效率,例句多了單詞的用法以及句子的掌握都更有效率,例句多了單詞的 ...
  • CSS中,*的作用是通配表示“全部”。遺憾的是,並沒有一種通配元素名的方法。 例如,我有好幾個東西class都標記為了my-element-序號,就像這樣: ```html ... ``` 我現在希望讓所有這些class的東西都應用同一個css規則。可惜,css並不支持這麼一種寫法: ```css ...
  • 近日在編寫一個小程式,將日記功能移植到小程式中,雖然在手機端寫日記一般用不到Markdown,但是仍想在小程式中查看在電腦端寫的Markdown格式的內容,如代碼塊等。 經過查詢,找到一個被廣泛使用的解決方案是towxml 現記錄如下: > 首先將代碼克隆下來 ```js git clone htt ...
  • > 效果預覽 ![調節頁面.gif](https://wansherry.com/api/fc01e2c58219126e20367e856ebad24c.gif) > 關鍵代碼 ```javascript //調節視窗大小 useEffect(() => { if (conref.current) ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 在業務中,有這麼一種場景,表格下的某一列 ID 值,文本超長了,正常而言會是這樣: 通常,這種情況都需要超長省略溢出打點,那麼,就會變成這樣: 但是,這種展示有個缺點,3 個 ID 看上去就完全一致了,因此,PM 希望能夠實現頭部省略打點 ...
一周排行
    -Advertisement-
    Play Games
  • Timer是什麼 Timer 是一種用於創建定期粒度行為的機制。 與標準的 .NET System.Threading.Timer 類相似,Orleans 的 Timer 允許在一段時間後執行特定的操作,或者在特定的時間間隔內重覆執行操作。 它在分散式系統中具有重要作用,特別是在處理需要周期性執行的 ...
  • 前言 相信很多做WPF開發的小伙伴都遇到過表格類的需求,雖然現有的Grid控制項也能實現,但是使用起來的體驗感並不好,比如要實現一個Excel中的表格效果,估計你能想到的第一個方法就是套Border控制項,用這種方法你需要控制每個Border的邊框,並且在一堆Bordr中找到Grid.Row,Grid. ...
  • .NET C#程式啟動閃退,目錄導致的問題 這是第2次踩這個坑了,很小的編程細節,容易忽略,所以寫個博客,分享給大家。 1.第一次坑:是windows 系統把程式運行成服務,找不到配置文件,原因是以服務運行它的工作目錄是在C:\Windows\System32 2.本次坑:WPF桌面程式通過註冊表設 ...
  • 在分散式系統中,數據的持久化是至關重要的一環。 Orleans 7 引入了強大的持久化功能,使得在分散式環境下管理數據變得更加輕鬆和可靠。 本文將介紹什麼是 Orleans 7 的持久化,如何設置它以及相應的代碼示例。 什麼是 Orleans 7 的持久化? Orleans 7 的持久化是指將 Or ...
  • 前言 .NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地添加、移除和管理功能。使用 Feature Management,開發人員可以根據不同用戶、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功 ...
  • 在 WPF 應用程式中,拖放操作是實現用戶交互的重要組成部分。通過拖放操作,用戶可以輕鬆地將數據從一個位置移動到另一個位置,或者將控制項從一個容器移動到另一個容器。然而,WPF 中預設的拖放操作可能並不是那麼好用。為瞭解決這個問題,我們可以自定義一個 Panel 來實現更簡單的拖拽操作。 自定義 Pa ...
  • 在實際使用中,由於涉及到不同編程語言之間互相調用,導致C++ 中的OpenCV與C#中的OpenCvSharp 圖像數據在不同編程語言之間難以有效傳遞。在本文中我們將結合OpenCvSharp源碼實現原理,探究兩種數據之間的通信方式。 ...
  • 一、前言 這是一篇搭建許可權管理系統的系列文章。 隨著網路的發展,信息安全對應任何企業來說都越發的重要,而本系列文章將和大家一起一步一步搭建一個全新的許可權管理系統。 說明:由於搭建一個全新的項目過於繁瑣,所有作者將挑選核心代碼和核心思路進行分享。 二、技術選擇 三、開始設計 1、自主搭建vue前端和. ...
  • Csharper中的表達式樹 這節課來瞭解一下表示式樹是什麼? 在C#中,表達式樹是一種數據結構,它可以表示一些代碼塊,如Lambda表達式或查詢表達式。表達式樹使你能夠查看和操作數據,就像你可以查看和操作代碼一樣。它們通常用於創建動態查詢和解析表達式。 一、認識表達式樹 為什麼要這樣說?它和委托有 ...
  • 在使用Django等框架來操作MySQL時,實際上底層還是通過Python來操作的,首先需要安裝一個驅動程式,在Python3中,驅動程式有多種選擇,比如有pymysql以及mysqlclient等。使用pip命令安裝mysqlclient失敗應如何解決? 安裝的python版本說明 機器同時安裝了 ...