跳转到内容

不同 Framework 之间的全局 Event

全局 Event 不会自动让微前端解耦。Browser-native Event 可以作为少量、短暂、framework-neutral Platform Intent 的传输机制。但只要 Remote 开始通过它传播业务 Event、同步 State,或者控制其他 Remote 的业务反应,就会形成隐藏的横向耦合。

Remote 可以请求 Platform 展示一个全局 UI,或者执行某项技术反应。

但它不应该通过 Global Event Bus 去协调其他 Remote 的业务逻辑。

全局 Event 可以协调 Platform,但不应该协调业务架构

Section titled “全局 Event 可以协调 Platform,但不应该协调业务架构”

假设一个应用包含 Angular Host、Angular Remote 和 React Remote。两个 Remote 都可以保存数据。保存成功后,产品需要展示一条全局 Success Message。

Notification System 属于 Host。Host 拥有全局展示区域,知道 Theme,处理 Focus Management 与 Accessibility,并决定消息在哪里出现、显示多久。React Remote 不能直接使用这套 Angular Component,但也不应该因此知道 Host 的 Angular Service、Host Store 或 NgRx Dispatcher。

Angular Remote 同样如此。它从技术上更容易接触 Host 的 Angular Internal,但不代表应该这么做。双方使用同一个 Framework 只是技术偶然,Platform Contract 仍然是一条 Application Boundary。

两个 Remote 只需要表达用户应该得到什么反馈:

type NotificationSeverity = 'success' | 'info' | 'warning' | 'error';
interface NotificationIntent {
severity: NotificationSeverity;
message: string;
}

这个 Contract 不描述 Angular Component,也不描述 React Component。它不包含 Store 或 Service,只表达一个语义:Platform 应该以某种 Severity 向用户展示消息。

这是第一条重要区分:NotificationIntent 描述的是消息的语义。它还没有规定这条消息怎样跨过技术边界,也没有规定 Host 内部怎样处理。

Intent、Transport 与处理是三层不同问题

Section titled “Intent、Transport 与处理是三层不同问题”

跨 Framework Communication 经常把三件事混在一起:

  1. 这条消息意味着什么?
  2. 它如何跨越 Application Boundary?
  3. Receiver 内部如何处理它?

在 Notification 例子中,语义是:用户需要知道某项操作已成功完成。Transport 可以是直接 Function Call,也可以是在某个 EventTarget 上发送 CustomEvent。进入 Host Boundary 后,Angular Host 可以调用 Service、更新 Signal、处理 Queue、修改 Signal Store,或者发送内部 NgRx Event。

这些决策不属于同一个 Contract。

Intent 不是 Browser Event。Browser Event 只是一种 Transport。Host 内部的 NgRx Event 也不是跨 Framework Contract,它只是 Host 自己的 Implementation Decision。

Platform Contract 在 Host Adapter 处结束。

Adapter 后面才是 Host 的内部架构。

Angular 与 React 使用同一份 Contract

Section titled “Angular 与 React 使用同一份 Contract”

Host 可以提供一个很小的 framework-neutral Platform Context:

interface PlatformNotifications {
show(intent: NotificationIntent): void;
}
interface PlatformContext {
notifications: PlatformNotifications;
}

Angular Remote 使用它:

platform.notifications.show({
severity: 'success',
message: 'Die Planung wurde gespeichert.',
});

React Remote 同样使用它:

platform.notifications.show({
severity: 'success',
message: 'Changes saved.',
});

两个应用看到的是同一项 Platform Capability。没有任何一个知道 Host 内部具体使用哪种 Notification Component。Angular Remote 不 Inject Host Service,不修改外部 Store,也不 Import Internal Event Creator。React Remote 更不需要知道 Angular DI 或 NgRx。

Host 只负责提供 Adapter:

const platformContext: PlatformContext = {
notifications: {
show: (intent) => {
notificationAdapter.show(intent);
},
},
};

Adapter 把稳定 Platform Contract 翻译成当前 Host Internal。今天可以调用 Notification Service,明天可以引入 Queue,或者把 Intent 转成 Host-internal Event。只要外部 Contract 稳定,Remote 不需要跟随这些内部修改。

NgRx、Signal 或 Internal Event Dispatcher 本身没有问题。问题只在于是否把它们穿过 Host Boundary,变成所有 Remote 的 Integration Contract。

Remote 不是“给 Angular 发一个 NgRx Event”。

它表达一个 framework-neutral Intent,再由 Angular Host 翻译进自己的架构。

Angular 与 React Remote 都通过 framework-neutral Platform Contract 向 Angular Host Adapter 传递相同 Notification Intent;Adapter 再把它转换为内部 Service、Signal、Signal Store 或 NgRx Event,并控制全局 Notification UI。

直接 Platform Contract 往往是更简单的线路

