@dino-dev/bridge-core-ts 0.0.4 → 0.0.5

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/src/mrp-bridge.ts CHANGED
@@ -17,22 +17,24 @@
17
17
  * ```
18
18
  */
19
19
 
20
+ import { installIframeHostBridge } from './iframe-host';
21
+
20
22
  // ============================================================
21
23
  // 类型定义
22
24
  // ============================================================
23
25
 
24
26
  /** Bridge 消息格式 */
25
27
  export interface BridgeMessage<T = Record<string, unknown>> {
26
- type: string;
27
- action: string;
28
- payload: T;
29
- messageId?: string;
30
- timestamp: number;
28
+ type: string;
29
+ action: string;
30
+ payload: T;
31
+ messageId?: string;
32
+ timestamp: number;
31
33
  }
32
34
 
33
35
  /** 消息处理函数 */
34
36
  export type MessageHandler<T = Record<string, unknown>> = (
35
- message: BridgeMessage<T>,
37
+ message: BridgeMessage<T>,
36
38
  ) => void;
37
39
 
38
40
  /** 取消监听函数 */
@@ -43,24 +45,24 @@ export type Unsubscribe = () => void;
43
45
  // ============================================================
44
46
 
45
47
  declare global {
46
- interface Window {
47
- /** Bridge SDK 单例 */
48
- mrpBridge: MrpBridge;
49
- /** Flutter 内部 Bridge 接口 */
50
- __FLUTTER_BRIDGE__?: {
51
- send(
52
- type: string,
53
- action: string,
54
- payload: Record<string, unknown>,
55
- ): Promise<unknown>;
56
- onMessage(jsonString: string): void;
57
- };
58
- /** 宿主注册的原生模块(按命名空间分组) */
59
- NativeModules?: Record<
60
- string,
61
- Record<string, (...args: unknown[]) => Promise<unknown>>
62
- >;
63
- }
48
+ interface Window {
49
+ /** Bridge SDK 单例 */
50
+ mrpBridge: MrpBridge;
51
+ /** Flutter 内部 Bridge 接口 */
52
+ __FLUTTER_BRIDGE__?: {
53
+ send(
54
+ type: string,
55
+ action: string,
56
+ payload: Record<string, unknown>,
57
+ ): Promise<unknown>;
58
+ onMessage(jsonString: string): void;
59
+ };
60
+ /** 宿主注册的原生模块(按命名空间分组) */
61
+ NativeModules?: Record<
62
+ string,
63
+ Record<string, (...args: unknown[]) => Promise<unknown>>
64
+ >;
65
+ }
64
66
  }
65
67
 
66
68
  // ============================================================
@@ -68,17 +70,32 @@ declare global {
68
70
  // ============================================================
69
71
 
70
72
  interface WrappedHandler extends MessageHandler {
71
- _original?: MessageHandler;
72
- _type?: string;
73
- _action?: string | null;
73
+ _original?: MessageHandler;
74
+ _type?: string;
75
+ _action?: string | null;
74
76
  }
75
77
 
76
78
  interface PendingMessage {
77
- type: string;
78
- action: string;
79
- payload: Record<string, unknown>;
79
+ type: string;
80
+ action: string;
81
+ payload: Record<string, unknown>;
82
+ }
83
+
84
+ /** Bridge 就绪状态 */
85
+ export type BridgeReadyState = 'pending' | 'ready' | 'unavailable';
86
+
87
+ /** `whenReady()` 选项 */
88
+ export interface WhenReadyOptions {
89
+ /** 超时毫秒数,超时后 Promise 以错误 reject;不传则沿用「永不 reject」的旧行为 */
90
+ timeoutMs?: number;
80
91
  }
81
92
 
93
+ const READY_POLL_INTERVAL_MS = 100;
94
+ const READY_MAX_RETRY = 50;
95
+ // 容器在 onLoadStop(window.onload,需等图片/iframe 等子资源加载完)后才注入
96
+ // __FLUTTER_BRIDGE__,弱网下可能远超 5 秒;已在 WebView 内时不能放弃,否则排队消息全部丢失。
97
+ const READY_SLOW_POLL_INTERVAL_MS = 250;
98
+
82
99
  // ============================================================
83
100
  // MrpBridge
84
101
  // ============================================================
@@ -91,197 +108,267 @@ interface PendingMessage {
91
108
  * 2. **模块调用**:通过 `module<T>()` 获取类型安全的原生模块代理
92
109
  */
93
110
  export class MrpBridge {
94
- private _messageHandlers: WrappedHandler[] = [];
95
- private _ready = false;
96
- private _pendingMessages: PendingMessage[] = [];
97
- private _readyPromiseResolve: (() => void) | null = null;
98
- private _readyPromise: Promise<void>;
99
- private _retryCount = 0;
100
-
101
- constructor() {
102
- this._readyPromise = new Promise<void>((resolve) => {
103
- this._readyPromiseResolve = resolve;
104
- });
105
-
106
- // 监听 Flutter → H5 消息
107
- window.addEventListener('flutterMessage', ((event: CustomEvent) => {
108
- const msg = event.detail as BridgeMessage;
109
- // 复制一份避免迭代中修改
110
- const handlers = [...this._messageHandlers];
111
- for (const handler of handlers) {
112
- try {
113
- handler(msg);
114
- } catch (e) {
115
- console.error('[MRP Bridge] 消息处理错误:', e);
116
- }
117
- }
118
- }) as EventListener);
119
-
120
- this._checkReady();
121
- }
122
-
123
- // ---- 就绪检测 ----
124
-
125
- private _checkReady(): void {
126
- if (window.__FLUTTER_BRIDGE__) {
127
- this._ready = true;
128
- this._readyPromiseResolve?.();
129
- this._pendingMessages.forEach((msg) => this._doSend(msg));
130
- this._pendingMessages = [];
131
- } else {
132
- this._retryCount++;
133
- if (this._retryCount < 50) {
134
- setTimeout(() => this._checkReady(), 100);
135
- } else {
136
- console.warn(
137
- '[MRP Bridge] Flutter Bridge 未注入,可能不在容器环境中运行',
138
- );
139
- }
140
- }
141
- }
142
-
143
- /** 等待 Bridge 就绪 */
144
- whenReady(): Promise<void> {
145
- return this._readyPromise;
146
- }
147
-
148
- /** 是否在 Flutter WebView 容器中运行 */
149
- get isInContainer(): boolean {
150
- return !!window.__FLUTTER_BRIDGE__ || !!(window as any).flutter_inappwebview;
151
- }
152
-
153
- // ---- 消息发送 ----
154
-
155
- /**
156
- * 发送消息到 Flutter
157
- * @param type - 消息类型
158
- * @param action - 动作名称
159
- * @param payload - 消息数据
160
- */
161
- send<R = unknown>(
162
- type: string,
163
- action: string,
164
- payload: Record<string, unknown> = {},
165
- ): Promise<R> {
166
- const msg: PendingMessage = { type, action, payload };
167
- if (this._ready) {
168
- return this._doSend(msg);
169
- }
170
- this._pendingMessages.push(msg);
171
- return Promise.resolve(null as R);
172
- }
173
-
174
- private _doSend<R>(msg: PendingMessage): Promise<R> {
175
- if (window.__FLUTTER_BRIDGE__) {
176
- return window.__FLUTTER_BRIDGE__.send(
177
- msg.type,
178
- msg.action,
179
- msg.payload,
180
- ) as Promise<R>;
181
- }
182
- return Promise.resolve(null as R);
183
- }
184
-
185
- // ---- 消息监听 ----
186
-
187
- /**
188
- * 监听 Flutter 发来的所有消息
189
- * @returns 取消监听的函数
190
- */
191
- onMessage<T = Record<string, unknown>>(
192
- handler: MessageHandler<T>,
193
- ): Unsubscribe {
194
- this._messageHandlers.push(handler as WrappedHandler);
195
- return () => {
196
- this._messageHandlers = this._messageHandlers.filter(
197
- (h) => h !== handler,
198
- );
199
- };
200
- }
201
-
202
- /**
203
- * 监听特定类型/动作的消息
204
- * @param type - 消息类型
205
- * @param action - 动作名称,传 null 匹配所有
206
- */
207
- on<T = Record<string, unknown>>(
208
- type: string,
209
- action: string | null,
210
- handler: MessageHandler<T>,
211
- ): Unsubscribe {
212
- const wrappedHandler: WrappedHandler = (msg) => {
213
- if (msg.type === type && (action === null || msg.action === action)) {
214
- (handler as MessageHandler)(msg);
215
- }
216
- };
217
- wrappedHandler._original = handler as MessageHandler;
218
- wrappedHandler._type = type;
219
- wrappedHandler._action = action;
220
- this._messageHandlers.push(wrappedHandler);
221
- return () => {
222
- this._messageHandlers = this._messageHandlers.filter(
223
- (h) => h !== wrappedHandler,
224
- );
225
- };
226
- }
227
-
228
- // ---- 原生模块调用 ----
229
-
230
- /**
231
- * 获取模块代理,按命名空间调用原生 API(类型安全)。
232
- *
233
- * 搭配 TypeScript Generator 生成的类型使用,获得完整的类型推断。
234
- *
235
- * @param namespace - 模块命名空间
236
- * @returns 模块方法代理对象
237
- *
238
- * @example
239
- * ```ts
240
- * import type { AudioModule } from '../generated/audio_module';
241
- * import type { UserModule } from '../generated/user_module';
242
- *
243
- * const audio = bridge.module<AudioModule>('audio');
244
- * await audio.play({ soundId: 'success', volume: 0.8 });
245
- *
246
- * const user = bridge.module<UserModule>('user');
247
- * const profile = await user.getProfile();
248
- * ```
249
- */
250
- module<
251
- T extends object = Record<
252
- string,
253
- (...args: any[]) => Promise<any>
254
- >,
255
- >(namespace: string): T {
256
- return new Proxy({} as T, {
257
- get: (_target, method: string | symbol) => {
258
- if (typeof method !== 'string') return undefined;
259
- return async (...args: unknown[]) => {
260
- if (window.NativeModules?.[namespace]?.[method]) {
261
- const result = await window.NativeModules[namespace][method](...args);
262
- // 检查模块访问被拒绝错误
263
- if (
264
- result &&
265
- typeof result === 'object' &&
266
- (result as Record<string, unknown>).__error__ === 'MODULE_ACCESS_DENIED'
267
- ) {
268
- console.error(
269
- `[MRP Bridge] 模块访问被拒绝: ${namespace}.${method}`,
270
- (result as Record<string, unknown>).__message__,
271
- );
272
- return null;
273
- }
274
- return result;
275
- }
276
- console.warn(
277
- `[MRP Bridge] NativeModules.${namespace}.${method} 不存在`,
278
- );
279
- return Promise.resolve(null);
280
- };
281
- },
282
- });
283
- }
111
+ private _messageHandlers: WrappedHandler[] = [];
112
+ private _ready = false;
113
+ private _pendingMessages: PendingMessage[] = [];
114
+ private _readyPromiseResolve: (() => void) | null = null;
115
+ private _readyPromise: Promise<void>;
116
+ private _retryCount = 0;
117
+ private _readyState: BridgeReadyState = 'pending';
118
+ private _readyTimer: ReturnType<typeof setTimeout> | null = null;
119
+
120
+ constructor() {
121
+ this._readyPromise = new Promise<void>((resolve) => {
122
+ this._readyPromiseResolve = resolve;
123
+ });
124
+
125
+ // 监听 Flutter → H5 消息
126
+ window.addEventListener('flutterMessage', ((event: CustomEvent) => {
127
+ const msg = event.detail as BridgeMessage;
128
+ // 复制一份避免迭代中修改
129
+ const handlers = [...this._messageHandlers];
130
+ for (const handler of handlers) {
131
+ try {
132
+ handler(msg);
133
+ } catch (e) {
134
+ console.error('[MRP Bridge] 消息处理错误:', e);
135
+ }
136
+ }
137
+ }) as EventListener);
138
+
139
+ this._checkReady();
140
+ }
141
+
142
+ // ---- 就绪检测 ----
143
+
144
+ private _checkReady(): void {
145
+ if (this._readyState === 'ready') return;
146
+
147
+ if (window.__FLUTTER_BRIDGE__) {
148
+ if (this._readyTimer !== null) {
149
+ clearTimeout(this._readyTimer);
150
+ this._readyTimer = null;
151
+ }
152
+ this._ready = true;
153
+ this._readyState = 'ready';
154
+ this._readyPromiseResolve?.();
155
+ this._pendingMessages.forEach((msg) => this._doSend(msg));
156
+ this._pendingMessages = [];
157
+ return;
158
+ }
159
+
160
+ this._retryCount++;
161
+ if (this._retryCount < READY_MAX_RETRY) {
162
+ this._readyTimer = setTimeout(
163
+ () => this._checkReady(),
164
+ READY_POLL_INTERVAL_MS,
165
+ );
166
+ return;
167
+ }
168
+
169
+ if ((window as any).flutter_inappwebview) {
170
+ this._readyTimer = setTimeout(
171
+ () => this._checkReady(),
172
+ READY_SLOW_POLL_INTERVAL_MS,
173
+ );
174
+ return;
175
+ }
176
+
177
+ this._readyTimer = null;
178
+ this._readyState = 'unavailable';
179
+ console.error(
180
+ [
181
+ '[MRP Bridge] 等待 %ss 后仍未检测到原生容器(window.__FLUTTER_BRIDGE__)。',
182
+ '· bridge.whenReady() 将永不 resolve,依赖它的页面会一直停在加载态;',
183
+ '· bridge.send() 与原生模块调用都会静默返回 null。',
184
+ '如果当前是在浏览器里开发,请安装 Mock Bridge:',
185
+ " import { installMockBridge } from '@h5-container/bridge-core-ts/mock';",
186
+ ].join('\n'),
187
+ ((READY_MAX_RETRY * READY_POLL_INTERVAL_MS) / 1000).toFixed(1),
188
+ );
189
+ }
190
+
191
+ /**
192
+ * 重新触发一次就绪检测。
193
+ *
194
+ * 供 Mock Bridge 在注入后立即生效使用;轮询已结束时也能重新唤醒。
195
+ */
196
+ refreshReady(): void {
197
+ if (this._readyState === 'ready') return;
198
+ this._retryCount = 0;
199
+ this._readyState = 'pending';
200
+ this._checkReady();
201
+ }
202
+
203
+ /** 当前就绪状态:等待中 / 已就绪 / 判定为不在容器环境 */
204
+ get readyState(): BridgeReadyState {
205
+ return this._readyState;
206
+ }
207
+
208
+ /**
209
+ * 等待 Bridge 就绪。
210
+ *
211
+ * 默认沿用旧语义:未就绪时 Promise 一直挂起、不会 reject。
212
+ * 传入 `timeoutMs` 可在超时后 reject,便于页面给出明确的错误态。
213
+ */
214
+ whenReady(options?: WhenReadyOptions): Promise<void> {
215
+ const timeoutMs = options?.timeoutMs;
216
+ if (timeoutMs === undefined) return this._readyPromise;
217
+
218
+ return new Promise<void>((resolve, reject) => {
219
+ const timer = setTimeout(() => {
220
+ reject(
221
+ new Error(
222
+ `[MRP Bridge] 等待原生容器就绪超时(${timeoutMs}ms)。` +
223
+ '当前可能不在容器环境中,浏览器开发请安装 Mock Bridge。',
224
+ ),
225
+ );
226
+ }, timeoutMs);
227
+ void this._readyPromise.then(() => {
228
+ clearTimeout(timer);
229
+ resolve();
230
+ });
231
+ });
232
+ }
233
+
234
+ /** 是否在 Flutter WebView 容器中运行 */
235
+ get isInContainer(): boolean {
236
+ return (
237
+ !!window.__FLUTTER_BRIDGE__ || !!(window as any).flutter_inappwebview
238
+ );
239
+ }
240
+
241
+ // ---- 消息发送 ----
242
+
243
+ /**
244
+ * 发送消息到 Flutter
245
+ * @param type - 消息类型
246
+ * @param action - 动作名称
247
+ * @param payload - 消息数据
248
+ */
249
+ send<R = unknown>(
250
+ type: string,
251
+ action: string,
252
+ payload: Record<string, unknown> = {},
253
+ ): Promise<R> {
254
+ const msg: PendingMessage = { type, action, payload };
255
+ if (this._ready) {
256
+ return this._doSend(msg);
257
+ }
258
+ this._pendingMessages.push(msg);
259
+ return Promise.resolve(null as R);
260
+ }
261
+
262
+ private _doSend<R>(msg: PendingMessage): Promise<R> {
263
+ if (window.__FLUTTER_BRIDGE__) {
264
+ return window.__FLUTTER_BRIDGE__.send(
265
+ msg.type,
266
+ msg.action,
267
+ msg.payload,
268
+ ) as Promise<R>;
269
+ }
270
+ return Promise.resolve(null as R);
271
+ }
272
+
273
+ // ---- 消息监听 ----
274
+
275
+ /**
276
+ * 监听 Flutter 发来的所有消息
277
+ * @returns 取消监听的函数
278
+ */
279
+ onMessage<T = Record<string, unknown>>(
280
+ handler: MessageHandler<T>,
281
+ ): Unsubscribe {
282
+ this._messageHandlers.push(handler as WrappedHandler);
283
+ return () => {
284
+ this._messageHandlers = this._messageHandlers.filter(
285
+ (h) => h !== handler,
286
+ );
287
+ };
288
+ }
289
+
290
+ /**
291
+ * 监听特定类型/动作的消息
292
+ * @param type - 消息类型
293
+ * @param action - 动作名称,传 null 匹配所有
294
+ */
295
+ on<T = Record<string, unknown>>(
296
+ type: string,
297
+ action: string | null,
298
+ handler: MessageHandler<T>,
299
+ ): Unsubscribe {
300
+ const wrappedHandler: WrappedHandler = (msg) => {
301
+ if (msg.type === type && (action === null || msg.action === action)) {
302
+ (handler as MessageHandler)(msg);
303
+ }
304
+ };
305
+ wrappedHandler._original = handler as MessageHandler;
306
+ wrappedHandler._type = type;
307
+ wrappedHandler._action = action;
308
+ this._messageHandlers.push(wrappedHandler);
309
+ return () => {
310
+ this._messageHandlers = this._messageHandlers.filter(
311
+ (h) => h !== wrappedHandler,
312
+ );
313
+ };
314
+ }
315
+
316
+ // ---- 原生模块调用 ----
284
317
 
318
+ /**
319
+ * 获取模块代理,按命名空间调用原生 API(类型安全)。
320
+ *
321
+ * 搭配 TypeScript Generator 生成的类型使用,获得完整的类型推断。
322
+ *
323
+ * @param namespace - 模块命名空间
324
+ * @returns 模块方法代理对象
325
+ *
326
+ * @example
327
+ * ```ts
328
+ * import type { AudioModule } from '../generated/audio_module';
329
+ * import type { UserModule } from '../generated/user_module';
330
+ *
331
+ * const audio = bridge.module<AudioModule>('audio');
332
+ * await audio.play({ soundId: 'success', volume: 0.8 });
333
+ *
334
+ * const user = bridge.module<UserModule>('user');
335
+ * const profile = await user.getProfile();
336
+ * ```
337
+ */
338
+ module<T extends object = Record<string, (...args: any[]) => Promise<any>>>(
339
+ namespace: string,
340
+ ): T {
341
+ return new Proxy({} as T, {
342
+ get: (_target, method: string | symbol) => {
343
+ if (typeof method !== 'string') return undefined;
344
+ return async (...args: unknown[]) => {
345
+ if (window.NativeModules?.[namespace]?.[method]) {
346
+ const result = await window.NativeModules[namespace][method](
347
+ ...args,
348
+ );
349
+ // 检查模块访问被拒绝错误
350
+ if (
351
+ result &&
352
+ typeof result === 'object' &&
353
+ (result as Record<string, unknown>).__error__ ===
354
+ 'MODULE_ACCESS_DENIED'
355
+ ) {
356
+ console.error(
357
+ `[MRP Bridge] 模块访问被拒绝: ${namespace}.${method}`,
358
+ (result as Record<string, unknown>).__message__,
359
+ );
360
+ return null;
361
+ }
362
+ return result;
363
+ }
364
+ console.warn(
365
+ `[MRP Bridge] NativeModules.${namespace}.${method} 不存在`,
366
+ );
367
+ return Promise.resolve(null);
368
+ };
369
+ },
370
+ });
371
+ }
285
372
  }
286
373
 
287
374
  // ============================================================
@@ -289,6 +376,7 @@ export class MrpBridge {
289
376
  // ============================================================
290
377
 
291
378
  /** Bridge 单例实例 */
379
+ installIframeHostBridge();
292
380
  export const bridge = new MrpBridge();
293
381
 
294
382
  // 挂载到 window,方便非模块化项目使用