@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 +769 -0
- package/dist/bus.d.ts +29 -0
- package/dist/bus.d.ts.map +1 -0
- package/dist/constants.d.ts +47 -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 +784 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +769 -0
- package/dist/index.mjs.map +1 -0
- package/dist/index.umd.js +790 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/plugin.d.ts +126 -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 +140 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +62 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vnb/mfe-events v1.0.0
|
|
3
|
+
* Micro frontend event plugin for cross-app communication with type safety
|
|
4
|
+
* License: MIT
|
|
5
|
+
*/
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @vnb/mfe-events — Transport Layer
|
|
12
|
+
*
|
|
13
|
+
* Provides abstraction for event transmission across different environments:
|
|
14
|
+
* - MemoryTransport: Shell internal (no window)
|
|
15
|
+
* - WindowTransport: MF/NF Remote Apps (CustomEvent)
|
|
16
|
+
* - PostMessageTransport: iframe Remote Apps (postMessage)
|
|
17
|
+
* - IframeProxyTransport: Shell → iframe proxy (postMessage)
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* MemoryTransport — Shell internal event bus
|
|
21
|
+
* Uses in-memory EventTarget, does not touch window
|
|
22
|
+
*/
|
|
23
|
+
class MemoryTransport {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.target = new EventTarget();
|
|
26
|
+
}
|
|
27
|
+
emit(event, data) {
|
|
28
|
+
this.target.dispatchEvent(new CustomEvent(event, { detail: data }));
|
|
29
|
+
}
|
|
30
|
+
on(event, handler) {
|
|
31
|
+
const wrapper = (e) => handler(e.detail);
|
|
32
|
+
this.target.addEventListener(event, wrapper);
|
|
33
|
+
return () => this.target.removeEventListener(event, wrapper);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* WindowTransport — MF/NF Remote Apps communication
|
|
38
|
+
* Uses window.CustomEvent for same-tab communication
|
|
39
|
+
*/
|
|
40
|
+
class WindowTransport {
|
|
41
|
+
emit(event, data) {
|
|
42
|
+
window.dispatchEvent(new CustomEvent(event, { detail: data }));
|
|
43
|
+
}
|
|
44
|
+
on(event, handler) {
|
|
45
|
+
const wrapper = (e) => handler(e.detail);
|
|
46
|
+
window.addEventListener(event, wrapper);
|
|
47
|
+
return () => window.removeEventListener(event, wrapper);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* PostMessageTransport — iframe Remote Apps communication
|
|
52
|
+
* Uses window.postMessage for cross-origin communication
|
|
53
|
+
*/
|
|
54
|
+
class PostMessageTransport {
|
|
55
|
+
constructor(options = {}) {
|
|
56
|
+
this.targetWindow = options.targetWindow || window.parent;
|
|
57
|
+
this.targetOrigin = options.targetOrigin || '*';
|
|
58
|
+
}
|
|
59
|
+
emit(event, data) {
|
|
60
|
+
this.targetWindow.postMessage({ scope: 'mfe', event, data, ts: Date.now() }, this.targetOrigin);
|
|
61
|
+
}
|
|
62
|
+
on(event, handler) {
|
|
63
|
+
const wrapper = (e) => {
|
|
64
|
+
if (e.data?.scope === 'mfe' && e.data?.event === event) {
|
|
65
|
+
handler(e.data.data);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
window.addEventListener('message', wrapper);
|
|
69
|
+
return () => window.removeEventListener('message', wrapper);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* IframeProxyTransport — Shell side proxy for iframe communication
|
|
74
|
+
* Sends postMessage to specific iframe and receives messages from it
|
|
75
|
+
*/
|
|
76
|
+
class IframeProxyTransport {
|
|
77
|
+
constructor(iframe, targetOrigin = '*') {
|
|
78
|
+
this.iframe = iframe;
|
|
79
|
+
this.targetOrigin = targetOrigin;
|
|
80
|
+
}
|
|
81
|
+
emit(event, data) {
|
|
82
|
+
this.iframe.contentWindow?.postMessage({ scope: 'mfe', event, data, ts: Date.now() }, this.targetOrigin);
|
|
83
|
+
}
|
|
84
|
+
on(event, handler) {
|
|
85
|
+
const wrapper = (e) => {
|
|
86
|
+
// Only receive messages from this specific iframe
|
|
87
|
+
if (e.source === this.iframe.contentWindow &&
|
|
88
|
+
e.data?.scope === 'mfe' &&
|
|
89
|
+
e.data?.event === event) {
|
|
90
|
+
handler(e.data.data);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
window.addEventListener('message', wrapper);
|
|
94
|
+
return () => window.removeEventListener('message', wrapper);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
var transport = /*#__PURE__*/Object.freeze({
|
|
99
|
+
__proto__: null,
|
|
100
|
+
IframeProxyTransport: IframeProxyTransport,
|
|
101
|
+
MemoryTransport: MemoryTransport,
|
|
102
|
+
PostMessageTransport: PostMessageTransport,
|
|
103
|
+
WindowTransport: WindowTransport
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @vnb/mfe-events — Type-safe Event Bus
|
|
108
|
+
*
|
|
109
|
+
* Provides compile-time type checking for event names and payloads.
|
|
110
|
+
* Wraps Transport with generic type constraints.
|
|
111
|
+
*/
|
|
112
|
+
class Bus {
|
|
113
|
+
constructor(transport) {
|
|
114
|
+
this.transport = transport;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Emit an event with type-safe payload
|
|
118
|
+
*/
|
|
119
|
+
emit(event, data) {
|
|
120
|
+
this.transport.emit(event, data);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Subscribe to an event with type-safe handler
|
|
124
|
+
* Returns unsubscribe function
|
|
125
|
+
*/
|
|
126
|
+
on(event, handler, options) {
|
|
127
|
+
if (options?.once) {
|
|
128
|
+
return this.once(event, handler);
|
|
129
|
+
}
|
|
130
|
+
return this.transport.on(event, handler);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Subscribe to an event once
|
|
134
|
+
* Returns unsubscribe function
|
|
135
|
+
*/
|
|
136
|
+
once(event, handler) {
|
|
137
|
+
const off = this.on(event, (data) => {
|
|
138
|
+
off();
|
|
139
|
+
handler(data);
|
|
140
|
+
});
|
|
141
|
+
return off;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @vnb/mfe-events — Event Constants
|
|
147
|
+
*
|
|
148
|
+
* Three scopes:
|
|
149
|
+
* - EvtShl: Shell internal (no window, memory only)
|
|
150
|
+
* - EvtMfe: Cross-app (Shell + Remote Apps)
|
|
151
|
+
* - EvtApp: Remote App internal (framework-specific)
|
|
152
|
+
*
|
|
153
|
+
* Naming convention: {scope}:{domain}-{action}
|
|
154
|
+
* - State/notification events: must have -{action} suffix
|
|
155
|
+
* - Event markers (error, shell-ready, raw): no suffix
|
|
156
|
+
*/
|
|
157
|
+
// ─────────────────────────────────────────────
|
|
158
|
+
// Shell Internal — no window, memory only
|
|
159
|
+
// ─────────────────────────────────────────────
|
|
160
|
+
const EvtShl = {
|
|
161
|
+
TabOverflow: 'shl:tab-overflow',
|
|
162
|
+
TabClose: 'shl:tab-close',
|
|
163
|
+
RouteReuse: 'shl:route-reuse',
|
|
164
|
+
AuthSync: 'shl:auth-sync',
|
|
165
|
+
StateSync: 'shl:state-sync',
|
|
166
|
+
AppMounted: 'shl:app-mounted',
|
|
167
|
+
AppUnmounted: 'shl:app-unmounted',
|
|
168
|
+
};
|
|
169
|
+
// ─────────────────────────────────────────────
|
|
170
|
+
// Cross-App — Shell and Remote Apps shared protocol
|
|
171
|
+
// ─────────────────────────────────────────────
|
|
172
|
+
const EvtMfe = {
|
|
173
|
+
// Global state domain
|
|
174
|
+
ThemeUpdate: 'mfe:theme-update',
|
|
175
|
+
LocaleUpdate: 'mfe:locale-update',
|
|
176
|
+
BreakpointUpdate: 'mfe:breakpoint-update',
|
|
177
|
+
StateUpdate: 'mfe:state-update',
|
|
178
|
+
ShellReady: 'mfe:shell-ready',
|
|
179
|
+
Error: 'mfe:error',
|
|
180
|
+
Raw: 'mfe:raw',
|
|
181
|
+
// Auth domain
|
|
182
|
+
AuthLogin: 'mfe:auth-login',
|
|
183
|
+
AuthLogout: 'mfe:auth-logout',
|
|
184
|
+
AuthTokenUpdate: 'mfe:auth-token-update',
|
|
185
|
+
AuthTokenClear: 'mfe:auth-token-clear',
|
|
186
|
+
AuthPermissionUpdate: 'mfe:auth-permission-update',
|
|
187
|
+
AuthExpired: 'mfe:auth-expired',
|
|
188
|
+
// Route domain
|
|
189
|
+
RouteNavigate: 'mfe:route-navigate',
|
|
190
|
+
RouteRefresh: 'mfe:route-refresh',
|
|
191
|
+
RouteChange: 'mfe:route-change',
|
|
192
|
+
// Lifecycle domain
|
|
193
|
+
LifecycleAppClose: 'mfe:lifecycle-app-close',
|
|
194
|
+
LifecycleAppReady: 'mfe:lifecycle-app-ready',
|
|
195
|
+
LifecycleAppUnmount: 'mfe:lifecycle-app-unmount',
|
|
196
|
+
// Notification domain
|
|
197
|
+
NotifyShow: 'mfe:notify-show',
|
|
198
|
+
NotifyClose: 'mfe:notify-close',
|
|
199
|
+
NotifyCloseAll: 'mfe:notify-close-all',
|
|
200
|
+
};
|
|
201
|
+
// ─────────────────────────────────────────────
|
|
202
|
+
// Remote App Internal — defined by each app
|
|
203
|
+
// ─────────────────────────────────────────────
|
|
204
|
+
function EvtApp(appId, name) {
|
|
205
|
+
return `app:${appId}-${name}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* @vnb/mfe-events — Plugin Core
|
|
210
|
+
*
|
|
211
|
+
* MfeEventPlugin manages Remote App registration and provides:
|
|
212
|
+
* - register/unregister for Remote Apps
|
|
213
|
+
* - emitTo for targeted communication
|
|
214
|
+
* - emit for broadcast communication
|
|
215
|
+
* - Notification API (success/info/warning/danger)
|
|
216
|
+
* - Automatic iframe message proxying
|
|
217
|
+
*/
|
|
218
|
+
class MfeEventPlugin {
|
|
219
|
+
constructor() {
|
|
220
|
+
this.registry = new Map();
|
|
221
|
+
this._notifyId = 0;
|
|
222
|
+
this._shellReady = false;
|
|
223
|
+
this._notifyContainer = null;
|
|
224
|
+
this._notifyItems = new Map();
|
|
225
|
+
this._pendingClosers = new Map();
|
|
226
|
+
this._customRenderer = null;
|
|
227
|
+
this._customCloser = null;
|
|
228
|
+
this._customClear = null;
|
|
229
|
+
if (typeof window === 'undefined')
|
|
230
|
+
return;
|
|
231
|
+
// Shell side: globally listen for iframe messages and forward as CustomEvent
|
|
232
|
+
if (window.self === window.top) {
|
|
233
|
+
window.addEventListener('message', (e) => {
|
|
234
|
+
if (e.data?.scope === 'mfe' && e.data?.event) {
|
|
235
|
+
window.dispatchEvent(new CustomEvent(e.data.event, { detail: e.data.data }));
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
// Always init DOM fallback for notifications
|
|
240
|
+
this._initDOMFallback();
|
|
241
|
+
}
|
|
242
|
+
// ─────────────────────────────────────────
|
|
243
|
+
// Custom Renderer (for Shell integration)
|
|
244
|
+
// ─────────────────────────────────────────
|
|
245
|
+
/**
|
|
246
|
+
* Register custom notification renderer (Shell use)
|
|
247
|
+
* When registered, DOM fallback is disabled
|
|
248
|
+
*/
|
|
249
|
+
useRenderer(render, close, clear) {
|
|
250
|
+
this._customRenderer = render;
|
|
251
|
+
this._customCloser = close || null;
|
|
252
|
+
this._customClear = clear || null;
|
|
253
|
+
}
|
|
254
|
+
// ─────────────────────────────────────────
|
|
255
|
+
// DOM Fallback Renderer
|
|
256
|
+
// ─────────────────────────────────────────
|
|
257
|
+
_initDOMFallback() {
|
|
258
|
+
// Inject styles
|
|
259
|
+
const styleId = 'mfe-notify-styles';
|
|
260
|
+
if (!document.getElementById(styleId)) {
|
|
261
|
+
const style = document.createElement('style');
|
|
262
|
+
style.id = styleId;
|
|
263
|
+
style.textContent = `
|
|
264
|
+
.mfe-notify-host {
|
|
265
|
+
position: fixed;
|
|
266
|
+
top: 16px;
|
|
267
|
+
left: 50%;
|
|
268
|
+
transform: translateX(-50%);
|
|
269
|
+
z-index: 99999;
|
|
270
|
+
display: flex;
|
|
271
|
+
flex-direction: column;
|
|
272
|
+
gap: 8px;
|
|
273
|
+
max-width: 480px;
|
|
274
|
+
width: calc(100% - 32px);
|
|
275
|
+
pointer-events: none;
|
|
276
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
277
|
+
}
|
|
278
|
+
.mfe-notify-item {
|
|
279
|
+
display: flex;
|
|
280
|
+
align-items: flex-start;
|
|
281
|
+
gap: 10px;
|
|
282
|
+
padding: 12px 14px;
|
|
283
|
+
background: #ffffff;
|
|
284
|
+
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
285
|
+
border-left: 4px solid var(--mfe-notify-color, #6b7280);
|
|
286
|
+
border-radius: 8px;
|
|
287
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
|
288
|
+
pointer-events: auto;
|
|
289
|
+
animation: mfe-notify-slide-in 180ms ease-out;
|
|
290
|
+
}
|
|
291
|
+
.mfe-notify-item[data-type="success"] { --mfe-notify-color: #16a34a; }
|
|
292
|
+
.mfe-notify-item[data-type="info"] { --mfe-notify-color: #3b82f6; }
|
|
293
|
+
.mfe-notify-item[data-type="warning"] { --mfe-notify-color: #d97706; }
|
|
294
|
+
.mfe-notify-item[data-type="danger"] { --mfe-notify-color: #dc2626; }
|
|
295
|
+
.mfe-notify-icon {
|
|
296
|
+
flex: 0 0 24px;
|
|
297
|
+
width: 24px;
|
|
298
|
+
height: 24px;
|
|
299
|
+
border-radius: 50%;
|
|
300
|
+
display: flex;
|
|
301
|
+
align-items: center;
|
|
302
|
+
justify-content: center;
|
|
303
|
+
font-size: 14px;
|
|
304
|
+
font-weight: 700;
|
|
305
|
+
color: #fff;
|
|
306
|
+
background: var(--mfe-notify-color, #6b7280);
|
|
307
|
+
}
|
|
308
|
+
.mfe-notify-body { flex: 1; min-width: 0; }
|
|
309
|
+
.mfe-notify-title {
|
|
310
|
+
font-size: 13px;
|
|
311
|
+
font-weight: 600;
|
|
312
|
+
color: #1f2937;
|
|
313
|
+
margin-bottom: 2px;
|
|
314
|
+
}
|
|
315
|
+
.mfe-notify-message {
|
|
316
|
+
font-size: 13px;
|
|
317
|
+
line-height: 1.4;
|
|
318
|
+
color: #374151;
|
|
319
|
+
word-break: break-word;
|
|
320
|
+
}
|
|
321
|
+
.mfe-notify-close {
|
|
322
|
+
flex: 0 0 24px;
|
|
323
|
+
width: 24px;
|
|
324
|
+
height: 24px;
|
|
325
|
+
border: none;
|
|
326
|
+
background: none;
|
|
327
|
+
color: #6b7280;
|
|
328
|
+
cursor: pointer;
|
|
329
|
+
font-size: 18px;
|
|
330
|
+
line-height: 1;
|
|
331
|
+
border-radius: 4px;
|
|
332
|
+
}
|
|
333
|
+
.mfe-notify-close:hover { background: rgba(0, 0, 0, 0.06); color: #111827; }
|
|
334
|
+
.mfe-notify-actions {
|
|
335
|
+
display: flex;
|
|
336
|
+
gap: 8px;
|
|
337
|
+
margin-top: 8px;
|
|
338
|
+
}
|
|
339
|
+
.mfe-notify-btn {
|
|
340
|
+
padding: 4px 12px;
|
|
341
|
+
font-size: 12px;
|
|
342
|
+
font-weight: 500;
|
|
343
|
+
border: 1px solid rgba(0, 0, 0, 0.15);
|
|
344
|
+
border-radius: 4px;
|
|
345
|
+
background: #fff;
|
|
346
|
+
color: #374151;
|
|
347
|
+
cursor: pointer;
|
|
348
|
+
transition: background 0.15s, border-color 0.15s;
|
|
349
|
+
}
|
|
350
|
+
.mfe-notify-btn:hover {
|
|
351
|
+
background: #f3f4f6;
|
|
352
|
+
border-color: rgba(0, 0, 0, 0.25);
|
|
353
|
+
}
|
|
354
|
+
.mfe-notify-btn:active {
|
|
355
|
+
background: #e5e7eb;
|
|
356
|
+
}
|
|
357
|
+
@keyframes mfe-notify-slide-in {
|
|
358
|
+
from { transform: translateY(-12px); opacity: 0; }
|
|
359
|
+
to { transform: translateY(0); opacity: 1; }
|
|
360
|
+
}
|
|
361
|
+
`;
|
|
362
|
+
document.head.appendChild(style);
|
|
363
|
+
}
|
|
364
|
+
// Create container
|
|
365
|
+
this._notifyContainer = document.createElement('div');
|
|
366
|
+
this._notifyContainer.className = 'mfe-notify-host';
|
|
367
|
+
this._notifyContainer.setAttribute('aria-live', 'polite');
|
|
368
|
+
document.body.appendChild(this._notifyContainer);
|
|
369
|
+
}
|
|
370
|
+
_renderToast(config) {
|
|
371
|
+
if (!this._notifyContainer)
|
|
372
|
+
return;
|
|
373
|
+
const { id, type = 'info', message = '', title, duration = 3000, buttons = [] } = config;
|
|
374
|
+
const icons = {
|
|
375
|
+
success: '✓',
|
|
376
|
+
info: 'i',
|
|
377
|
+
warning: '!',
|
|
378
|
+
danger: '✕',
|
|
379
|
+
};
|
|
380
|
+
// Escape HTML to prevent XSS
|
|
381
|
+
const escapeHtml = (str) => {
|
|
382
|
+
const div = document.createElement('div');
|
|
383
|
+
div.textContent = str;
|
|
384
|
+
return div.innerHTML;
|
|
385
|
+
};
|
|
386
|
+
const item = document.createElement('div');
|
|
387
|
+
item.className = 'mfe-notify-item';
|
|
388
|
+
item.setAttribute('data-type', type);
|
|
389
|
+
item.setAttribute('data-notify-id', id);
|
|
390
|
+
// Build buttons HTML
|
|
391
|
+
let buttonsHtml = '';
|
|
392
|
+
if (buttons.length > 0) {
|
|
393
|
+
buttonsHtml = `<div class="mfe-notify-actions">${buttons.map((btn, idx) => `<button class="mfe-notify-btn" data-btn-idx="${idx}">${escapeHtml(btn.text)}</button>`).join('')}</div>`;
|
|
394
|
+
}
|
|
395
|
+
item.innerHTML = `
|
|
396
|
+
<div class="mfe-notify-icon">${icons[type] || 'i'}</div>
|
|
397
|
+
<div class="mfe-notify-body">
|
|
398
|
+
${title ? `<div class="mfe-notify-title">${escapeHtml(title)}</div>` : ''}
|
|
399
|
+
<div class="mfe-notify-message">${escapeHtml(message)}</div>
|
|
400
|
+
${buttonsHtml}
|
|
401
|
+
</div>
|
|
402
|
+
<button class="mfe-notify-close" type="button" aria-label="Dismiss">×</button>
|
|
403
|
+
`;
|
|
404
|
+
// Bind button handlers
|
|
405
|
+
buttons.forEach((btn, idx) => {
|
|
406
|
+
const btnEl = item.querySelector(`[data-btn-idx="${idx}"]`);
|
|
407
|
+
if (btnEl && btn.handler) {
|
|
408
|
+
btnEl.addEventListener('click', () => {
|
|
409
|
+
btn.handler();
|
|
410
|
+
if (btn.role !== 'sticky') {
|
|
411
|
+
this._removeToast(id);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
const closeBtn = item.querySelector('.mfe-notify-close');
|
|
417
|
+
closeBtn.addEventListener('click', () => this._removeToast(id));
|
|
418
|
+
this._notifyContainer.appendChild(item);
|
|
419
|
+
this._notifyItems.set(id, item);
|
|
420
|
+
if (duration > 0) {
|
|
421
|
+
setTimeout(() => this._removeToast(id), duration);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
_removeToast(id) {
|
|
425
|
+
const item = this._notifyItems.get(id);
|
|
426
|
+
if (item) {
|
|
427
|
+
item.remove();
|
|
428
|
+
this._notifyItems.delete(id);
|
|
429
|
+
}
|
|
430
|
+
this._pendingClosers.delete(id);
|
|
431
|
+
}
|
|
432
|
+
_removeAllToasts() {
|
|
433
|
+
this._notifyItems.forEach((item) => item.remove());
|
|
434
|
+
this._notifyItems.clear();
|
|
435
|
+
this._pendingClosers.clear();
|
|
436
|
+
}
|
|
437
|
+
// ─────────────────────────────────────────
|
|
438
|
+
// Remote App Registration
|
|
439
|
+
// ─────────────────────────────────────────
|
|
440
|
+
/**
|
|
441
|
+
* Register a Remote App with event handlers
|
|
442
|
+
*/
|
|
443
|
+
register(options) {
|
|
444
|
+
if (this.registry.has(options.appId)) {
|
|
445
|
+
this.unregister(options.appId);
|
|
446
|
+
}
|
|
447
|
+
const meta = {
|
|
448
|
+
appId: options.appId,
|
|
449
|
+
type: options.type || 'mf',
|
|
450
|
+
};
|
|
451
|
+
const cleanups = [];
|
|
452
|
+
// Auto-subscribe handlers
|
|
453
|
+
Object.entries(options.handlers).forEach(([event, handler]) => {
|
|
454
|
+
if (!handler)
|
|
455
|
+
return;
|
|
456
|
+
if (options.type === 'iframe') {
|
|
457
|
+
// iframe: listen via postMessage
|
|
458
|
+
const wrapper = (e) => {
|
|
459
|
+
if (e.data?.scope === 'mfe' && e.data?.event === event) {
|
|
460
|
+
handler(e.data.data);
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
window.addEventListener('message', wrapper);
|
|
464
|
+
cleanups.push(() => window.removeEventListener('message', wrapper));
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
// MF/NF: listen via window.CustomEvent
|
|
468
|
+
const wrapper = (e) => handler(e.detail);
|
|
469
|
+
window.addEventListener(event, wrapper);
|
|
470
|
+
cleanups.push(() => window.removeEventListener(event, wrapper));
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
this.registry.set(options.appId, { meta, options, cleanups });
|
|
474
|
+
// fallback 存入 registry,不覆盖全局 _customRenderer
|
|
475
|
+
// 全局渲染器仅由 Shell 的 useRenderer() 设置
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Register an iframe Remote App (Shell side)
|
|
479
|
+
*/
|
|
480
|
+
registerIframe(appId, iframe, targetOrigin) {
|
|
481
|
+
// Lazy import to avoid bundling issues
|
|
482
|
+
Promise.resolve().then(function () { return transport; }).then(({ IframeProxyTransport }) => {
|
|
483
|
+
const transport = new IframeProxyTransport(iframe, targetOrigin);
|
|
484
|
+
this.registry.set(appId, {
|
|
485
|
+
meta: { appId, type: 'iframe', transport },
|
|
486
|
+
options: { appId, type: 'iframe', handlers: {} },
|
|
487
|
+
cleanups: [],
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Unregister a Remote App
|
|
493
|
+
*/
|
|
494
|
+
unregister(appId) {
|
|
495
|
+
const entry = this.registry.get(appId);
|
|
496
|
+
if (entry) {
|
|
497
|
+
entry.cleanups.forEach((fn) => fn());
|
|
498
|
+
}
|
|
499
|
+
this.registry.delete(appId);
|
|
500
|
+
}
|
|
501
|
+
// ─────────────────────────────────────────
|
|
502
|
+
// Shell Targeted Communication
|
|
503
|
+
// ─────────────────────────────────────────
|
|
504
|
+
/**
|
|
505
|
+
* Send event to a specific Remote App
|
|
506
|
+
*/
|
|
507
|
+
emitTo(appId, event, data) {
|
|
508
|
+
const app = this.registry.get(appId);
|
|
509
|
+
if (!app)
|
|
510
|
+
return;
|
|
511
|
+
if (app.meta.type === 'iframe' && app.meta.transport) {
|
|
512
|
+
// iframe: send via postMessage
|
|
513
|
+
app.meta.transport.emit(event, data);
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
// MF: call handler directly or dispatch CustomEvent
|
|
517
|
+
const handler = app.options.handlers[event];
|
|
518
|
+
if (handler) {
|
|
519
|
+
handler(data);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// ─────────────────────────────────────────
|
|
524
|
+
// Shell Broadcast Communication
|
|
525
|
+
// ─────────────────────────────────────────
|
|
526
|
+
/**
|
|
527
|
+
* Broadcast event to all Remote Apps (MF + iframe)
|
|
528
|
+
*/
|
|
529
|
+
emit(event, data) {
|
|
530
|
+
// Broadcast to all MF Remote Apps
|
|
531
|
+
window.dispatchEvent(new CustomEvent(event, { detail: data }));
|
|
532
|
+
// Broadcast to all iframe Remote Apps
|
|
533
|
+
this.registry.forEach(({ meta }) => {
|
|
534
|
+
if (meta.type === 'iframe' && meta.transport) {
|
|
535
|
+
meta.transport.emit(event, data);
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
// ─────────────────────────────────────────
|
|
540
|
+
// Notification API
|
|
541
|
+
// ─────────────────────────────────────────
|
|
542
|
+
/**
|
|
543
|
+
* Build notification config from flexible arguments
|
|
544
|
+
* Supports: success("msg"), success({message:"msg",duration:5000}), success("msg","title",{duration:5000})
|
|
545
|
+
*/
|
|
546
|
+
_buildConfig(type, message, args) {
|
|
547
|
+
const config = { type };
|
|
548
|
+
if (typeof message === 'string') {
|
|
549
|
+
config.message = message;
|
|
550
|
+
}
|
|
551
|
+
else if (message && typeof message === 'object') {
|
|
552
|
+
Object.assign(config, message);
|
|
553
|
+
}
|
|
554
|
+
args.forEach((arg) => {
|
|
555
|
+
if (typeof arg === 'string') {
|
|
556
|
+
config.title = arg;
|
|
557
|
+
}
|
|
558
|
+
else if (arg && typeof arg === 'object') {
|
|
559
|
+
Object.assign(config, arg);
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
return config;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Show a notification
|
|
566
|
+
*/
|
|
567
|
+
notify(config) {
|
|
568
|
+
if (typeof window === 'undefined')
|
|
569
|
+
return { close: () => { } };
|
|
570
|
+
const id = `mfe-${++this._notifyId}`;
|
|
571
|
+
const finalConfig = {
|
|
572
|
+
duration: 3000,
|
|
573
|
+
...config,
|
|
574
|
+
id,
|
|
575
|
+
};
|
|
576
|
+
// 1. Always dispatch CustomEvent (for Shell listeners)
|
|
577
|
+
window.dispatchEvent(new CustomEvent(EvtMfe.NotifyShow, {
|
|
578
|
+
detail: finalConfig,
|
|
579
|
+
bubbles: true,
|
|
580
|
+
cancelable: true,
|
|
581
|
+
}));
|
|
582
|
+
// 2. Render DOM fallback (or use custom renderer)
|
|
583
|
+
if (this._customRenderer) {
|
|
584
|
+
this._customRenderer(finalConfig);
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
this._renderToast(finalConfig);
|
|
588
|
+
}
|
|
589
|
+
// 3. Track closer for closeAll()
|
|
590
|
+
const closeFn = () => {
|
|
591
|
+
// Dispatch close event
|
|
592
|
+
window.dispatchEvent(new CustomEvent(EvtMfe.NotifyClose, {
|
|
593
|
+
detail: { id },
|
|
594
|
+
bubbles: true,
|
|
595
|
+
}));
|
|
596
|
+
// Remove DOM toast
|
|
597
|
+
if (this._customCloser) {
|
|
598
|
+
this._customCloser(id);
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
this._removeToast(id);
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
this._pendingClosers.set(id, closeFn);
|
|
605
|
+
return {
|
|
606
|
+
close: () => {
|
|
607
|
+
const closer = this._pendingClosers.get(id);
|
|
608
|
+
if (closer) {
|
|
609
|
+
closer();
|
|
610
|
+
this._pendingClosers.delete(id);
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Show success notification
|
|
617
|
+
*/
|
|
618
|
+
success(message, ...args) {
|
|
619
|
+
return this.notify(this._buildConfig('success', message, args));
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Show info notification
|
|
623
|
+
*/
|
|
624
|
+
info(message, ...args) {
|
|
625
|
+
return this.notify(this._buildConfig('info', message, args));
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Show warning notification
|
|
629
|
+
*/
|
|
630
|
+
warning(message, ...args) {
|
|
631
|
+
return this.notify(this._buildConfig('warning', message, args));
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Show danger notification
|
|
635
|
+
*/
|
|
636
|
+
danger(message, ...args) {
|
|
637
|
+
return this.notify(this._buildConfig('danger', message, args));
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Close all notifications
|
|
641
|
+
*/
|
|
642
|
+
closeAll() {
|
|
643
|
+
// Call all pending closers
|
|
644
|
+
this._pendingClosers.forEach((closer) => closer());
|
|
645
|
+
this._pendingClosers.clear();
|
|
646
|
+
// Dispatch close-all event
|
|
647
|
+
window.dispatchEvent(new CustomEvent(EvtMfe.NotifyCloseAll, {
|
|
648
|
+
detail: {},
|
|
649
|
+
bubbles: true,
|
|
650
|
+
}));
|
|
651
|
+
// Clear DOM toasts
|
|
652
|
+
if (this._customClear) {
|
|
653
|
+
this._customClear();
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
this._removeAllToasts();
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
// ─────────────────────────────────────────
|
|
660
|
+
// Lifecycle Helpers
|
|
661
|
+
// ─────────────────────────────────────────
|
|
662
|
+
/**
|
|
663
|
+
* Mark Shell as ready
|
|
664
|
+
*/
|
|
665
|
+
shellReady() {
|
|
666
|
+
setTimeout(() => {
|
|
667
|
+
window.dispatchEvent(new CustomEvent(EvtMfe.ShellReady, {
|
|
668
|
+
detail: {},
|
|
669
|
+
bubbles: true,
|
|
670
|
+
cancelable: true,
|
|
671
|
+
}));
|
|
672
|
+
this._shellReady = true;
|
|
673
|
+
}, 100);
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Check if Shell is ready
|
|
677
|
+
*/
|
|
678
|
+
isShellReady() {
|
|
679
|
+
return !!this._shellReady;
|
|
680
|
+
}
|
|
681
|
+
// ─────────────────────────────────────────
|
|
682
|
+
// Auth Helpers (convenience methods)
|
|
683
|
+
// ─────────────────────────────────────────
|
|
684
|
+
/**
|
|
685
|
+
* Broadcast auth login event
|
|
686
|
+
*/
|
|
687
|
+
authLogin(user) {
|
|
688
|
+
this.emit(EvtMfe.AuthLogin, { user, token: '' });
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Broadcast auth logout event
|
|
692
|
+
*/
|
|
693
|
+
authLogout(reason) {
|
|
694
|
+
this.emit(EvtMfe.AuthLogout, { reason: reason || 'user' });
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Broadcast token update event
|
|
698
|
+
*/
|
|
699
|
+
tokenUpdate(data) {
|
|
700
|
+
this.emit(EvtMfe.AuthTokenUpdate, data);
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Broadcast auth expired event
|
|
704
|
+
*/
|
|
705
|
+
authExpired() {
|
|
706
|
+
this.emit(EvtMfe.AuthExpired, {});
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Broadcast locale update event
|
|
710
|
+
*/
|
|
711
|
+
locale(locale) {
|
|
712
|
+
this.emit(EvtMfe.LocaleUpdate, { locale });
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
// Singleton export
|
|
716
|
+
const mfeEventPlugin = new MfeEventPlugin();
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* @vnb/mfe-events — Micro Frontend Event Plugin
|
|
720
|
+
*
|
|
721
|
+
* Unified event communication for Module Federation / Native Federation / iframe architectures.
|
|
722
|
+
*
|
|
723
|
+
* Features:
|
|
724
|
+
* - Three scopes: EvtShl (Shell internal), EvtMfe (cross-app), EvtApp (Remote App internal)
|
|
725
|
+
* - Type-safe event bus with compile-time checking
|
|
726
|
+
* - Automatic Transport adaptation (CustomEvent for MF, postMessage for iframe)
|
|
727
|
+
* - Plugin API for Remote App registration and communication
|
|
728
|
+
* - Notification API with fallback support
|
|
729
|
+
*
|
|
730
|
+
* Usage:
|
|
731
|
+
* ```typescript
|
|
732
|
+
* import mfeEventPlugin, { mfeBus, shlBus } from '@vnb/mfe-events'
|
|
733
|
+
* import { EvtMfe, EvtShl, EvtApp } from '@vnb/mfe-events'
|
|
734
|
+
* ```
|
|
735
|
+
*/
|
|
736
|
+
// Transport layer
|
|
737
|
+
/**
|
|
738
|
+
* Shell internal bus — isolated, does not touch window
|
|
739
|
+
* Use for Shell-internal state synchronization
|
|
740
|
+
*/
|
|
741
|
+
const shlBus = new Bus(new MemoryTransport());
|
|
742
|
+
/**
|
|
743
|
+
* Cross-app bus — auto-detects environment and selects Transport
|
|
744
|
+
* - Shell/MF/NF: WindowTransport (CustomEvent)
|
|
745
|
+
* - iframe: PostMessageTransport (postMessage)
|
|
746
|
+
*/
|
|
747
|
+
function createMfeBus() {
|
|
748
|
+
if (typeof window === 'undefined') {
|
|
749
|
+
// SSR fallback — use MemoryTransport
|
|
750
|
+
return new Bus(new MemoryTransport());
|
|
751
|
+
}
|
|
752
|
+
if (window.self !== window.top) {
|
|
753
|
+
// iframe environment → send to parent via postMessage
|
|
754
|
+
return new Bus(new PostMessageTransport());
|
|
755
|
+
}
|
|
756
|
+
// Shell/MF/NF environment → use window.CustomEvent
|
|
757
|
+
// Federation modules may not share the same instance,
|
|
758
|
+
// so window is used as the shared channel fallback
|
|
759
|
+
return new Bus(new WindowTransport());
|
|
760
|
+
}
|
|
761
|
+
const mfeBus = createMfeBus();
|
|
762
|
+
// ─────────────────────────────────────────────
|
|
763
|
+
// Global variable for backward compatibility
|
|
764
|
+
// ─────────────────────────────────────────────
|
|
765
|
+
if (typeof window !== 'undefined') {
|
|
766
|
+
const win = window;
|
|
767
|
+
if (!win.__MFE_EVENT_PLUGIN__) {
|
|
768
|
+
win.__MFE_EVENT_PLUGIN__ = mfeEventPlugin;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
exports.Bus = Bus;
|
|
773
|
+
exports.EvtApp = EvtApp;
|
|
774
|
+
exports.EvtMfe = EvtMfe;
|
|
775
|
+
exports.EvtShl = EvtShl;
|
|
776
|
+
exports.IframeProxyTransport = IframeProxyTransport;
|
|
777
|
+
exports.MemoryTransport = MemoryTransport;
|
|
778
|
+
exports.PostMessageTransport = PostMessageTransport;
|
|
779
|
+
exports.WindowTransport = WindowTransport;
|
|
780
|
+
exports.default = mfeEventPlugin;
|
|
781
|
+
exports.mfeBus = mfeBus;
|
|
782
|
+
exports.mfeEventPlugin = mfeEventPlugin;
|
|
783
|
+
exports.shlBus = shlBus;
|
|
784
|
+
//# sourceMappingURL=index.js.map
|