Section titled “直接 Platform Contract 往往是更简单的线路”

如果 Host 本身 Mount Remote,那么 Host 与 Remote 之间已经存在直接技术关系。它可以在 Mount 时传入 Platform Context:

interface RemoteApplication {
mount(container: HTMLElement, context: PlatformContext): void;
unmount(): void;
}

例如:

reactRemote.mount(container, platformContext);

这既不是 Angular Mechanism,也不是 React Mechanism,只是两个应用之间普通的 JavaScript Contract。

这种方式有可见 Owner 和明确 Receiver。调用可以 Type-safe,容易测试,也和 Remote Lifecycle 绑定。不存在“全局搜索 Listener”,也不存在未知数量的潜在 Reaction。

因此,如果 Host 本来就能够提供 Platform Context,直接 Contract 经常比 Global Event 更容易理解。Cross-framework Communication 并不自动需要 Browser-native Event。

这也不代表 Function Call 永远更好。它只是另一种 Addressing。Callback 仍然可能携带糟糕的业务 Contract,Browser Event 也可能携带设计良好的 Platform Intent。

Callback 与 CustomEvent 只是同一消息的两根不同线路。

线路本身不会决定架构是否合理。

什么时候 Browser-native Event 可以合理

Section titled “什么时候 Browser-native Event 可以合理”

并不是所有 Integration 都有直接 Mount Relationship。应用可能彼此独立加载,Platform Context 也可能无法直接取得,或者产品 Platform 本来就定义了一个技术 Channel。

这时 Browser-native Event 可以成为合理 Transport:

platformEvents.dispatchEvent(
new CustomEvent('platform:notification-requested', {
detail: {
severity: 'success',
message: 'Changes saved.',
},
}),
);

Angular Host 在这个 Channel 上注册 Listener,把 Payload 交给 Adapter。只有从 Adapter 之后,才重新进入 Angular Internal Architecture。

关键在于限制 Channel 的边界。通过 Platform Context 提供的 Product-specific EventTarget,比直接在 window 上建立一个无限增长的 Event Bus 更容易控制:

interface PlatformContext {
events: EventTarget;
}

“全产品可用”不等于“必须挂在 window 上”。

当设计上刻意避免 Direct Reference,或者确实存在多个被明确批准的技术 Observer 时,EventTarget 可以合理。但它不会解决独立 Load Timing:Dispatch 时还没注册的 Consumer 仍然会错过消息。因此,仅仅因为 Angular 和 React 同时存在,并不能让 Event 自动成为更好的方案。

不同 Framework 的存在,只证明 Contract 需要 framework-neutral。

它并没有规定 Transport 必须是什么。

Event、Intent 与 State 不是同一件事

Section titled “Event、Intent 与 State 不是同一件事”

技术类型 CustomEvent 完全无法告诉你 Payload 在架构上是什么。它可以携带 Event、Intent、State Change,也可以携带一个设计糟糕的 Command。

做一个简单语义区分就很有帮助:

Event
└── 某件事已经发生
Intent
└── 希望 Platform 做某件事
State
└── 当前成立的事实

OrderCreated 描述已经完成的业务事实。NotificationRequested 请求 Platform 展示内容。CurrentLocale 描述当前有效状态。

这些概念不需要在每个边界问题上被学术化。但不能把 Transport 当成 Meaning。名为 refreshRemoteBCustomEvent 仍然是一个可疑的 Remote-to-Remote Command,而直接 Function Call 完全可以传递合理 Platform Intent。

Browser API 不会替你分类架构。

真正重要的是:消息意味着什么、谁拥有 Reaction,以及谁会依赖它的发生。

Notification 很适合 Platform Capability

Section titled “Notification 很适合 Platform Capability”

Global Notification 是一个好例子,因为它有明确 UI Owner。Host 控制产品共同 UI,因此可以一致处理 Presentation、Theme、Position、Focus 与 Accessibility。

这个 Intent 还是短暂的。后来启动的 Remote 没必要重建几分钟前出现过的 Success Message。Notification 不是 Authoritative Business State Source。通常 Sender 也不等待 Response,其他 Remote 更不应该因为这条 Notification 启动自己的业务流程。

正是这些属性限制了耦合:

  • Host 中有明确 Owner;
  • 简单的 framework-neutral Payload;
  • 短生命周期效果;
  • Remote 之间不存在业务 Reaction Chain;
  • 不承担“永久表示当前 State”的责任。

类似 Platform Capability 还可能包括 Global Help、Navigation、Command Palette 或 Telemetry。但即便如此,也应该继续判断 Direct Platform Function 是否比 Event 更清晰。Global Channel 不是“凡是多个应用都会碰到的东西”的垃圾桶。

对于短暂 Notification,后加载的 Remote 不知道早先消息完全没问题。但对 Platform State 来说,这就不成立。

