前言
自從微前端框架micro-app開源后,很多小伙伴都非常感興趣,問我是如何實現的,但這並不是幾句話可以說明白的。為了講清楚其中的原理,我會從零開始實現一個簡易的微前端框架,它的核心功能包括:渲染、JS沙箱、樣式隔離、數據通信。由於內容太多,會根據功能分成四篇文章進行講解,這是系列文章的最終篇:數據通信篇。
通過這些文章,你可以了解微前端框架的具體原理和實現方式,這在你以后使用微前端或者自己寫一套微前端框架時會有很大的幫助。如果這篇文章對你有幫助,歡迎點贊留言。
相關推薦
- micro-app倉庫地址
- simple-micro-app倉庫地址
- 從零開始寫一個微前端框架-渲染篇
- 從零開始寫一個微前端框架-沙箱篇
- 從零開始寫一個微前端框架-樣式隔離篇
- 從零開始寫一個微前端框架-數據通信篇
- micro-app介紹
開始
架構設計
微前端各個應用本身是獨立運行的,通信系統不應該對應用侵入太深,所以我們采用發布訂閱系統。但是由於子應用封裝在micro-app標簽內,作為一個類webComponents的組件,發布訂閱系統的弱綁定和它格格不入。
最好的方式是像普通屬性一樣通過micro-app元素傳遞數據。但自定義元素無法支持對象類型的屬性,只能傳遞字符串,例如<micro-app data={x: 1}></micro-app>
會轉換為 <micro-app data='[object Object]'></micro-app>
,想要以組件化形式進行數據通信必須讓元素支持對象類型屬性,為此我們需要重寫micro-app原型鏈上setAttribute方法處理對象類型屬性。
流程圖
代碼實現
創建文件data.js
,數據通信的功能主要在這里實現。
發布訂閱系統
實現發布訂閱系統的方式很多,我們簡單寫一個,滿足基本的需求即可。
// /src/data.js
// 發布訂閱系統
class EventCenter {
// 緩存數據和綁定函數
eventList = new Map()
/**
* 綁定監聽函數
* @param name 事件名稱
* @param f 綁定函數
*/
on (name, f) {
let eventInfo = this.eventList.get(name)
// 如果沒有緩存,則初始化
if (!eventInfo) {
eventInfo = {
data: {},
callbacks: new Set(),
}
// 放入緩存
this.eventList.set(name, eventInfo)
}
// 記錄綁定函數
eventInfo.callbacks.add(f)
}
// 解除綁定
off (name, f) {
const eventInfo = this.eventList.get(name)
// eventInfo存在且f為函數則卸載指定函數
if (eventInfo && typeof f === 'function') {
eventInfo.callbacks.delete(f)
}
}
// 發送數據
dispatch (name, data) {
const eventInfo = this.eventList.get(name)
// 當數據不相等時才更新
if (eventInfo && eventInfo.data !== data) {
eventInfo.data = data
// 遍歷執行所有綁定函數
for (const f of eventInfo.callbacks) {
f(data)
}
}
}
}
// 創建發布訂閱對象
const eventCenter = new EventCenter()
發布訂閱系統很靈活,但太過於靈活可能會導致數據傳輸的混亂,必須定義一套清晰的數據流。所以我們要進行數據綁定,基座應用一次只能向指定的子應用發送數據,子應用只能發送數據到基座應用,至於子應用之間的數據通信則通過基座應用進行控制,這樣數據流就會變得清晰
通過格式化訂閱名稱來進行數據的綁定通信。
// /src/data.js
/**
* 格式化事件名稱,保證基座應用和子應用的綁定通信
* @param appName 應用名稱
* @param fromBaseApp 是否從基座應用發送數據
*/
function formatEventName (appName, fromBaseApp) {
if (typeof appName !== 'string' || !appName) return ''
return fromBaseApp ? `__from_base_app_${appName}__` : `__from_micro_app_${appName}__`
}
由於基座應用和子應用的數據通信方式不同,我們分開定義。
// /src/data.js
// 基座應用的數據通信方法集合
export class EventCenterForBaseApp {
/**
* 向指定子應用發送數據
* @param appName 子應用名稱
* @param data 對象數據
*/
setData (appName, data) {
eventCenter.dispatch(formatEventName(appName, true), data)
}
/**
* 清空某個應用的監聽函數
* @param appName 子應用名稱
*/
clearDataListener (appName) {
eventCenter.off(formatEventName(appName, false))
}
}
// 子應用的數據通信方法集合
export class EventCenterForMicroApp {
constructor (appName) {
this.appName = appName
}
/**
* 監聽基座應用發送的數據
* @param cb 綁定函數
*/
addDataListener (cb) {
eventCenter.on(formatEventName(this.appName, true), cb)
}
/**
* 解除監聽函數
* @param cb 綁定函數
*/
removeDataListener (cb) {
if (typeof cb === 'function') {
eventCenter.off(formatEventName(this.appName, true), cb)
}
}
/**
* 向基座應用發送數據
* @param data 對象數據
*/
dispatch (data) {
const app = appInstanceMap.get(this.appName)
if (app?.container) {
// 子應用以自定義事件的形式發送數據
const event = new CustomEvent('datachange', {
detail: {
data,
}
})
app.container.dispatchEvent(event)
}
}
/**
* 清空當前子應用綁定的所有監聽函數
*/
clearDataListener () {
eventCenter.off(formatEventName(this.appName, true))
}
}
在入口文件中創建基座應用通信對象。
// /src/index.js
+ import { EventCenterForBaseApp } from './data'
+ const BaseAppData = new EventCenterForBaseApp()
在沙箱中創建子應用的通信對象,並在沙箱關閉時清空所有綁定的事件。
// /src/sandbox.js
import { EventCenterForMicroApp } from './data'
export default class SandBox {
constructor (appName) {
// 創建數據通信對象
this.microWindow.microApp = new EventCenterForMicroApp(appName)
...
}
stop () {
...
// 清空所有綁定函數
this.microWindow.microApp.clearDataListener()
}
}
到這里,數據通信大部分功能都完成了,但還缺少一點,就是對micro-app元素對象類型屬性的支持。
我們重寫Element原型鏈上setAttribute方法,當micro-app元素設置data屬性時進行特殊處理。
// /src/index.js
// 記錄原生方法
const rawSetAttribute = Element.prototype.setAttribute
// 重寫setAttribute
Element.prototype.setAttribute = function setAttribute (key, value) {
// 目標為micro-app標簽且屬性名稱為data時進行處理
if (/^micro-app/i.test(this.tagName) && key === 'data') {
if (toString.call(value) === '[object Object]') {
// 克隆一個新的對象
const cloneValue = {}
Object.getOwnPropertyNames(value).forEach((propertyKey) => {
// 過濾vue框架注入的數據
if (!(typeof propertyKey === 'string' && propertyKey.indexOf('__') === 0)) {
cloneValue[propertyKey] = value[propertyKey]
}
})
// 發送數據
BaseAppData.setData(this.getAttribute('name'), cloneValue)
}
} else {
rawSetAttribute.call(this, key, value)
}
}
大功告成,我們驗證一下是否可以正常運行,在vue2項目中向子應用發送數據,並接受來自子應用的數據。
// vue2/pages/page1.vue
<template>
...
<micro-app
name='app'
url='http://localhost:3001/'
v-if='showapp'
id='micro-app-app1'
:data='data'
@datachange='handleDataChange'
></micro-app>
</template>
<script>
export default {
...
mounted () {
setTimeout(() => {
this.data = {
name: '來自基座應用的數據'
}
}, 2000)
},
methods: {
handleDataChange (e) {
console.log('接受數據:', e.detail.data)
}
}
}
</script>
在react17項目中監聽來自基座應用的數據並向基座應用發送數據。
// react17/index.js
// 數據監聽
window.microApp?.addDataListener((data) => {
console.log("接受數據:", data)
})
setTimeout(() => {
window.microApp?.dispatch({ name: '來自子應用的數據' })
}, 3000);
查看控制抬的打印信息:
數據正常打印,數據通信功能生效。
結語
從這些文章中可以看出,微前端的實現並不難,真正難的是開發、生產環境中遇到的各種問題,沒有完美的微前端框架,無論是Module Federation、qiankun。micro-app以及其它微前端解決方案,都會在某些場景下出現問題,了解微前端原理才能快速定位和處理問題,讓自己立於不敗之地。