EventBus
适用于 iframe 的基座与子应用通信工具,支持跨浏览器标签页通信
安装
npm install event-bus-iframe
原理
- 基座和子应用之间的通信是基于浏览器 postMessage API 的
- 同源多标签页之间的通信、同源iframe之间的通信是基于 sharedWorker 的
- registryApi 仅是在基座的 bus 实例上注册一个回调函数,在调用的时候实时获取数据
- getData 函数返回 promise 类型的数据,原理是模拟了【子应用发送post事件 -> 基座通过 on 监听事件 -> 执行回调函数 -> 基座发送 post 事件 -> 子应用通过 on 监听事件】这么一个过程
使用说明
- 在 utils 文件夹下创建 bus.ts 文件用于初始化 EventBus 对象
// 基座中使用
import EventBus, { type BusOption } from 'event-bus-iframe'
const option: BusOption = {
name: 'app-main', // 当前应用的 name
children: ['app-sub', 'app-sub2'], // 可选,仅基座需要配置,子应用列表
// workerPath: '/worker.js', // 可选,当需要开启跨页签通信时,传入 worker.js 的地址
// devMode: import.meta.env.DEV, // 可选,开启时,可在控制台输出主子应用之间通信日志,生产环境关闭
}
const bus = new EventBus(option)
export default bus
// iframe 子应用中使用
import EventBus, { type BusOption } from 'event-bus-iframe'
const option: BusOption = {
name: 'app-sub', // 当前应用的 name
// workerPath: `/${import.meta.env.BASE_URL}/worker.js`, // 可选,当需要开启多 iframe 通信时,传入 worker.js 的地址。子应用不要使用框架的 workerPath
// devMode: import.meta.env.DEV, // 可选,开启时,可在控制台输出主子应用之间通信日志,生产环境关闭
}
const bus = new EventBus(option)
export default bus
- 使用
import bus, { type TransferData } from '@/utils/bus' // 初始化 EventBus
// 注册监听事件,包括监听来自基座/子应用 post 事件和自身的 emit 事件
bus.on<CustomType>('msg', (res) => {
console.log('基座收到 msg: ', res)
})
// 向基座/子应用发消息,使用 post
const data: TransferData = {
type: 'msg',
data: {
msg: 'message'
}
}
// 基座向子应用发送消息,第二个参数为目标子应用名称,如果第二个参数为字符串 global,则向子应用列表中的所有子应用广播消息
// 子应用向基座发送消息,第二个参数为空
bus.post(data, 'app-vite')
// 向自身发消息 —— EventBus 也支持当前应用中的发布订阅模式,类似于 mitt 或 this.$bus
bus.emit('msg', { msg: 'message'})
如果需要使用 script 标签引入的方式来使用,可将 lib 资源下的 main.js 文件上传到自己的静态服务来引入使用,引入方式参考:
<!-- 将 main.js 存放在当前工程公共目录 /event-bus 中 -->
<script src="/event-bus/main.js">
<script>
const bus = new EventBus(option)
</script>
更多配置
- api 调用
api 的设计借鉴了 ajax 的思想,子应用通过
bus.getData('api-path', params)
向基座发起一个数据请求时,基座会根据api-path
组织需要的数据并返回,这个过程是基于Promise
的,所以子应用可以在任何时候,任何位置获取基座可提供的数据。
// 基座 bus.ts
// 基座注册一个 user-info 的 api ,子应用可以通过 bus.getData('user-info') 来随时获取 return 的数据
bus.registryApi('user-info', async () => {
return {
username: 'Job',
age: 18
}
})
// 子应用中调用
interface IUserInfo {
token: string
}
bus.getData<IUserInfo>('user-info').then(res => {
console.log(res) // res 是符合 ApiData 结构的数据
})
- 动态更新子应用列表,默认会覆盖初始化的子应用列表,可通过第二个参数 isMerge 控制是否对子应用列表进行合并
const list: string[] = ['app-app1', 'app-app2']
bus.setChildrenApp(list)
bus.setChildrenApp(list, true) // 合并已有的子应用列表
使用注意事项
- 初始化的时候 name 不能为空,否则会报错:
EventBus: The name cannot be empty during initialization
; - 基座向子应用发送消息,需要确保子应用已渲染;
- 基座中使用时需要特别注意!基座向子应用发送消息基于 iframe 的 name 属性,所以编码的时候,需要确保 bus.post(data, target) 时 target 值与 iframe 的 name 属性一致,注册在 children 列表中的子应用名称与 iframe 的 name 属性一致。
保留字符
-
global
: post 的第二个参数如果为 global,则向子应用列表中的所有子应用以及父应用发送消息,所以子应用命名不可为 global ; -
get-data / set-data
: 用于服务基座注册接口,子应用调用接口的通信服务,事件的 type 不要使用这两个保留名称; -
page-unload
: 用于页面刷新或关闭时通知 shared-worker 清除缓存,事件的 type 不要使用该名称; -
appName
: 业务中 appName 被用于识别子应用,子应用路由使用查询参数时应避免使用 appName 参数名;
类型说明
/**
* 约定数据格式
* type - 事件类型
* from - 事件发起方
* target - 事件目标方
* timestamp - 事件发送时间戳
* data - 事件传递的数据
*/
export interface TransferData<T = unknown> extends Record<PropertyKey, unknown> {
type: string;
from?: string;
target?: string;
timestamp?: number;
data?: T;
}
export interface ApiData<T = unknown> extends TransferData {
type: EventBusType.GetData | EventBusType.SetData;
api?: string;
promise?: 'resolve' | 'reject';
params?: object;
data?: T;
}
/**
* event-bus 实例化配置
* name - 唯一标识
* children - 子应用列表,用于发送 global 事件
* workerPath - worker 静态文件地址,用于同源多标签页或同源 iframe 通信
* devMode - 开启时,可以在控制台输出 event-bus 通信的日志
*/
export interface BusOption {
name: string;
children?: string[];
workerPath?: string;
devMode?: boolean;
}
export declare class EventBus {
constructor(option: BusOption);
/**
* 监听事件,同时监听 emit 和 post 的事件
* @param type 事件类型
* @param cb 触发时的回调函数,接收符合 TransferData 结构的 res 参数
*/
on<T>(type: '*', cb: EventCBAll<T>): void;
on<T>(type: string, cb: EventCB<T>): void;
/**
* 发送本地事件
* @param type 事件类型
* @param data 传递的数据
*/
emit<T = unknown>(type: string, data?: T): void;
/**
* 向子应用、父应用发送消息
* @param data 传输的数据
* @param target 目标 iframe
* @param byId 是否通过 id 查找 iframe,默认通过 name 查找 iframe
*/
post<T = unknown>(data: TransferData<T>, target?: 'global' | string | string[], byId?: boolean): void;
/**
* 关闭事件监听
* @param type 事件类型
* @param cb 关闭监听的回调函数,如果不传 cb ,则关闭该事件类型下的所有函数
*/
off<T = unknown>(type: string, cb?: EventCB<T>): void;
/**
* 清除所有事件监听
*/
clearAll(): void;
/**
* 注册 api,用于 getData / getDataSelf 获取数据
* @param api api 名称
* @param cb 子应用调用 api 时的回调函数,cb 返回一个 Promise,resolve 的数据即为 getData 时拿到的数据
*/
registryApi(api: string, cb: ApiCb): void;
/**
* 调用框架提供的 api
* @param api 框架注册的接口名
* @param params 需要传递的参数
* @returns 返回一个 Promise eg. .then(res => res.data)
*/
getData<T>(api: string, params?: object): Promise<ApiData<T>>;
/**
* 浏览器某个标签页或 iframe 调用自己提供的 api
* @param api 注册的接口名
* @param params 需要传递的参数
* @returns 返回一个 Promise eg. .then(res => res.data)
*/
getDataSelf<T>(api: string, params?: object): Promise<ApiData<T>>;
/**
* 设置子应用列表
* @param list 传入的子应用名称列表
* @param isMerge 是否合并列表,默认替换 event-bus 实例上的 children 列表 (false),为 true 时与 children 列表进行合并。已进行去重处理。
*/
setChildrenApp(list: string[], isMerge?: boolean): void;
}
Shared Worker
- sharedworker 是一种特殊的 webworker ,可以由多个同源的页面共享,我们选取 sharedworker 进行跨标签页的通信。
- 因为 sharedworker 是受同源策略限制的,所以我们约定新打开的页面如果需要跨标签页通信的话,需要保持同源,如果要在新页签打开超链接,可以通过反向代理的方式,在同源代理超链接。
- 同一个 url 只会创建一个 sharedworker,其他页面再使用相同 url 创建 sharedworker,会复用已创建的 worker。
- 调试方式: 浏览器输入 chrome://inspect/
- 目前浏览器支持度不是很高,主流的 web 浏览器基本都支持,移动端浏览器支持欠佳。
- 在不支持 SharedWorker 的浏览器中,我们通过 onstorage 的方案来进行兼容。
- 子应用使用 sharedWorker 时,传入独立的的 worker path ,不要和框架共用。
Shared Worker 使用方法
- 对于 vue 项目来说,可以在 public 文件夹下创建 worker.js 文件,拷贝
node_modules/@hsmos/event-bus/src/worker.js
中的内容
// node_modules/@hsmos/event-bus/src/worker.js
const portList = []
onconnect = e => {
const port = e.ports[0]
portList.push(port)
port.onmessage = m => {
const data = m.data
if (data && data.type) {
if (data.type === 'page-unload') {
// 页面刷新或关闭时清除 port 缓存
const index = portList.findIndex(v => v === port)
if (index !== -1) portList.splice(index, 1)
} else {
// 排除自身 port,向其他 port 发消息
portList.filter(v => v !== port).forEach(v => {
v.postMessage(data)
})
}
}
}
}
-
在初始化 EventBus 的时候,option 中传入相应的 workerPath 。
-
使用 bus.emit() / bus.on() 即可在同源页签中发送/监听消息事件。
报错信息说明
-
EventBus: The name cannot be empty during initialization
出现这个报错一般是在初始化 EventBus 的时候没有传入 name,或 name 为空,常用的获取 name 的方式为通过 iframe 的 src 参数动态获取,此时应该检查初始化 EventBus 的地方和 iframe src -
EventBus: Please check if the iframe[name]: [' + iframeName + '] exists
出现该报错一般是基座给子应用发送消息的时候,根据 post() 第二个参数 target 查找指定的 iframe 没有找到,此时应该检查基座发送事件的参数及 iframe 的 name 属性 -
EventBus: Current tabs/iframes not exist api:
出现该报错一般是在执行 getDataSelf 方法的时候,没有找到注册的 api,此时应该检查 registryApi 是否注册成功
更新日志
- v0.0.2 - 2023-06-03
- EventBus 工具发布,支持 iframe 基座与子应用通信,支持跨标签页通信;
- v0.0.3 - 2023-06-10
- 支持同源多标签页、同源多 iframe 之间注册接口(registryApi)通信;
- v0.0.4 - 2023-07-11
- off 接口第二个参数选传;
- v0.0.5 - 2023-07-31
- registryApi / getData 支持 reject ;
- Post 第二个参数为
global
时,不再向父级发送消息,而且只向存活的子应用发送消息; - 不兼容更新,registryApi 回调函数新增入参 from;
- 解决框架在给子应用发消息时,子应用通过 sharedworker 重复通信的问题;
- v0.0.6 - 2023-08-03
- 页面关闭和刷新的时候,清除缓存的 worker 逻辑修正;
- v0.0.7 - 2023-08-22
- 解决子应用给框架发消息时,多个浏览器标签页同时接收到的问题;
- v0.0.8 - 2023-08-23
- 新增日志配置, options.devMode: true 或 localStorage 中配置 show-event-bus-log;