接上一篇文章《系统架构设计师--软件架构风格--独立构件风格--发布-订阅模式(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
也就是说,要把"内存里的 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. 建议:做一个"网络版"的最小改造方案
如果你愿意,我可以把这个示例升级成真正跨浏览器的版本,思路是:
- 新增一个轻量 WebSocket 服务器(比如 Node 的
ws,约 20 行),充当"事件中心",维护所有连接,broadcast事件; - 原
EventEmitter不变,但加一个"网络桥":emit时通过 WS 发给服务器;浏览器收到服务器推送时,再调用本地bus.emit去更新 DOM; - 前端
subscribe逻辑基本不动,只是把"事件来源"从本地emit换成"服务器推送"。
这样你用手机浏览器、桌面浏览器、另一台电脑同时打开,任意一端发布,其余端实时收到——这才是真正的分布式 Pub/Sub。
要不要我按上面第 5 点给你写一份具体的改造方案(含服务器代码 + 前端改动)?如果要,请确认你倾向哪种技术栈:
- A:Node.js +
ws(WebSocket,最贴近原示例,推荐) - B:Node.js + SSE(实现更简单,单向推送)
- C:直接用 MQTT / Redis 这类现成 broker(更"生产级")
确认后我可以给出完整可运行的代码与步骤(注意:当前是 ask 模式,需要你切换到 craft 模式我才能帮你落地文件)。