localeChanged 可以告诉你语言刚刚变化。但一个更晚启动的 Remote 已经错过这个 Event。如果没有另一处 Source,它根本不知道当前 Locale 是什么。

因此,Platform State 需要一份随时可以 Query 的当前表示,并且可以额外提供变化通知:

interface LocaleSource {
current(): string;
subscribe(listener: (locale: string) => void): () => void;
}

Theme、Session、当前 User、Permission、Feature Configuration、Tenant 或 Network Status 都是同一个原则。Event 可以通知 State Changed,但不能成为 Current State 的唯一来源。

凡是后来启动的 Remote 必须重建出来的 State,就不能只依赖短暂 Event。

这条边界很重要,因为 Global Event Channel 极易被误用成简单 Synchronization Mechanism。一开始只有 localeChanged,随后出现 sessionUpdatedpermissionsChangedfeatureFlagsLoaded。每加一条 Event,就多一条隐含假设:所有相关应用必须在正确时间监听,并按预期顺序收到每一条消息。

这不是稳健 State Architecture。

这是 Temporal Coupling。

Remote 之间的业务 Event 仍然是横向通信

Section titled “Remote 之间的业务 Event 仍然是横向通信”

Global Channel 真正开始危险,是当一个 Remote 发布业务消息,目的是让其他 Remote 产生业务 Reaction。

例如 Angular Remote 发布:

planningSaved

然后 React Remote 重载数据,Vue Remote 更新 Badge,Host 改变 Navigation。

Sender 没有 Import 其他 Remote Function,甚至可能不知道它们名字。但所有 Consumer 仍然依赖这条消息:Event Name、Payload、Timing、Order、缺失、重复,以及 Contract Version。

一条可见依赖:

Remote A ─────────▶ Remote B

只是变成了间接结构:

Remote A
└── planningSaved
Global Event Bus
├── Remote B reloads data
├── Remote C updates a badge
└── Host changes navigation

Dependency Graph 并没有变小。

只是更难看见了。

Sender 不知道 Consumer,但 Consumer 对消息的依赖仍然存在。

这并没有与“Remote 不应该直接通信”矛盾,反而是在把它说得更准确:通过 Global Bus 建立的间接业务 Reaction Chain,仍然属于横向通信。中间多了一层 Browser,不会让它自动变成 Vertical Communication。

Direct Coupling 很容易识别:

remoteB.reload();

Event-based Coupling 看起来就模糊得多:

events.dispatchEvent(new CustomEvent('planning:saved'));

如果 Remote B 在业务上必须在 planning:saved 后 Reload,这条 Dependency 就是真实存在的。它只是从 Dispatch Site 消失,分散进 Listener、Registration 与 Reaction Logic。

Event Bus 可以减少 Technical Reference,却不会保证 Business Independence。更糟时,它甚至会隐藏哪些应用事实上必须共同修改和共同测试。

Loose Addressing 不等于 Loose Business Coupling。

常见 Myth 是:Sender 不认识 Consumer,所以应用已经解耦。事实上,只是 Static Reference 的方向消失了,Behavior Dependency 仍然存在。planningSaved 的语义一变,多个未知位置可能错误响应;Event 没发出,Projection 过期;发两次,产生重复 Load;Consumer 启动太晚,消息直接丢失。

Global Event Bus 不会消除耦合。

它经常只是把耦合从可见 Dependency Graph 中拿掉。

直接 Remote 依赖与 Global Event Bus 的对比:planningSaved 通过 Bus 在多个 Remote 和 Host 中触发隐式业务 Reaction。

Global Channel 很少一开始就被设计成完整业务基础设施。最初也许只有 Notification,再加一个技术 remoteReady。然后慢慢出现 planningSavedcustomerChangedselectionChangedrefreshRequesteddataReloaded

再过一段时间,Reaction Chain 出现了:

planningSaved
└── refreshRequested
└── dataReloaded
└── selectionChanged
└── badgeUpdated

每条消息局部看都合理,组合在一起却形成一个分布式流程,没有任何地方能够完整看到它。

此时 Order 与 Load Timing 都变得重要。Event 可能丢失或被重复处理。Unmount 后 Listener 还活着。再次 Mount 又注册一份同样 Handler。Notification 出现两次,Data Load 两次,Test 开始依赖 Application 启动顺序。

Ownership 同样模糊。谁允许触发 selectionChanged?哪个 Payload 才是 Contract?新 Consumer 需要理解所有历史特殊情况吗?谁决定某条 Event 什么时候可以删除?Reaction Chain 到哪一步才算真正结束?

一旦业务流程依赖这些 Reaction Chain,Event Bus 就已经成为隐藏的 Application Architecture。

问题不在 Browser API。自研 Dispatcher 或 Shared Library 一样可能做出同样结构。问题是:独立业务应用之间形成了 Global、Implicit Behavioral Contract。

