网络版本计划--系统架构设计师--软件架构风格--独立构件风格--发布-订阅模式(Pub/Sub)例子

0 阅读5分钟

接上一篇文章《系统架构设计师--软件架构风格--独立构件风格--发布-订阅模式(Pub/Sub)例子》,我先让 SI 把后续可以完善的工作写出来,做个记录。

现在的这个 event-pub-sub.html 是一个纯浏览器内存版、同页面内的发布-订阅示例。一旦让"发布者"和"订阅者"分别跑在**不同的浏览器(甚至只是同一个浏览器的两个标签页)**上,它们之间完全不会互通——发布者发出的事件,订阅者一个都收不到。

下面解释为什么,以及要做到"跨网络"需要补什么。


1. 现在的代码到底在做什么

整套机制的核心是一个内存里的对象 bus:

class EventEmitter {
    constructor() {
        this.events = new Map();   // { 事件名: Set<回调函数> }
    }
    on(name, cb) { ... this.events.get(name).add(cb); }
    emit(name, data) {
        const set = this.events.get(name);
        if (set) [...set].forEach((cb) => cb(data));  // 只是循环调用本页面里的函数
    }
}

发布按钮也只是调用了同一个 bus 实例:

const bus = new EventEmitter();  // 实例化事件中心
const EVENT = "priceUpdate";

subscribe("📱 手机端", ...);
subscribe("💻 桌面端", ...);
subscribe("🤖 量化机器人", ...);

document.getElementById("publish").onclick = () => {
    const data = document.getElementById("msg").value;
    bus.emit(EVENT, data);   // 只在"这一页"的内存里广播
};

关键点:bus.events 这个 Map 里存的是当前页面里的函数引用(闭包)。emit 做的事就是 forEach 调用这些本地函数。整个过程没有一行代码碰到网络。


2. 跨浏览器时会怎样(为什么不行)

每个浏览器标签页(tab)/每个浏览器进程,都是一个独立的 JavaScript 运行时 + 独立内存空间:

  • 你在浏览器 A 打开这个文件 → 它自己 new 了一个 busA,自己 subscribe 了三块 div;
  • 你在浏览器 B 打开同一个文件 → 它自己 new 了一个 busB,自己 subscribe 了三块 div;
  • 浏览器 A 点"发布" → busA.emit() → 只触发 busA 里的回调 → 只更新浏览器 A 自己的 DOM;
  • 浏览器 B 自始至终毫无反应,因为它从来没往 busA 里 on 过任何回调,也根本拿不到 busA 这个对象(busA 在另一个进程的内存里,跨进程/跨机器无法共享一个 JS 对象引用)。

emit 里遍历的 Set<回调> 是内存里的函数对象,函数没法序列化、没法通过网络传过去,所以现在的"事件中心"本质上是个进程内(in-process)的事件中心,不是网络事件中心。

补充:就算你把"不同浏览器"降格成"同一浏览器的两个标签页",结论也一样——每个标签页是独立的 JS 上下文,各自有各自的 bus,依然不通。(同浏览器跨标签页可用 BroadcastChannel,但它仅限于同一浏览器的同源页面,解决不了"不同浏览器/不同机器"。)


3. 要让它真正跨网络,缺什么

缺一个网络层的"事件中心 / 消息代理(broker)"——一个所有浏览器都能连上的中间服务器。浏览器之间不能直接互发,必须都连到这个中心,由中心来转发/广播。

flowchart LR
    subgraph B1[浏览器 A · 发布者]
        P[publish 按钮] -->|WS/HTTP 发送事件| S
    end
    subgraph S[网络事件中心 / 消息代理<br/>(必须新增的服务器)]
        HUB[(event hub: 维护连接列表)]
    end
    subgraph B2[浏览器 B · 订阅者]
        S -->|WS/SSE 推送| SUB1[📱 手机端]
    end
    subgraph B3[浏览器 C · 订阅者]
        S -->|WS/SSE 推送| SUB2[💻 桌面端]
    end
    subgraph B4[浏览器 D · 订阅者]
        S -->|WS/SSE 推送| SUB3[🤖 量化机器人]
    end

image.png

也就是说,要把"内存里的 Map"换成"服务器上维护的客户端连接列表",把 emit 换成"向所有连接写socket"。


4. 几种可选方案对比

方案适合场景说明
WebSocket + 自建小服务器(Node ws / Python 等)双向、实时、最通用浏览器连上 WS,发布者 send 事件,服务器 broadcast 给所有订阅者。最贴近你这个示例。
SSE(Server-Sent Events)服务器→浏览器单向推送订阅者用 EventSource 接收;发布者用普通 fetch/POST 把事件发给服务器。实现比 WS 简单。
BroadcastChannel仅同一浏览器、跨标签页零服务器,但不满足你的"不同浏览器"需求,这里提一下避免误用。
现成消息中间件(MQTT / Redis Pub-Sub / Kafka / RabbitMQ)生产级、多服务把 EventEmitter 换成连这些 broker 的客户端,概念上最"正统"的分布式 Pub/Sub。

5. 建议:做一个"网络版"的最小改造方案

如果你愿意,我可以把这个示例升级成真正跨浏览器的版本,思路是:

  1. 新增一个轻量 WebSocket 服务器(比如 Node 的 ws,约 20 行),充当"事件中心",维护所有连接,broadcast 事件;
  2. 原 EventEmitter 不变,但加一个"网络桥":emit 时通过 WS 发给服务器;浏览器收到服务器推送时,再调用本地 bus.emit 去更新 DOM;
  3. 前端 subscribe 逻辑基本不动,只是把"事件来源"从本地 emit 换成"服务器推送"。

这样你用手机浏览器、桌面浏览器、另一台电脑同时打开,任意一端发布,其余端实时收到——这才是真正的分布式 Pub/Sub。


要不要我按上面第 5 点给你写一份具体的改造方案(含服务器代码 + 前端改动)?如果要,请确认你倾向哪种技术栈:

  • A:Node.js + ws(WebSocket,最贴近原示例,推荐)
  • B:Node.js + SSE(实现更简单,单向推送)
  • C:直接用 MQTT / Redis 这类现成 broker(更"生产级")

确认后我可以给出完整可运行的代码与步骤(注意:当前是 ask 模式,需要你切换到 craft 模式我才能帮你落地文件)。