前端抽象化,打破框架枷锁:多端消息通讯
在我的编程小圈子里,出现了这样的一个话题:
情况
场景:一个项目需要拆分成多个部分,同时与 APP 进行混合开发。这带来了两个子问题:
1. 多个部分是采用微前端库进行沟通还是先用iframe?
2. 与APP的通讯又使用什么库?
讨论中,大家针对这两个问题各有推荐,争论点集中在不同库的特性、可移植性、扩展性,以及未来替换库的成本。
这些讨论虽然热闹,但往往是分开进行的,缺乏统一视角。都是分别讨论这两个问题的,各有各自的推荐方式。
个人的想法是:我认为,这两个问题本质上是同一个问题,不需要分开讨论。通过合理的架构设计,我们可以忽略具体的实现差异,避免陷入“选哪个库”的争论。
为什么?
无论是:
- APP 与 Web 端(如 DSBridge、UniWebView)
- 多 Web 端互通(如微前端、iframe)
- Electron 的 Node 与 Web 端(如 IPC)
它们的底层逻辑都是发消息与监听消息,很类似于发布-订阅模式(Publish-Subscribe Pattern,简称 Pub/Sub)
相关的框架底层,也是依赖这种类型的内置方式进行实现的,但是通常会扩展一些已经定义好的API和规范
统一思路
基于这个共性,我们可以抽象出一种通用的通信模式:
- 核心:统一消息格式、发送方式和接收方式。
- 好处:具体实现(用什么库、框架)变成可替换的细节,随时调整不影响整体架构。
这样,无论是对内的多部分通信,还是对外的 APP 交互,都可以用统一的抽象层处理。未来若需更换某个库,只需调整底层实现,调用层几乎无需改动。
这里将展示web端的抽象,有了这个,换到APP端,也能通过各自在线免费的AI工具,转换成对应语言的抽象
抽象接口
// /Bridge.ts
/**
* @description: 信息交流模块
* @return {*}
*/
export interface Bridge {
listenerBox: Record<string, BridgeListener>;
sendMessage(message: MessageDto): void;
addBridgeListener(listener: BridgeListener): void;
}
/**
* @description: 监听消息的实例
* @return {*}
*/
export interface BridgeListener<T = string> {
listentType: T;
onMessage(message: MessageDto): void;
}
/**
* @description: 消息的数据类型,传递时转为json字符串,接收后反序列化
* @return {*}
*/
export interface MessageDto<T = string> {
/**
* @description: 消息类型,用于区分不同的消息
*/
messageType: T;
/**
* @description: 消息数据
*/
data: Record<string, any>;
}
// /Bridge.enum.ts
enum TypeEnum {
/**
* @description: TOKEN传递 APP->Web
*/
TOKEN = 'token'
}
示例的实现
/**
* @description: wenview 通信模块
* @return {*}
*/
export class webviewEvent implements Bridge {
listenerBox: Record<string, BridgeListener> = {};
constructor() {}
sendMessage(message: MessageDto<TypeEnum>): void {
// do something
window.postMessage(JSON.stringify(message), "*");
}
addBridgeListener(listener: BridgeListener<TypeEnum>): void {
this.listenerBox[listener.listentType] = listener;
}
openListening() {
window.addEventListener("message", (event) => {
const message = JSON.parse(event.data);
const listener = this.listenerBox[message.messageType];
if (listener) {
listener.onMessage(message);
}
});
}
}
/**
* @description: 创建监听器 这个其实要不要都可以,有了,只是更方便创建一个监听器
* @return {*}
*/
export class ListenerBuilder implements BridgeListener<TypeEnum> {
listentType: TypeEnum;
constructor(
listentType: TypeEnum,
onMessage: BridgeListener<TypeEnum>["onMessage"]
) {
this.listentType = listentType;
this.onMessage = onMessage;
}
onMessage(message: MessageDto<TypeEnum>): void {
// do something
}
}
UML图,来展示一下依赖关系
如此进行设计之后
通过这样的抽象设计,无论面对何种 Web 跨端交互(Web 与 APP、Web 与 Web、Node 与 Web),我们在开发时无需关心:
- 对端是什么(APP、iframe、Electron 等)。
- 底层如何实现(DSBridge、postMessage、IPC 等)。
开发者只需按照统一的接口类型(Messenger)调用和使用即可。这种设计带来的好处是:
- 低成本重构:
- 如果需要切换底层库(比如从 DSBridge 换成 UniWebView),只需在 Bridge 的实现处修改代码,其他调用层无需调整,全局生效。
- 关注点分离:
- 业务逻辑与通信实现解耦,开发更聚焦于功能本身。
针对开头的问题(多部分通信与 APP 交互),可以认为这是一个问题,通过工厂模式统一解决:
- 定义一个 BridgeFactory,根据场景创建不同的 Bridge 实现。
- 例如,一个 Bridge 处理微前端通信,一个处理 APP 交互。
一些建议
实现代码也可以结合单例模式和依赖注入,来让web中用起来及其方便,