向 Platform 垂直通信,而不是 Remote 之间横向协调

Section titled “向 Platform 垂直通信,而不是 Remote 之间横向协调”

Notification Intent 的方向很清楚:

Remote
└── Notification Intent
Host Platform
└── owns presentation and reaction

Host 是 Global UI 的明确 Owner。其他 Remote 不需要对消息产生业务 Reaction。Contract framework-neutral,描述的是 Platform Capability。

业务 Event 的结构不同:

Remote A
└── business Event
Global Event Bus
Remote B and Remote C
└── must react

这里出现多个隐式 Consumer 和业务 Reaction Chain。结束条件模糊,Dependency Graph 分散在应用里,一次修改可能同时影响多个 Remote。

与 Platform 的 Vertical Communication 可以合理。

Remote 之间的 Horizontal Coordination 仍然值得警惕。

这个区别也不只取决于 Event Name。即便叫 notificationRequested,如果多个 Remote 据此启动自己的业务流程,它仍然切错了。反过来,只要机制留在 Host Boundary 内部,Host 内部使用什么 Event/Store/Service 都是它自己的决定。

消息的 Meaning、Direction 和 Ownership,决定它是 Platform Capability,还是隐藏的 Horizontal Dependency。

很小的 Platform Contract 也需要维护

Section titled “很小的 Platform Contract 也需要维护”

合理 Platform Intent 也不是无人管理的自由区域。Contract 需要明确 Owner、Documented Payload 和清楚 Name。

platform:notification-requestedupdatechangedshowSuccess 更能表达 Intent,因为它指向 Platform Capability,而不是某个具体 Snackbar Component。Payload 应尽量由简单、framework-neutral Data 组成。

Component Instance、Angular Service、React Node、Store 或业务 Aggregate 都不应该进入 Global Transport Contract。Framework Callback 也不应该借 Payload 再次穿透边界。

Contract Change 需要有意识进行。增加 Optional Field 通常比彻底改变既有语义更容易 Backward Compatible;发生较大语义断裂时,新 Name 或 Contract Version 反而更清楚。这里不一定需要复杂 Schema Registry,但至少要承认:一个很小的 Browser Event 仍然是一份需要维护的 Platform Contract。

如果 Platform 提供 Global Event Channel,就必须连同它的 Lifecycle 一起拥有。

Listener 在 Mount 时注册,在 Unmount 时移除。这种对称性属于 Integration Contract,不是后续清理工作。

mount
└── register listener
unmount
└── remove listener

没有明确 Cleanup,旧 Handler 会继续存在;再次 Mount 会重复注册;已经不可见的应用仍然响应;Test 互相污染;短暂技术消息最后留下 Global Residual State。

没有 Cleanup 的 Global Event,不是 Loose Coupling。

只是 Global Residual State。

Browser Event 本身仍然可以使用,只是 Lifecycle 必须和 Channel 一样被设计。

小而无聊的 Event Channel 是质量信号

Section titled “小而无聊的 Event Channel 是质量信号”

合理 Global Channel 只包含少量、明确选择的 Platform Contract。每一项都有 Owner,传递简单数据,不在 Remote 之间触发业务 Reaction Chain。

它适合短暂、技术性或 UI-related Platform Intent:不需要 Response,也不替代必须长期可重建的 State。

不应该放进这里的是:业务 State Synchronization、refreshRemoteB 这类 Remote-to-Remote Command、用于全局协调的 CRUD Event、Business Process Orchestration、Foreign Store,以及任何 Authoritative Data Source 的替代品。

因此,Channel 的大小不是审美问题。它揭示 Platform 是提供少量清楚拥有的 Capability,还是已经让 Event Bus 接管了 Remote 的业务 Integration。

好的 Global Event Channel 应该小、短暂,而且很无聊。

Angular 与 React Remote 可以把同一个 framework-neutral Notification Intent 交给 Angular Host。究竟通过直接 Platform Contract 还是 Browser-native Event 传输,只是 Transport Decision。真正进入 Host 之后,Adapter 才把 Intent 转成 Service Call、Signal、Store、Queue 或 Internal NgRx Event。

Transport 不等于消息语义。Notification 是短暂 Platform Intent,有清晰 UI Owner。Current Platform State 则还需要 Queryable Source,让后来启动的 Remote 能重建当前值。

如果其他 Remote 依赖业务 State 或业务 Event,就不应该把它们塞进 Global Channel。Event Bus 只会删除 Direct Reference,不会删除 Horizontal Coupling。技术上看起来优雅,也改变不了多个 Consumer 仍然依赖共同 Name、Payload、Order、Load Timing 和 Reaction Chain。

没有 Direct Reference,不等于 Independent Changeability。

好的 Global Event Channel 保持小、短暂、无聊:

它协调 Platform,而不是协调业务。