前端抽象化,打破框架枷锁:多端消息通讯

在我的编程小圈子里,出现了这样的一个话题:

情况

场景:一个项目需要拆分成多个部分,同时与 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图,来展示一下依赖关系

uml diagram

如此进行设计之后

通过这样的抽象设计,无论面对何种 Web 跨端交互(Web 与 APP、Web 与 Web、Node 与 Web),我们在开发时无需关心:

  • 对端是什么(APP、iframe、Electron 等)。
  • 底层如何实现(DSBridge、postMessage、IPC 等)。

开发者只需按照统一的接口类型(Messenger)调用和使用即可。这种设计带来的好处是:

  1. 低成本重构
  • 如果需要切换底层库(比如从 DSBridge 换成 UniWebView),只需在 Bridge 的实现处修改代码,其他调用层无需调整,全局生效。
  1. 关注点分离
  • 业务逻辑与通信实现解耦,开发更聚焦于功能本身。

针对开头的问题(多部分通信与 APP 交互),可以认为这是一个问题,通过工厂模式统一解决:

  • 定义一个 BridgeFactory,根据场景创建不同的 Bridge 实现。
  • 例如,一个 Bridge 处理微前端通信,一个处理 APP 交互。

一些建议

实现代码也可以结合单例模式和依赖注入,来让web中用起来及其方便,
最后更新日期 6/24/2025, 10:29:18 AM