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