@vnb/mfe-events 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,769 @@
1
+ # @vnb/mfe-events
2
+
3
+ > 微前端事件通信插件 — Shell 与子应用间的统一事件协议
4
+
5
+ 适用于 Module Federation、Native Federation、iframe 等多种微前端架构。支持类型安全的事件总线、跨应用通知、iframe 消息代理等核心功能。
6
+
7
+ ---
8
+
9
+ ## 特性
10
+
11
+ - **三层事件域**:`EvtShl`(Shell 内部)、`EvtMfe`(跨应用)、`EvtApp`(子应用内部)
12
+ - **类型安全**:TypeScript 泛型约束,编译时检查事件名和 payload 类型
13
+ - **自动传输层适配**:MF/NF 用 `CustomEvent`,iframe 用 `postMessage`,Shell 内部用内存 EventTarget
14
+ - **插件化注册**:子应用只需一行代码注册,即可接收 Shell 推送
15
+ - **通知系统**:内置 DOM toast 渲染,支持自定义渲染器
16
+ - **CDN 支持**:UMD 构建,可直接 `<script>` 引入
17
+
18
+ ---
19
+
20
+ ## 安装
21
+
22
+ ### npm(推荐)
23
+
24
+ ```bash
25
+ npm install @vnb/mfe-events
26
+ ```
27
+
28
+ **Module Federation 共享配置**(子应用侧):
29
+
30
+ ```javascript
31
+ // webpack.config.js / ModuleFederationPlugin
32
+ shared: {
33
+ '@vnb/mfe-events': { singleton: true, eager: true, requiredVersion: false }
34
+ }
35
+ ```
36
+
37
+ **Native Federation 共享配置**(子应用侧):
38
+
39
+ ```json
40
+ // angular.json 或 federation.config.json
41
+ {
42
+ "shares": {
43
+ "@vnb/mfe-events": { "singleton": true, "eager": true, "requiredVersion": false }
44
+ }
45
+ }
46
+ ```
47
+
48
+ ### 本地文件(开发/调试)
49
+
50
+ ```json
51
+ // package.json
52
+ {
53
+ "@vnb/mfe-events": "file:../packages/mfe-events"
54
+ }
55
+ ```
56
+
57
+ 然后重新构建依赖:
58
+
59
+ ```bash
60
+ npm install
61
+ ```
62
+
63
+ ### CDN(不支持构建工具时)
64
+
65
+ ```html
66
+ <!-- 引入 UMD 构建 -->
67
+ <script src="https://@vnb/mfe-events.surge.sh/index.umd.js"></script>
68
+ <!-- <script src="https://unpkg.com/@vnb/mfe-events/dist/index.umd.js"></script> -->
69
+
70
+ <script>
71
+ const { mfeEventPlugin, mfeBus, EvtMfe, EvtShl } = window.MfeEvents;
72
+
73
+ mfeEventPlugin.register({
74
+ appId: 'my-app',
75
+ handlers: {
76
+ [EvtMfe.ThemeUpdate]: (data) => console.log('theme:', data.theme),
77
+ [EvtMfe.LocaleUpdate]: (data) => console.log('locale:', data.locale),
78
+ }
79
+ });
80
+ </script>
81
+ ```
82
+
83
+ UMD 构建同时暴露 `window.MfeEvents` 和 `window.__MFE_EVENT_PLUGIN__`。
84
+
85
+ ---
86
+
87
+ ## 架构
88
+
89
+ ### 三层事件域
90
+
91
+ ```
92
+ ┌─────────────────────────────────────────────────────────────┐
93
+ │ Shell 基座 │
94
+ │ │
95
+ │ shlBus.emit(EvtShl.TabOverflow, {...}) ← Shell 内部 │
96
+ │ ↓ │
97
+ │ mfeEventPlugin.emit(EvtMfe.ThemeUpdate, {...}) ← 广播 │
98
+ │ ↓ │
99
+ │ mfeEventPlugin.emitTo('mma', EvtMfe.RouteNavigate, {...}) ← 定向 │
100
+ └──────────────────────┬──────────────────────────────────────┘
101
+
102
+ ┌─────────────┴─────────────┐
103
+ │ CustomEvent │ postMessage
104
+ ↓ ↓
105
+ ┌──────────────────┐ ┌──────────────────┐
106
+ │ Module Federation │ │ iframe │
107
+ │ Native Federation │ │ │
108
+ │ mfeBus.on(event) │ │ mfeBus.on(event) │
109
+ └──────────────────┘ └──────────────────┘
110
+ ```
111
+
112
+ ### 传输层选择
113
+
114
+ | 场景 | 传输方式 | 代码路径 |
115
+ |------|---------|---------|
116
+ | Shell 内部 | 内存 EventTarget | `new Bus(new MemoryTransport())` |
117
+ | MF/NF 子应用 → Shell | `window.CustomEvent` | `new Bus(new WindowTransport())` |
118
+ | iframe 子应用 → Shell | `window.postMessage` | `new Bus(new PostMessageTransport())` |
119
+ | Shell → iframe | `iframe.contentWindow.postMessage` | `new IframeProxyTransport(iframe)` |
120
+
121
+ `mfeBus` 在初始化时自动检测环境选择合适的传输层。Shell 内部的 `shlBus` 永远不会触碰 `window`,完全隔离。
122
+
123
+ ---
124
+
125
+ ## 快速接入
126
+
127
+ ### 子应用接入(Angular/Vue/React 通用)
128
+
129
+ ```typescript
130
+ import mfeEventPlugin, { mfeBus } from '@vnb/mfe-events';
131
+ import { EvtMfe } from '@vnb/mfe-events';
132
+
133
+ // 1. 注册 → 接收 Shell 推送的事件
134
+ mfeEventPlugin.register({
135
+ appId: 'my-app', // 必须与 Shell 配置的 appId 一致
136
+ type: 'mf', // 'mf' | 'iframe'
137
+ handlers: {
138
+ // Shell → App:Shell 导航到指定路径
139
+ [EvtMfe.RouteNavigate]: (data) => {
140
+ // data.path: string,例如 '/booking/123'
141
+ router.navigateByUrl(data.path);
142
+ },
143
+
144
+ // Shell → App:主题切换
145
+ [EvtMfe.ThemeUpdate]: (data) => {
146
+ // data.theme: 'light' | 'dark'
147
+ document.documentElement.dataset.theme = data.theme;
148
+ },
149
+
150
+ // Shell → App:语言切换
151
+ [EvtMfe.LocaleUpdate]: (data) => {
152
+ // data.locale: string,例如 'zh-CN'
153
+ i18n.setLocale(data.locale);
154
+ },
155
+
156
+ // Shell → App:认证登出
157
+ [EvtMfe.AuthLogout]: () => {
158
+ clearAuthState();
159
+ router.navigate('/login');
160
+ },
161
+
162
+ // Shell → App:Token 刷新
163
+ [EvtMfe.AuthTokenUpdate]: (data) => {
164
+ // data: { token: string; user?: User; permissions?: string[] }
165
+ updateToken(data.token);
166
+ },
167
+
168
+ // Shell → App:认证过期
169
+ [EvtMfe.AuthExpired]: () => {
170
+ clearAuthState();
171
+ router.navigate('/login');
172
+ },
173
+ },
174
+ });
175
+
176
+ // 2. 向 Shell 上报路由变化(子应用内部导航时必须调用)
177
+ router.afterEach((to) => {
178
+ mfeBus.emit(EvtMfe.RouteChange, {
179
+ appId: 'my-app',
180
+ path: to.fullPath, // 完整路径,如 '/booking/123'
181
+ });
182
+ });
183
+
184
+ // 3. 向 Shell 上报应用就绪
185
+ mfeBus.emit(EvtMfe.LifecycleAppReady, { appId: 'my-app' });
186
+
187
+ // 4. 向 Shell 上报应用卸载(可选,但推荐)
188
+ window.addEventListener('unload', () => {
189
+ mfeBus.emit(EvtMfe.LifecycleAppUnmount, { appId: 'my-app' });
190
+ });
191
+
192
+ // 5. 使用通知(所有子应用共享 Shell 的通知系统)
193
+ mfeEventPlugin.success('保存成功');
194
+ mfeEventPlugin.info({ message: '详情信息', title: '提示', duration: 5000 });
195
+ mfeEventPlugin.warning('请注意,此操作不可撤销');
196
+ mfeEventPlugin.danger('操作失败,请重试');
197
+
198
+ // 6. 销毁时注销
199
+ mfeEventPlugin.unregister('my-app');
200
+ ```
201
+
202
+ ### Shell 端接入
203
+
204
+ ```typescript
205
+ import mfeEventPlugin, { mfeBus, shlBus } from '@vnb/mfe-events';
206
+ import { EvtMfe, EvtShl } from '@vnb/mfe-events';
207
+
208
+ // 注册子应用(接收子应用上报的事件)
209
+ mfeEventPlugin.register({
210
+ appId: 'mma',
211
+ handlers: {
212
+ [EvtMfe.RouteChange]: (data) => {
213
+ // data: { appId: string; path: string }
214
+ console.log('MMA 路由变化:', data.path);
215
+ },
216
+ [EvtMfe.LifecycleAppReady]: (data) => {
217
+ // data: { appId: string }
218
+ console.log('MMA 就绪');
219
+ },
220
+ [EvtMfe.LifecycleAppUnmount]: (data) => {
221
+ console.log('MMA 卸载:', data.appId);
222
+ },
223
+ [EvtMfe.Error]: (data) => {
224
+ // 子应用错误上报
225
+ console.error('MMA 错误:', data.source, data.message, data.stack);
226
+ },
227
+ },
228
+ });
229
+
230
+ // 定向发送(只发给指定子应用)
231
+ mfeEventPlugin.emitTo('mma', EvtMfe.RouteNavigate, { path: '/chairman' });
232
+ mfeEventPlugin.emitTo('mma', EvtMfe.RouteRefresh, { path: '/chairman' });
233
+
234
+ // 广播(发给所有子应用)
235
+ mfeEventPlugin.emit(EvtMfe.ThemeUpdate, { theme: 'dark' });
236
+ mfeEventPlugin.emit(EvtMfe.LocaleUpdate, { locale: 'zh-CN' });
237
+
238
+ // 生命周期
239
+ mfeEventPlugin.shellReady(); // 通知所有子应用 Shell 已就绪
240
+ mfeEventPlugin.isShellReady(); // 查询 Shell 就绪状态
241
+
242
+ // 认证快捷方法
243
+ mfeEventPlugin.authLogin({ userId: '1', name: 'Admin' });
244
+ mfeEventPlugin.authLogout('user'); // reason: 'user' | 'timeout' | 'error'
245
+ mfeEventPlugin.tokenUpdate({ token: 'xxx', permissions: ['read', 'write'] });
246
+ mfeEventPlugin.authExpired();
247
+ mfeEventPlugin.locale('zh-CN');
248
+
249
+ // Shell 内部通信(shlBus,不碰 window)
250
+ shlBus.on(EvtShl.TabOverflow, (data) => {
251
+ // data: { appId: string; label: string; openIds: string[] }
252
+ showTabOverflowDialog(data);
253
+ });
254
+ ```
255
+
256
+ ---
257
+
258
+ ## 事件协议
259
+
260
+ ### 事件命名规范
261
+
262
+ ```
263
+ {scope}:{domain}-{action}
264
+ ```
265
+
266
+ - `scope`:`shl`(Shell内部)、`mfe`(跨应用)、`app`(子应用内部)
267
+ - `domain`:状态域(theme、locale、auth、route、lifecycle、notify)
268
+ - `action`:动词(update、change、navigate、show、close)
269
+
270
+ ### 跨应用事件(EvtMfe)
271
+
272
+ | 常量 | 值 | 方向 | Payload |
273
+ |------|-----|------|---------|
274
+ | **ThemeUpdate** | `mfe:theme-update` | Shell → App | `{ theme: 'light' \| 'dark' }` |
275
+ | **LocaleUpdate** | `mfe:locale-update` | Shell → App | `{ locale: string }` |
276
+ | **BreakpointUpdate** | `mfe:breakpoint-update` | Shell → App | `{ current: string; orientation: 'portrait' \| 'landscape' }` |
277
+ | **StateUpdate** | `mfe:state-update` | Shell → App | `{ state: AppState; prev: AppState \| null }` |
278
+ | **ShellReady** | `mfe:shell-ready` | Shell → App | `{}` |
279
+ | **Error** | `mfe:error` | App → Shell | `{ source: string; message: string; stack?: string }` |
280
+ | **Raw** | `mfe:raw` | Any | `{ type: string; payload: any; source: string; ts: number }` |
281
+ | **AuthLogin** | `mfe:auth-login` | Shell → App | `{ user: User; token: string }` |
282
+ | **AuthLogout** | `mfe:auth-logout` | Shell → App | `{ reason?: 'user' \| 'timeout' \| 'error' }` |
283
+ | **AuthTokenUpdate** | `mfe:auth-token-update` | Shell → App | `{ token: string; user?: User; permissions?: string[] }` |
284
+ | **AuthTokenClear** | `mfe:auth-token-clear` | Shell → App | `{}` |
285
+ | **AuthPermissionUpdate** | `mfe:auth-permission-update` | Shell → App | `{ permissions: string[] }` |
286
+ | **AuthExpired** | `mfe:auth-expired` | Shell → App | `{}` |
287
+ | **RouteNavigate** | `mfe:route-navigate` | Shell → App | `{ path: string }` |
288
+ | **RouteRefresh** | `mfe:route-refresh` | Shell → App | `{ path: string }` |
289
+ | **RouteChange** | `mfe:route-change` | App → Shell | `{ appId: string; path: string }` |
290
+ | **LifecycleAppClose** | `mfe:lifecycle-app-close` | Shell → App | `{ appId: string }` |
291
+ | **LifecycleAppReady** | `mfe:lifecycle-app-ready` | App → Shell | `{ appId: string }` |
292
+ | **LifecycleAppUnmount** | `mfe:lifecycle-app-unmount` | App → Shell | `{ appId: string }` |
293
+ | **NotifyShow** | `mfe:notify-show` | Any | `NotificationConfig` |
294
+ | **NotifyClose** | `mfe:notify-close` | Any | `{ id: string }` |
295
+ | **NotifyCloseAll** | `mfe:notify-close-all` | Any | `{}` |
296
+
297
+ ### Shell 内部事件(EvtShl)
298
+
299
+ 仅 Shell 内部使用,通过 `shlBus` 通信,不经过 `window`。
300
+
301
+ | 常量 | 值 | Payload |
302
+ |------|-----|---------|
303
+ | **TabOverflow** | `shl:tab-overflow` | `{ appId: string; label: string; openIds: string[] }` |
304
+ | **TabClose** | `shl:tab-close` | `{ appId: string }` |
305
+ | **RouteReuse** | `shl:route-reuse` | `{ route: string; strategy: 'attach' \| 'detach' }` |
306
+ | **AuthSync** | `shl:auth-sync` | `{ token: string; user: User }` |
307
+ | **StateSync** | `shl:state-sync` | `{ state: AppState; prev: AppState \| null }` |
308
+ | **AppMounted** | `shl:app-mounted` | `{ appId: string; container: HTMLElement }` |
309
+ | **AppUnmounted** | `shl:app-unmounted` | `{ appId: string }` |
310
+
311
+ ### 子应用内部事件(EvtApp)
312
+
313
+ 用于子应用内部模块间通信,不跨应用。
314
+
315
+ ```typescript
316
+ import { EvtApp } from '@vnb/mfe-events';
317
+
318
+ const eventName = EvtApp('mma', 'booking-created');
319
+ // → 'app:mma:booking-created'
320
+
321
+ mfeBus.emit(eventName, { bookingId: '123' });
322
+ ```
323
+
324
+ ### 类型定义
325
+
326
+ ```typescript
327
+ // 用户信息
328
+ interface User {
329
+ userId: string;
330
+ username: string;
331
+ email?: string;
332
+ roles?: string[];
333
+ }
334
+
335
+ // 全局状态
336
+ interface AppState {
337
+ theme: 'light' | 'dark';
338
+ locale: string;
339
+ user: User | null;
340
+ permissions: string[];
341
+ features: Record<string, boolean>;
342
+ }
343
+
344
+ // 通知配置
345
+ interface NotificationConfig {
346
+ type: 'success' | 'info' | 'warning' | 'danger';
347
+ message: string;
348
+ title?: string;
349
+ duration?: number; // 默认 3000ms,0 = 不自动关闭
350
+ buttons?: Array<{
351
+ text: string;
352
+ role?: 'primary' | 'sticky';
353
+ handler: () => void;
354
+ }>;
355
+ }
356
+ ```
357
+
358
+ ---
359
+
360
+ ## API 参考
361
+
362
+ ### mfeEventPlugin(单例)
363
+
364
+ ```typescript
365
+ import mfeEventPlugin from '@vnb/mfe-events';
366
+ ```
367
+
368
+ #### 注册与注销
369
+
370
+ ```typescript
371
+ // 注册子应用
372
+ mfeEventPlugin.register({
373
+ appId: 'my-app', // 必填,子应用唯一标识
374
+ type: 'mf', // 选填,'mf' | 'iframe',默认 'mf'
375
+ handlers: { // 必填,事件处理函数映射
376
+ [EvtMfe.RouteNavigate]: (data) => { /* data: { path: string } */ },
377
+ [EvtMfe.ThemeUpdate]: (data) => { /* data: { theme: 'light' | 'dark' } */ },
378
+ // ...
379
+ },
380
+ fallback: (config) => { // 选填,自定义通知渲染器(优先级低于 useRenderer)
381
+ myToast.show(config);
382
+ return { close: () => myToast.hide(config.id) };
383
+ }
384
+ });
385
+
386
+ // 注册 iframe 子应用(Shell 侧调用)
387
+ mfeEventPlugin.registerIframe(
388
+ 'my-iframe-app', // appId
389
+ iframeElement, // HTMLIFrameElement
390
+ 'https://parent-origin' // 选填,targetOrigin
391
+ );
392
+
393
+ // 注销子应用
394
+ mfeEventPlugin.unregister('my-app');
395
+ ```
396
+
397
+ #### 发送事件
398
+
399
+ ```typescript
400
+ // 定向发送(只发给指定子应用)
401
+ mfeEventPlugin.emitTo('mma', EvtMfe.RouteNavigate, { path: '/booking' });
402
+ mfeEventPlugin.emitTo('mma', EvtMfe.RouteRefresh, { path: '/chairman' });
403
+
404
+ // 广播(发给所有子应用)
405
+ mfeEventPlugin.emit(EvtMfe.ThemeUpdate, { theme: 'dark' });
406
+ mfeEventPlugin.emit(EvtMfe.LocaleUpdate, { locale: 'en-US' });
407
+ mfeEventPlugin.emit(EvtMfe.ShellReady, {});
408
+ ```
409
+
410
+ #### 通知
411
+
412
+ ```typescript
413
+ // 快捷方式(推荐)
414
+ mfeEventPlugin.success('保存成功'); // 3s 自动关闭
415
+ mfeEventPlugin.success('已保存', '提示'); // 带标题
416
+ mfeEventPlugin.info({ message: '详情', title: '信息', duration: 5000 });
417
+ mfeEventPlugin.warning('请确认操作', undefined, { duration: 0 }); // 手动关闭
418
+ mfeEventPlugin.danger('操作失败');
419
+
420
+ // 完整配置
421
+ const { close } = mfeEventPlugin.notify({
422
+ type: 'success',
423
+ message: '数据已保存',
424
+ title: '成功',
425
+ duration: 5000,
426
+ buttons: [
427
+ { text: '查看', role: 'primary', handler: () => navigate('/list') },
428
+ { text: '取消', handler: () => {} },
429
+ ],
430
+ });
431
+
432
+ // 关闭单个
433
+ close();
434
+
435
+ // 关闭所有
436
+ mfeEventPlugin.closeAll();
437
+ ```
438
+
439
+ #### 自定义通知渲染器(Shell 侧)
440
+
441
+ Shell 可以接管通知渲染,使用自己的 UI 组件。
442
+
443
+ ```typescript
444
+ import { mfeEventPlugin, EvtMfe } from '@vnb/mfe-events';
445
+ import { MyToastContainer } from './components/ToastContainer';
446
+
447
+ // 注册自定义渲染器
448
+ mfeEventPlugin.useRenderer(
449
+ // render: 收到通知配置时调用
450
+ (config) => {
451
+ MyToastContainer.show({
452
+ id: config.id,
453
+ type: config.type,
454
+ message: config.message,
455
+ title: config.title,
456
+ duration: config.duration,
457
+ onClose: () => mfeEventPlugin.notify({ type: 'info', message: '' }).close?.(),
458
+ });
459
+ },
460
+ // close: 关闭指定通知
461
+ (id) => MyToastContainer.hide(id),
462
+ // clear: 关闭所有通知
463
+ () => MyToastContainer.clear()
464
+ );
465
+ ```
466
+
467
+ #### 生命周期
468
+
469
+ ```typescript
470
+ // 通知所有子应用 Shell 已就绪(Shell 初始化完成后调用一次)
471
+ mfeEventPlugin.shellReady();
472
+
473
+ // 查询 Shell 就绪状态
474
+ const ready = mfeEventPlugin.isShellReady(); // boolean
475
+ ```
476
+
477
+ #### 认证快捷方法
478
+
479
+ ```typescript
480
+ // 广播登录事件(data: User)
481
+ mfeEventPlugin.authLogin({ userId: '1', username: 'admin', roles: ['admin'] });
482
+
483
+ // 广播登出事件
484
+ mfeEventPlugin.authLogout(); // reason: 'user'
485
+ mfeEventPlugin.authLogout('timeout'); // reason: 'timeout'
486
+ mfeEventPlugin.authLogout('error'); // reason: 'error'
487
+
488
+ // 广播 Token 刷新
489
+ mfeEventPlugin.tokenUpdate({
490
+ token: 'new-jwt-token',
491
+ user: { userId: '1', username: 'admin', roles: ['admin'] },
492
+ permissions: ['read', 'write'],
493
+ });
494
+
495
+ // 广播认证过期
496
+ mfeEventPlugin.authExpired();
497
+
498
+ // 广播语言切换
499
+ mfeEventPlugin.locale('zh-CN');
500
+ ```
501
+
502
+ ---
503
+
504
+ ### mfeBus / shlBus
505
+
506
+ ```typescript
507
+ import { mfeBus, shlBus } from '@vnb/mfe-events';
508
+ ```
509
+
510
+ #### Bus 方法
511
+
512
+ ```typescript
513
+ // 发送事件
514
+ mfeBus.emit(EvtMfe.RouteChange, { appId: 'my-app', path: '/booking' });
515
+ shlBus.emit(EvtShl.TabOverflow, { appId: 'mma', label: 'MMA', openIds: ['mma', 'mrbs'] });
516
+
517
+ // 订阅事件,返回取消函数
518
+ const off = mfeBus.on(EvtMfe.ThemeUpdate, (data) => {
519
+ console.log('theme changed to:', data.theme);
520
+ });
521
+
522
+ // 订阅一次
523
+ const offOnce = mfeBus.once(EvtMfe.ShellReady, () => {
524
+ console.log('Shell is ready!');
525
+ });
526
+
527
+ // 取消订阅
528
+ off();
529
+ offOnce();
530
+ ```
531
+
532
+ ---
533
+
534
+ ### Transport 类(高级用法)
535
+
536
+ ```typescript
537
+ import { MemoryTransport, WindowTransport, PostMessageTransport, IframeProxyTransport } from '@vnb/mfe-events';
538
+ ```
539
+
540
+ | 类 | 用途 | 构造参数 |
541
+ |---|------|---------|
542
+ | `MemoryTransport` | Shell 内部通信 | 无 |
543
+ | `WindowTransport` | MF/NF 同Tab通信 | 无 |
544
+ | `PostMessageTransport` | iframe 子应用通信 | `{ targetWindow?, targetOrigin? }` |
545
+ | `IframeProxyTransport` | Shell → iframe 代理 | `iframe: HTMLIFrameElement, targetOrigin?: string` |
546
+
547
+ ---
548
+
549
+ ## 框架集成示例
550
+
551
+ ### Angular 子应用
552
+
553
+ ```typescript
554
+ // app.component.ts 或专用初始化文件
555
+ import { mfeEventPlugin, mfeBus, EvtMfe } from '@vnb/mfe-events';
556
+ import { Router } from '@angular/router';
557
+
558
+ @Injectable({ providedIn: 'root' })
559
+ export class MfeEventsService {
560
+ private router = inject(Router);
561
+
562
+ init() {
563
+ mfeEventPlugin.register({
564
+ appId: 'mma',
565
+ type: 'mf',
566
+ handlers: {
567
+ [EvtMfe.RouteNavigate]: (data) => this.router.navigateByUrl(data.path),
568
+ [EvtMfe.ThemeUpdate]: (data) => document.documentElement.dataset['theme'] = data.theme,
569
+ [EvtMfe.AuthLogout]: () => this.handleLogout(),
570
+ [EvtMfe.AuthTokenUpdate]: (data) => this.updateToken(data.token),
571
+ [EvtMfe.AuthExpired]: () => this.handleLogout(),
572
+ },
573
+ });
574
+
575
+ this.router.events.pipe(filter(e => e instanceof NavigationEnd)).subscribe((e) => {
576
+ const url = (e as NavigationEnd).urlAfterRedirects;
577
+ mfeBus.emit(EvtMfe.RouteChange, { appId: 'mma', path: url });
578
+ });
579
+
580
+ mfeBus.emit(EvtMfe.LifecycleAppReady, { appId: 'mma' });
581
+ }
582
+
583
+ private handleLogout() {
584
+ localStorage.clear();
585
+ this.router.navigate(['/login']);
586
+ }
587
+
588
+ private updateToken(token: string) {
589
+ localStorage.setItem('token', token);
590
+ }
591
+ }
592
+ ```
593
+
594
+ ### Vue 子应用
595
+
596
+ ```typescript
597
+ // main.ts
598
+ import { createApp } from 'vue';
599
+ import App from './App.vue';
600
+ import { mfeEventPlugin, mfeBus, EvtMfe } from '@vnb/mfe-events';
601
+
602
+ const app = createApp(App);
603
+
604
+ app.mount('#app');
605
+
606
+ mfeEventPlugin.register({
607
+ appId: 'mrbs',
608
+ type: 'mf',
609
+ handlers: {
610
+ [EvtMfe.RouteNavigate]: (data) => router.push(data.path),
611
+ [EvtMfe.ThemeUpdate]: (data) => document.documentElement.dataset.theme = data.theme,
612
+ [EvtMfe.AuthLogout]: () => { localStorage.clear(); router.push('/login'); },
613
+ [EvtMfe.AuthTokenUpdate]: (data) => localStorage.setItem('token', data.token),
614
+ [EvtMfe.AuthExpired]: () => { localStorage.clear(); router.push('/login'); },
615
+ },
616
+ });
617
+
618
+ router.afterEach((to) => {
619
+ mfeBus.emit(EvtMfe.RouteChange, { appId: 'mrbs', path: to.fullPath });
620
+ });
621
+
622
+ mfeBus.emit(EvtMfe.LifecycleAppReady, { appId: 'mrbs' });
623
+ ```
624
+
625
+ ### React 子应用
626
+
627
+ ```typescript
628
+ // index.tsx
629
+ import React from 'react';
630
+ import ReactDOM from 'react-dom/client';
631
+ import App from './App';
632
+ import { mfeEventPlugin, mfeBus, EvtMfe } from '@vnb/mfe-events';
633
+
634
+ mfeEventPlugin.register({
635
+ appId: 'frontend',
636
+ type: 'mf',
637
+ handlers: {
638
+ [EvtMfe.RouteNavigate]: (data) => navigate(data.path),
639
+ [EvtMfe.ThemeUpdate]: (data) => document.documentElement.dataset.theme = data.theme,
640
+ [EvtMfe.AuthLogout]: () => { localStorage.clear(); navigate('/login'); },
641
+ [EvtMfe.AuthTokenUpdate]: (data) => localStorage.setItem('token', data.token),
642
+ [EvtMfe.AuthExpired]: () => { localStorage.clear(); navigate('/login'); },
643
+ },
644
+ });
645
+
646
+ const navigate = (path: string) => window.history.pushState(null, '', path);
647
+
648
+ window.addEventListener('popstate', () => {
649
+ mfeBus.emit(EvtMfe.RouteChange, { appId: 'frontend', path: window.location.pathname });
650
+ });
651
+
652
+ mfeBus.emit(EvtMfe.LifecycleAppReady, { appId: 'frontend' });
653
+
654
+ ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
655
+ ```
656
+
657
+ ---
658
+
659
+ ## 常见问题
660
+
661
+ ### 事件收不到
662
+
663
+ 1. 确认 `appId` 与 Shell 配置一致(区分大小写)
664
+ 2. 确认 Federation 配置中有 `@vnb/mfe-events` 且 `singleton: true`
665
+ 3. 确认使用的是事件常量 `EvtMfe.XXX` 而非硬编码字符串
666
+ 4. 控制台检查 `window.__MFE_EVENT_PLUGIN__` 是否存在
667
+ 5. 如果是 iframe,确认调用了 `mfeEventPlugin.registerIframe()`
668
+
669
+ ### 通知不显示
670
+
671
+ 1. 确认调用的是 `mfeEventPlugin.success/warning/danger()` 而非直接 dispatch 事件
672
+ 2. 如果 Shell 侧注册了 `useRenderer()`,通知会由 Shell 渲染,子应用侧看到的是 Shell 的 UI
673
+ 3. 检查 `duration` 是否为 `0`(需手动 `close()`)
674
+
675
+ ### iframe 子应用收不到事件
676
+
677
+ 1. 确认 Shell 侧调用了 `mfeEventPlugin.registerIframe(appId, iframeEl, targetOrigin)`
678
+ 2. 确认 `targetOrigin` 与 iframe 的 `src` origin 匹配(生产环境建议明确指定)
679
+ 3. 检查浏览器控制台是否有 postMessage 相关的 CSP 警告
680
+
681
+ ### TypeScript 编译报错找不到 `@vnb/mfe-events`
682
+
683
+ ```bash
684
+ npm install @vnb/mfe-events --save
685
+ ```
686
+
687
+ 如果使用本地文件路径,确保 `tsconfig.json` 的 `paths` 配置正确:
688
+
689
+ ```json
690
+ {
691
+ "compilerOptions": {
692
+ "paths": {
693
+ "@vnb/mfe-events": ["../packages/mfe-events/src"]
694
+ }
695
+ }
696
+ }
697
+ ```
698
+
699
+ ### 如何上报自定义错误到 Shell
700
+
701
+ ```typescript
702
+ try {
703
+ await fetch('/api/data');
704
+ } catch (err) {
705
+ mfeBus.emit(EvtMfe.Error, {
706
+ source: 'my-app',
707
+ message: err.message,
708
+ stack: err.stack,
709
+ });
710
+ }
711
+ ```
712
+
713
+ ### 子应用之间如何通信
714
+
715
+ 通过 Shell 中转:
716
+
717
+ ```typescript
718
+ // 子应用 A 想发消息给子应用 B
719
+ // → 发给 Shell
720
+ mfeBus.emit(EvtMfe.RouteChange, { appId: 'app-a', path: '/some-path' });
721
+
722
+ // Shell 监听后,通过 emitTo 转发给 B
723
+ mfeEventPlugin.register({
724
+ appId: 'app-a',
725
+ handlers: {
726
+ [EvtMfe.RouteChange]: (data) => {
727
+ if (data.path.includes('book')) {
728
+ mfeEventPlugin.emitTo('app-b', EvtMfe.RouteNavigate, { path: '/notification' });
729
+ }
730
+ },
731
+ },
732
+ });
733
+ ```
734
+
735
+ ---
736
+
737
+ ## 类型安全
738
+
739
+ `@vnb/mfe-events` 提供完整的 TypeScript 类型支持。事件名和 payload 类型在编译时检查:
740
+
741
+ ```typescript
742
+ import { mfeBus, mfeEventPlugin, EvtMfe } from '@vnb/mfe-events';
743
+
744
+ // ✅ 类型安全:data 被推断为 { theme: 'light' | 'dark' }
745
+ mfeBus.on(EvtMfe.ThemeUpdate, (data) => {
746
+ document.documentElement.dataset.theme = data.theme;
747
+ });
748
+
749
+ // ✅ 类型安全:EvtMfe.RouteNavigate 的 data 是 { path: string }
750
+ mfeEventPlugin.emitTo('mma', EvtMfe.RouteNavigate, { path: '/chairman' });
751
+
752
+ // ❌ 编译错误:缺少 required field `path`
753
+ mfeEventPlugin.emitTo('mma', EvtMfe.RouteNavigate, {});
754
+
755
+ // ❌ 编译错误:invalid payload type
756
+ mfeEventPlugin.emitTo('mma', EvtMfe.RouteNavigate, { path: 123 });
757
+ ```
758
+
759
+ ---
760
+
761
+ ## 更新日志
762
+
763
+ See [CHANGELOG](./CHANGELOG.md) for version history.
764
+
765
+ ---
766
+
767
+ ## 许可证
768
+
769
+ MIT