https://github.com/micro-zoe/micro-app/issues/21
在做框架的时候,没法避免的遇见父子通信和兄弟通信的问题,但是如何实现通信这是个每个框架需要解决的问题。
最好的方式应该是像普通元素一样,通过micro-app元素传递数据。但自定义元素无法支持对象类型的属性,只能传递字符串,例如
会转换为
。
所以选择通过改写原型链上setAttribute方法处理对象类型属性,对传递的数据进行拦截,加上发布订阅实现父子的通信。
// /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)
}
}
微前端各个应用本身是独立运行的,通信系统不应该对应用侵入太深,所以我们采用发布订阅系统。
子应用要发给其他子应用的数据先发给基座,基座进行分发。
发布订阅系统很灵活,但太过于灵活可能会导致数据传输的混乱,必须定义一套清晰的数据流。所以我们要进行数据绑定,基座应用一次只能向指定的子应用发送数据,子应用只能发送数据到基座应用,至于子应用之间的数据通信则通过基座应用进行控制,这样数据流就会变得清晰
因为元素不能传递对象,所以最终的结果还是走了发布订阅。通过修改原生的setAttribute方法,当改变属性的时候,进行获取值,然后将获取的值放入发布订阅当中。进行传递。
// /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)
}
}
发布订阅功能的代码实现
// /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))
}
}
子应用对父亲的传值,通过标签本身的属性[data],通过datachange方法来进行传值的
核心代码
const app = appInstanceMap.get(this.appName)
if (app?.container) {
// 子应用以自定义事件的形式发送数据
const event = new CustomEvent('datachange', {
detail: {
data,
}
})
app.container.dispatchEvent(event)
}
// /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))
}
}