@vnb/mfe-events 0.0.1-rc.6 → 0.0.1-rc.8

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 CHANGED
@@ -225,7 +225,7 @@ mfeBus.on(EvtMfe.ShlTokenExpired, (data) => {
225
225
  // 子应用 token 过期 → 重发当前 token
226
226
  });
227
227
 
228
- // 3. 定向发送(子应用必须已 register(),否则 emitTo 静默失败)
228
+ // 3. 定向发送(三级降级:registry DOM 扫描 iframe[data-mfe-app] → 广播 fallback)
229
229
  mfeEventPlugin.emitTo('mma', EvtMfe.RouteNavigate, { path: '/chairman' });
230
230
 
231
231
  // 4. 广播(所有子应用均可收到)
@@ -332,7 +332,7 @@ shlBus.on(EvtShl.TabOverflow, (data) => {
332
332
  |------|-----|------|------|---------|---------|
333
333
  | **ThemeUpdate** | `mfe:theme-update` | Shell → App | 主题切换 | `{ theme: 'light' \| 'dark' }` | "主题切换了,你们跟着换" |
334
334
  | **LocaleUpdate** | `mfe:locale-update` | Shell → App | 语言切换 | `{ locale: string }` | "语言切换了,你们跟着换" |
335
- | **BreakpointUpdate** | `mfe:breakpoint-update` | Shell → App | 响应式断点/横竖屏变化(Shell 基于 `window.matchMedia` 监听并广播,子应用收到后同步更新布局,如侧边栏收起、表格横向滚动等) | `{ current: string; orientation: 'portrait' \| 'landscape' }` | "屏幕尺寸变了,你们调整一下布局" |
335
+ | **BreakpointUpdate** | `mfe:breakpoint-update` | Shell → App | 响应式断点/横竖屏变化(Shell 基于 `window.matchMedia` 监听并广播,子应用收到后同步更新布局,如侧边栏收起、表格横向滚动等) | `{ current: string; orientation: string }` | "屏幕尺寸变了,你们调整一下布局" |
336
336
  | **StateUpdate** | `mfe:state-update` | Shell → App | 全局状态同步 | `{ state: AppState; prev: AppState \| null }` | "全局状态更新了,你们同步" |
337
337
  | **ShellReady** | `mfe:shell-ready` | Shell → App | Shell 初始化完成 | `{}` | "我准备好了,你们可以开始工作" |
338
338
  | **Error** | `mfe:error` | App → Shell | 子应用错误上报 | `{ source: string; message: string; stack?: string }` | "我报错了,你看一下" |
@@ -349,8 +349,8 @@ shlBus.on(EvtShl.TabOverflow, (data) => {
349
349
  | **AuthSelfOk** | `mfe:auth-self-ok` | App → Shell | 自行登录成功 | `{ appId: string; user?: any; token?: string }` | "我自行登录成功了" |
350
350
  | **AuthSelfErr** | `mfe:auth-self-err` | App → Shell | 自行登录失败 | `{ appId: string; reason?: string }` | "我自行登录失败了" |
351
351
  | **AuthSelfDeny** | `mfe:auth-self-deny` | App → Shell | 子应用拒绝加载,请求基座卸载 | `{ appId: string; reason?: string }` | "我不想被加载,卸载掉我" |
352
- | **RouteNavigate** | `mfe:route-navigate` | Shell → App | Shell 指定子应用导航到路径(`emitTo` 定向) | `{ path: string }` | "跳到这个路径去" |
353
- | **RouteRefresh** | `mfe:route-refresh` | Shell → App | 要求子应用刷新当前页面,保留路由(`emitTo` 定向) | `{ path: string }` | "刷新你当前的路由/页面" |
352
+ | **RouteNavigate** | `mfe:route-navigate` | Shell → App | Shell 指定子应用导航到路径(`emitTo` 定向) | `{ path: string; appId?: string }` | "跳到这个路径去" |
353
+ | **RouteRefresh** | `mfe:route-refresh` | Shell → App | 要求子应用刷新当前页面,保留路由(`emitTo` 定向) | `{ path: string; appId?: string }` | "刷新你当前的路由/页面" |
354
354
  | **RouteChange** | `mfe:route-change` | App → Shell | 子应用上报路由变化 | `{ appId: string; path: string }` | "我路由变了,跟一下" |
355
355
  | **LifecycleAppClose** | `mfe:lifecycle-app-close` | Shell → App | Shell 关闭子应用 | `{ appId: string }` | "我要关掉你了" |
356
356
  | **LifecycleAppReady** | `mfe:lifecycle-app-ready` | App → Shell | 子应用初始化完成 | `{ appId: string }` | "我加载完了" |
@@ -573,8 +573,8 @@ mfeEventPlugin.register({
573
573
  appId: 'my-app',
574
574
  handlers: {
575
575
  [EvtMfe.BreakpointUpdate]: (data) => {
576
- // data.current: 'xs' | 'sm' | 'lg'
577
- // data.orientation: 'portrait' | 'landscape'
576
+ // data.current: string (如 'xs', 'sm', 'lg', 'md', 'xl' 等)
577
+ // data.orientation: string (如 'portrait', 'landscape')
578
578
  document.documentElement.dataset.breakpoint = data.current;
579
579
  document.documentElement.dataset.orientation = data.orientation;
580
580
  },
@@ -823,18 +823,14 @@ ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
823
823
  2. 确认 Federation 配置中有 `@vnb/mfe-events` 且 `singleton: true`
824
824
  3. 确认使用的是事件常量 `EvtMfe.XXX` 而非硬编码字符串
825
825
  4. 控制台检查 `window.__MFE_EVENT_PLUGIN__` 是否存在
826
- 5. 如果是 iframe,确认调用了 `mfeEventPlugin.registerIframe()`
827
- 6. 如果是动态加载场景,`emitTo` 调用时子应用尚未注册,会在控制台看到警告:`emitTo: app not registered`
826
+ 5. 如果是 iframe,确认调用了 `mfeEventPlugin.registerIframe()`,或 iframe 设置了 `data-mfe-app="appId"` 属性
827
+ 6. `emitTo` 支持三级降级:registry 命中 → DOM 扫描 `iframe[data-mfe-app]` 广播 fallback(带 appId 让子应用自判断)
828
828
 
829
829
  ### emitTo 报 app not registered
830
830
 
831
- 动态加载时,如果 Shell 在子应用注册之前就调用了 `emitTo`,消息会丢失。控制台会输出:
832
-
833
- ```
834
- [mfe-events] emitTo: app not registered: "mma" (event: mfe:route-navigate)
835
- ```
836
-
837
- 解决方案:确保子应用先调用 `register()`,再让 Shell 发 `emitTo`。或者在子应用 `LifecycleAppReady` 之后,再由子应用主动请求数据(而不是等 Shell push)。
831
+ 动态加载时,如果 Shell 在子应用注册之前就调用了 `emitTo`,插件会自动降级处理:
832
+ 1. 扫描 DOM 中 `iframe[data-mfe-app="appId"]`,找到则动态创建 transport 定向发送
833
+ 2. 都没找到则广播 `emit`(带 appId 参数),子应用收到后自行判断是否处理
838
834
 
839
835
  ### 通知不显示
840
836
 
@@ -844,7 +840,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
844
840
 
845
841
  ### iframe 子应用收不到事件
846
842
 
847
- 1. 确认 Shell 侧调用了 `mfeEventPlugin.registerIframe(appId, iframeEl, targetOrigin)`
843
+ 1. 确认 iframe 设置了 `data-mfe-app="appId"` 属性,或 Shell 调用了 `mfeEventPlugin.registerIframe(appId, iframeEl, targetOrigin)`
848
844
  2. 确认 `targetOrigin` 与 iframe 的 `src` origin 匹配(生产环境建议明确指定)
849
845
  3. 检查浏览器控制台是否有 postMessage 相关的 CSP 警告
850
846
 
package/dist/bus.d.ts CHANGED
@@ -17,7 +17,6 @@ export declare class Bus<T extends Record<string, any>> {
17
17
  * Returns unsubscribe function
18
18
  */
19
19
  on<K extends keyof T>(event: K, handler: (data: T[K]) => void, options?: {
20
- priority?: number;
21
20
  once?: boolean;
22
21
  }): () => void;
23
22
  /**
@@ -25,5 +24,9 @@ export declare class Bus<T extends Record<string, any>> {
25
24
  * Returns unsubscribe function
26
25
  */
27
26
  once<K extends keyof T>(event: K, handler: (data: T[K]) => void): () => void;
27
+ /**
28
+ * Destroy the bus and clean up all resources
29
+ */
30
+ destroy(): void;
28
31
  }
29
32
  //# sourceMappingURL=bus.d.ts.map
package/dist/bus.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"bus.d.ts","sourceRoot":"","sources":["../src/bus.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,qBAAa,GAAG,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAChC,OAAO,CAAC,SAAS;gBAAT,SAAS,EAAE,SAAS;IAExC;;OAEG;IACH,IAAI,CAAC,CAAC,SAAS,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI;IAInD;;;OAGG;IACH,EAAE,CAAC,CAAC,SAAS,MAAM,CAAC,EAClB,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,EAC7B,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,GAC9C,MAAM,IAAI;IAOb;;;OAGG;IACH,IAAI,CAAC,CAAC,SAAS,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI;CAO7E"}
1
+ {"version":3,"file":"bus.d.ts","sourceRoot":"","sources":["../src/bus.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,qBAAa,GAAG,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAChC,OAAO,CAAC,SAAS;gBAAT,SAAS,EAAE,SAAS;IAExC;;OAEG;IACH,IAAI,CAAC,CAAC,SAAS,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI;IAInD;;;OAGG;IACH,EAAE,CAAC,CAAC,SAAS,MAAM,CAAC,EAClB,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,EAC7B,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,GAC3B,MAAM,IAAI;IAOb;;;OAGG;IACH,IAAI,CAAC,CAAC,SAAS,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI;IAQ5E;;OAEG;IACH,OAAO,IAAI,IAAI;CAGhB"}
package/dist/index.d.ts CHANGED
@@ -20,7 +20,8 @@ export { MemoryTransport, WindowTransport, PostMessageTransport, IframeProxyTran
20
20
  export type { Transport } from './transport';
21
21
  export { Bus } from './bus';
22
22
  export { EvtShl, EvtMfe, EvtApp } from './constants';
23
- export type { PayloadShl, PayloadMfe, PayloadApp, NotificationType, NotificationConfig, AppType, AppMeta, PluginOptions, } from './types';
23
+ export type { PayloadShl, PayloadMfe, PayloadApp, NotificationType, NotificationConfig, AppType, AppMeta, PluginOptions, User, AppState, BreakpointValue, DeviceTypeValue, } from './types';
24
+ export { Breakpoint, DeviceType, breakpointToDevice, } from './types';
24
25
  export { mfeEventPlugin } from './plugin';
25
26
  export type { default as MfeEventPlugin } from './plugin';
26
27
  export { mfeEventPlugin as default } from './plugin';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC3G,YAAY,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAG7C,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAG5B,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrD,YAAY,EACV,UAAU,EACV,UAAU,EACV,UAAU,EACV,gBAAgB,EAChB,kBAAkB,EAClB,OAAO,EACP,OAAO,EACP,aAAa,GACd,MAAM,SAAS,CAAC;AAGjB,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,YAAY,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1D,OAAO,EAAE,cAAc,IAAI,OAAO,EAAE,MAAM,UAAU,CAAC;AAOrD,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAE5B,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEtD;;;GAGG;AACH,eAAO,MAAM,MAAM,iBAA6C,CAAC;AAwBjE,eAAO,MAAM,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC3G,YAAY,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAG7C,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAG5B,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrD,YAAY,EACV,UAAU,EACV,UAAU,EACV,UAAU,EACV,gBAAgB,EAChB,kBAAkB,EAClB,OAAO,EACP,OAAO,EACP,aAAa,EACb,IAAI,EACJ,QAAQ,EACR,eAAe,EACf,eAAe,GAChB,MAAM,SAAS,CAAC;AAEjB,OAAO,EACL,UAAU,EACV,UAAU,EACV,kBAAkB,GACnB,MAAM,SAAS,CAAC;AAGjB,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,YAAY,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1D,OAAO,EAAE,cAAc,IAAI,OAAO,EAAE,MAAM,UAAU,CAAC;AAOrD,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAE5B,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEtD;;;GAGG;AACH,eAAO,MAAM,MAAM,iBAA6C,CAAC;AAmBjE,eAAO,MAAM,MAAM,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @vnb/mfe-events v0.0.1-rc.6
2
+ * @vnb/mfe-events v0.0.1-rc.8
3
3
  * Micro frontend event plugin for cross-app communication with type safety
4
4
  * License: MIT
5
5
  */
@@ -32,27 +32,47 @@ class MemoryTransport {
32
32
  this.target.addEventListener(event, wrapper);
33
33
  return () => this.target.removeEventListener(event, wrapper);
34
34
  }
35
+ destroy() {
36
+ this.target = new EventTarget();
37
+ }
35
38
  }
36
39
  /**
37
40
  * WindowTransport — MF/NF Remote Apps communication
38
41
  * Uses window.CustomEvent for same-tab communication
39
42
  */
40
43
  class WindowTransport {
44
+ constructor() {
45
+ this.listeners = new Map();
46
+ }
41
47
  emit(event, data) {
42
48
  window.dispatchEvent(new CustomEvent(event, { detail: data }));
43
49
  }
44
50
  on(event, handler) {
45
51
  const wrapper = (e) => handler(e.detail);
46
52
  window.addEventListener(event, wrapper);
47
- return () => window.removeEventListener(event, wrapper);
53
+ this.listeners.set(event, wrapper);
54
+ return () => {
55
+ window.removeEventListener(event, wrapper);
56
+ this.listeners.delete(event);
57
+ };
58
+ }
59
+ destroy() {
60
+ this.listeners.forEach((wrapper, event) => {
61
+ window.removeEventListener(event, wrapper);
62
+ });
63
+ this.listeners.clear();
48
64
  }
49
65
  }
50
66
  /**
51
67
  * PostMessageTransport — iframe Remote Apps communication
52
68
  * Uses window.postMessage for cross-origin communication
69
+ *
70
+ * 注意:Shell 环境下不要使用此Transport(Shell 应使用 IframeProxyTransport)。
71
+ * 此Transport 用于 iframe 子应用向 parent Shell 发送消息。
53
72
  */
54
73
  class PostMessageTransport {
55
74
  constructor(options = {}) {
75
+ this.listeners = new Map();
56
76
  this.targetWindow = options.targetWindow || window.parent;
57
77
  this.targetOrigin = options.targetOrigin || '*';
58
78
  }
@@ -66,7 +86,17 @@ class PostMessageTransport {
66
86
  }
67
87
  };
68
88
  window.addEventListener('message', wrapper);
69
- return () => window.removeEventListener('message', wrapper);
89
+ this.listeners.set(event, wrapper);
90
+ return () => {
91
+ window.removeEventListener('message', wrapper);
92
+ this.listeners.delete(event);
93
+ };
94
+ }
95
+ destroy() {
96
+ this.listeners.forEach((wrapper) => {
97
+ window.removeEventListener('message', wrapper);
98
+ });
99
+ this.listeners.clear();
70
100
  }
71
101
  }
72
102
  /**
@@ -75,6 +105,7 @@ class PostMessageTransport {
75
105
  */
76
106
  class IframeProxyTransport {
77
107
  constructor(iframe, targetOrigin = '*') {
108
+ this.listeners = new Map();
78
109
  this.iframe = iframe;
79
110
  this.targetOrigin = targetOrigin;
80
111
  }
@@ -83,7 +114,6 @@ class IframeProxyTransport {
83
114
  }
84
115
  on(event, handler) {
85
116
  const wrapper = (e) => {
86
- // Only receive messages from this specific iframe
87
117
  if (e.source === this.iframe.contentWindow &&
88
118
  e.data?.scope === 'mfe' &&
89
119
  e.data?.event === event) {
@@ -91,18 +121,20 @@ class IframeProxyTransport {
91
121
  }
92
122
  };
93
123
  window.addEventListener('message', wrapper);
94
- return () => window.removeEventListener('message', wrapper);
124
+ this.listeners.set(event, wrapper);
125
+ return () => {
126
+ window.removeEventListener('message', wrapper);
127
+ this.listeners.delete(event);
128
+ };
129
+ }
130
+ destroy() {
131
+ this.listeners.forEach((wrapper) => {
132
+ window.removeEventListener('message', wrapper);
133
+ });
134
+ this.listeners.clear();
95
135
  }
96
136
  }
97
137
 
98
- var transport = /*#__PURE__*/Object.freeze({
99
- __proto__: null,
100
- IframeProxyTransport: IframeProxyTransport,
101
- MemoryTransport: MemoryTransport,
102
- PostMessageTransport: PostMessageTransport,
103
- WindowTransport: WindowTransport
104
- });
105
-
106
138
  /**
107
139
  * @xxxx/mfe-events — Type-safe Event Bus
108
140
  *
@@ -140,6 +172,12 @@ class Bus {
140
172
  });
141
173
  return off;
142
174
  }
175
+ /**
176
+ * Destroy the bus and clean up all resources
177
+ */
178
+ destroy() {
179
+ this.transport.destroy?.();
180
+ }
143
181
  }
144
182
 
145
183
  /**
@@ -212,18 +250,45 @@ const EvtMfe = {
212
250
  // ─────────────────────────────────────────────
213
251
  function EvtApp(appId, name) {
214
252
  if (!appId || !name) {
215
- console.warn("[EvtApp] appId and name must not be empty");
253
+ console.warn('[EvtApp] appId and name must not be empty');
216
254
  return '';
217
255
  }
218
256
  return `app:${appId}-${name}`;
219
257
  }
220
258
 
259
+ /**
260
+ * @xxxx/mfe-events — Type Definitions
261
+ *
262
+ * Payload types for each event scope.
263
+ * Keys must reference constants from constants.ts.
264
+ */
265
+ /** Shell breakpoint constants */
266
+ const Breakpoint = {
267
+ XS: 'xs',
268
+ SM: 'sm',
269
+ LG: 'lg',
270
+ };
271
+ /** Device type constants */
272
+ const DeviceType = {
273
+ MOBILE: 'mobile',
274
+ TABLET: 'tablet',
275
+ WEB: 'web',
276
+ };
277
+ /** Breakpoint → Device type (mobile<768 / tablet 768-1023 / web≥1024) */
278
+ function breakpointToDevice(bp) {
279
+ if (bp === 'xs')
280
+ return 'mobile';
281
+ if (bp === 'sm')
282
+ return 'tablet';
283
+ return 'web';
284
+ }
285
+
221
286
  /**
222
287
  * @xxxx/mfe-events — Plugin Core
223
288
  *
224
289
  * MfeEventPlugin manages Remote App registration and provides:
225
290
  * - register/unregister for Remote Apps
226
- * - emitTo for targeted communication
291
+ * - emitTo for targeted communication (with 3-tier fallback)
227
292
  * - emit for broadcast communication
228
293
  * - Notification API (success/info/warning/danger)
229
294
  * - Automatic iframe message proxying
@@ -270,6 +335,18 @@ class MfeEventPlugin {
270
335
  }
271
336
  }
272
337
  // ─────────────────────────────────────────
338
+ // Environment Detection
339
+ // ─────────────────────────────────────────
340
+ /**
341
+ * Auto-detect current runtime environment.
342
+ * - iframe (window.self !== window.top) → 'iframe'
343
+ * - top-level window → 'nf' (Native Federation)
344
+ * - Webpack MF sub-app (also top-level) → 'nf', can explicitly pass type:'mf'
345
+ */
346
+ detectType() {
347
+ return typeof window !== 'undefined' && window.self !== window.top ? 'iframe' : 'nf';
348
+ }
349
+ // ─────────────────────────────────────────
273
350
  // Custom Renderer (for Shell integration)
274
351
  // ─────────────────────────────────────────
275
352
  /**
@@ -461,7 +538,7 @@ class MfeEventPlugin {
461
538
  this._notifyItems.delete(id);
462
539
  }
463
540
  this._pendingClosers.delete(id);
464
- // NotifyShell listeners of close — keep DOM and programmatic paths symmetric
541
+ // Dispatch NotifyClose — keep DOM and programmatic paths symmetric
465
542
  window.dispatchEvent(new CustomEvent(EvtMfe.NotifyClose, {
466
543
  detail: { id },
467
544
  bubbles: true,
@@ -477,29 +554,40 @@ class MfeEventPlugin {
477
554
  // ─────────────────────────────────────────
478
555
  /**
479
556
  * Register a Remote App with event handlers
557
+ *
558
+ * `options.type` 可选 —— 不传时自动识别环境:
559
+ * - iframe 内(window.self !== window.top)→ 'iframe':监听 postMessage,
560
+ * 并自动建立下行 message → CustomEvent 桥,同窗自定义事件也能收到。
561
+ * - 顶层 window → 'nf'(Native Federation,监听 CustomEvent);
562
+ * Webpack MF 同窗也是 CustomEvent,可显式传 type:'mf'。
480
563
  */
481
564
  register(options) {
482
565
  if (this.registry.has(options.appId)) {
483
566
  this.unregister(options.appId);
484
567
  }
568
+ const type = options.type || this.detectType();
485
569
  const meta = {
486
570
  appId: options.appId,
487
- type: options.type || 'mf',
571
+ type,
488
572
  };
489
573
  const cleanups = [];
490
574
  // Auto-subscribe handlers
491
575
  Object.entries(options.handlers).forEach(([event, handler]) => {
492
576
  if (!handler)
493
577
  return;
494
- if (options.type === 'iframe') {
495
- // iframe: listen via postMessage
496
- const wrapper = (e) => {
578
+ if (type === 'iframe') {
579
+ // iframe: Shell postMessage 下行 → 监听 message 事件
580
+ const msgWrapper = (e) => {
497
581
  if (e.data?.scope === 'mfe' && e.data?.event === event) {
498
582
  handler(e.data.data);
499
583
  }
500
584
  };
501
- window.addEventListener('message', wrapper);
502
- cleanups.push(() => window.removeEventListener('message', wrapper));
585
+ window.addEventListener('message', msgWrapper);
586
+ cleanups.push(() => window.removeEventListener('message', msgWrapper));
587
+ // Auto-bridge: Shell postMessage → same-tab CustomEvent
588
+ const ceWrapper = (e) => handler(e.detail);
589
+ window.addEventListener(event, ceWrapper);
590
+ cleanups.push(() => window.removeEventListener(event, ceWrapper));
503
591
  }
504
592
  else {
505
593
  // MF/NF: listen via window.CustomEvent
@@ -514,25 +602,12 @@ class MfeEventPlugin {
514
602
  * Register an iframe Remote App (Shell side)
515
603
  */
516
604
  registerIframe(appId, iframe, targetOrigin) {
517
- const TIMEOUT_MS = 5000;
518
- Promise.resolve().then(function () { return transport; })
519
- .then(({ IframeProxyTransport }) => {
520
- const transport = new IframeProxyTransport(iframe, targetOrigin);
521
- this.registry.set(appId, {
522
- meta: { appId, type: 'iframe', transport },
523
- options: { appId, type: 'iframe', handlers: {} },
524
- cleanups: [],
525
- });
526
- })
527
- .catch((err) => {
528
- MfeEventPlugin._logError('registerIframe', 'failed to load transport for', `"${appId}": ${err}`);
605
+ const transport = new IframeProxyTransport(iframe, targetOrigin);
606
+ this.registry.set(appId, {
607
+ meta: { appId, type: 'iframe', transport },
608
+ options: { appId, type: 'iframe', handlers: {} },
609
+ cleanups: [],
529
610
  });
530
- // Timeout protection — if import hangs, registry stays clean and caller knows nothing
531
- setTimeout(() => {
532
- if (!this.registry.has(appId)) {
533
- MfeEventPlugin._logError('registerIframe', 'timeout', `${TIMEOUT_MS}ms for "${appId}"`);
534
- }
535
- }, TIMEOUT_MS);
536
611
  }
537
612
  /**
538
613
  * Unregister a Remote App
@@ -541,47 +616,84 @@ class MfeEventPlugin {
541
616
  const entry = this.registry.get(appId);
542
617
  if (entry) {
543
618
  entry.cleanups.forEach((fn) => fn());
619
+ entry.meta.transport?.destroy?.();
544
620
  }
545
621
  this.registry.delete(appId);
546
622
  }
623
+ /**
624
+ * Unregister all Remote Apps
625
+ */
626
+ unregisterAll() {
627
+ this.registry.forEach((entry) => {
628
+ entry.cleanups.forEach((fn) => fn());
629
+ entry.meta.transport?.destroy?.();
630
+ });
631
+ this.registry.clear();
632
+ }
633
+ /**
634
+ * Check if an app is registered
635
+ */
636
+ has(appId) {
637
+ return this.registry.has(appId);
638
+ }
547
639
  // ─────────────────────────────────────────
548
640
  // Shell Targeted Communication
549
641
  // ─────────────────────────────────────────
550
642
  /**
551
643
  * Send event to a specific Remote App
644
+ *
645
+ * 3-tier fallback:
646
+ * 1. Registry hit → send directly
647
+ * 2. DOM scan → find iframe[data-mfe-app="appId"], create transport on-the-fly
648
+ * 3. Fallback → broadcast with appId for self-filtering
552
649
  */
553
650
  emitTo(appId, event, data) {
651
+ // 1. Registry hit
554
652
  const app = this.registry.get(appId);
555
- if (!app) {
556
- MfeEventPlugin._logWarn('emitTo', 'app not registered', `"${appId}" (event: ${event})`);
653
+ if (app) {
654
+ if (app.meta.type === 'iframe' && app.meta.transport) {
655
+ app.meta.transport.emit(event, data);
656
+ }
657
+ else {
658
+ const handler = app.options.handlers[event];
659
+ if (handler) {
660
+ handler(data);
661
+ }
662
+ }
557
663
  return;
558
664
  }
559
- if (app.meta.type === 'iframe' && app.meta.transport) {
560
- // iframe: send via postMessage
561
- app.meta.transport.emit(event, data);
562
- }
563
- else {
564
- // MF/NF: call the registered handler directly
565
- const handler = app.options.handlers[event];
566
- if (handler) {
567
- handler(data);
568
- }
665
+ // 2. DOM scan: iframe[data-mfe-app="appId"]
666
+ const iframe = document.querySelector(`iframe[data-mfe-app="${appId}"]`);
667
+ if (iframe) {
668
+ const transport = new IframeProxyTransport(iframe);
669
+ this.registry.set(appId, {
670
+ meta: { appId, type: 'iframe', transport },
671
+ options: { appId, type: 'iframe', handlers: {} },
672
+ cleanups: [],
673
+ });
674
+ transport.emit(event, data);
675
+ return;
569
676
  }
677
+ // 3. Fallback: broadcast with appId for self-filtering
678
+ const fallbackData = { ...data };
679
+ fallbackData.appId = appId;
680
+ this.emit(event, fallbackData);
570
681
  }
571
682
  // ─────────────────────────────────────────
572
683
  // Shell Broadcast Communication
573
684
  // ─────────────────────────────────────────
574
685
  /**
575
686
  * Broadcast event to all Remote Apps (MF + iframe)
687
+ *
688
+ * - MF/NF: window CustomEvent
689
+ * - iframe: postMessage to every iframe with data-mfe-app attribute
576
690
  */
577
691
  emit(event, data) {
578
- // Broadcast to all MF Remote Apps
692
+ // Broadcast to all MF/NF Remote Apps via CustomEvent
579
693
  window.dispatchEvent(new CustomEvent(event, { detail: data }));
580
- // Broadcast to all iframe Remote Apps
581
- this.registry.forEach(({ meta }) => {
582
- if (meta.type === 'iframe' && meta.transport) {
583
- meta.transport.emit(event, data);
584
- }
694
+ // Broadcast to all iframes with data-mfe-app attribute
695
+ document.querySelectorAll('iframe[data-mfe-app]').forEach((iframe) => {
696
+ iframe.contentWindow?.postMessage({ scope: 'mfe', event, data, ts: Date.now() }, '*');
585
697
  });
586
698
  }
587
699
  // ─────────────────────────────────────────
@@ -613,11 +725,6 @@ class MfeEventPlugin {
613
725
  }
614
726
  /**
615
727
  * Build the closer function for a notification
616
- *
617
- * Render path determines closer behavior:
618
- * - _customRenderer (Shell): dispatch NotifyClose + customCloser
619
- * - appWithFallback: dispatch NotifyClose + fallback.close()
620
- * - DOM fallback: _removeToast (which dispatches NotifyClose itself)
621
728
  */
622
729
  _buildCloser(id, renderPath, fallbackCloser) {
623
730
  return () => {
@@ -637,7 +744,6 @@ class MfeEventPlugin {
637
744
  fallbackCloser?.close();
638
745
  break;
639
746
  case 'dom':
640
- // _removeToast dispatches NotifyClose itself (symmetric with DOM-driven closes)
641
747
  this._removeToast(id);
642
748
  break;
643
749
  }
@@ -648,7 +754,6 @@ class MfeEventPlugin {
648
754
  * Show a notification
649
755
  *
650
756
  * Flow: dispatch NotifyShow → render (custom > fallback > DOM) → register closer
651
- * The closer is registered exactly once per notification, no overwrite.
652
757
  */
653
758
  notify(config) {
654
759
  if (typeof window === 'undefined')
@@ -668,19 +773,16 @@ class MfeEventPlugin {
668
773
  // 2. Render + register closer (exactly once per path)
669
774
  let closer;
670
775
  if (this._customRenderer) {
671
- // Shell path: custom renderer handles display
672
776
  this._customRenderer(finalConfig);
673
777
  closer = this._buildCloser(id, 'custom');
674
778
  }
675
779
  else {
676
- // Check if any registered app provided a fallback renderer
677
780
  const appWithFallback = [...this.registry.values()].find((app) => app.options.fallback);
678
781
  if (appWithFallback) {
679
782
  const fallbackCloser = appWithFallback.options.fallback(finalConfig);
680
783
  closer = this._buildCloser(id, 'fallback', fallbackCloser);
681
784
  }
682
785
  else {
683
- // DOM fallback
684
786
  this._renderToast(finalConfig);
685
787
  closer = this._buildCloser(id, 'dom');
686
788
  }
@@ -728,21 +830,15 @@ class MfeEventPlugin {
728
830
  }
729
831
  /**
730
832
  * Close all notifications
731
- *
732
- * Snapshot closers before iteration to prevent re-entrancy:
733
- * a closer may dispatch new events that add to _pendingClosers.
734
833
  */
735
834
  closeAll() {
736
- // Snapshot — closers may trigger new notify() calls
737
835
  const closers = [...this._pendingClosers.values()];
738
836
  this._pendingClosers.clear();
739
837
  closers.forEach((closer) => closer());
740
- // Dispatch close-all event
741
838
  window.dispatchEvent(new CustomEvent(EvtMfe.NotifyCloseAll, {
742
839
  detail: {},
743
840
  bubbles: true,
744
841
  }));
745
- // Clear DOM toasts
746
842
  if (this._customClear) {
747
843
  this._customClear();
748
844
  }
@@ -858,16 +954,11 @@ const shlBus = new Bus(new MemoryTransport());
858
954
  */
859
955
  function createMfeBus() {
860
956
  if (typeof window === 'undefined') {
861
- // SSR fallback — use MemoryTransport
862
957
  return new Bus(new MemoryTransport());
863
958
  }
864
959
  if (window.self !== window.top) {
865
- // iframe environment → send to parent via postMessage
866
960
  return new Bus(new PostMessageTransport());
867
961
  }
868
- // Shell/MF/NF environment → use window.CustomEvent
869
- // Federation modules may not share the same instance,
870
- // so window is used as the shared channel fallback
871
962
  return new Bus(new WindowTransport());
872
963
  }
873
964
  const mfeBus = createMfeBus();
@@ -881,7 +972,9 @@ if (typeof window !== 'undefined') {
881
972
  }
882
973
  }
883
974
 
975
+ exports.Breakpoint = Breakpoint;
884
976
  exports.Bus = Bus;
977
+ exports.DeviceType = DeviceType;
885
978
  exports.EvtApp = EvtApp;
886
979
  exports.EvtMfe = EvtMfe;
887
980
  exports.EvtShl = EvtShl;
@@ -889,6 +982,7 @@ exports.IframeProxyTransport = IframeProxyTransport;
889
982
  exports.MemoryTransport = MemoryTransport;
890
983
  exports.PostMessageTransport = PostMessageTransport;
891
984
  exports.WindowTransport = WindowTransport;
985
+ exports.breakpointToDevice = breakpointToDevice;
892
986
  exports.default = mfeEventPlugin;
893
987
  exports.mfeBus = mfeBus;
894
988
  exports.mfeEventPlugin = mfeEventPlugin;