@jhh96/light-app-bridge 1.0.5 → 1.1.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 CHANGED
@@ -15,16 +15,15 @@
15
15
  | `window.worklink`(含 `invokeModuleMethodSync`) | 发送消息 / 取 webContentsId | preload 注入 |
16
16
  | `window.worklink.plugin.getContentContext` | 解析目标轻应用 webContentsId(**仅 plugin 角色**) | pluginLightAppPreload 注入 |
17
17
 
18
- **收消息(二选一,自动选择,不强依赖 waltz):**
18
+ **收消息(自动选择,不强依赖 waltz):**
19
19
 
20
20
  | 监听后端 | 优先级 | 适用环境 | 说明 |
21
21
  |---------|--------|---------|------|
22
22
  | `window.waltz.event.on('native-call-js')` | 高(优先) | 有 waltz 注入的环境 | 推荐路径,立即可用 |
23
- | `window.handleMessageFromNative` | 低(回退) | 无 waltz 的环境 | hook 此全局方法(waltzBridge 投递消息的统一入口)。**可能晚于构造注入**,SDK 会轮询等待(上限 10s) |
23
+ | `window.handleMessageFromNative` | 低(回退) | 无 waltz 的环境 | 由 SDK 定义此全局方法(该方法原本由 waltz.min.js 注册;无 waltz 时为 undefined,renderer 投递消息前会检查其类型,没人定义则消息被丢弃)。renderer 收到消息后主动调用它传入消息。若该方法已存在则包装透传,不破坏原逻辑 |
24
24
 
25
- - 两者都始终不可用时,`connect()`/`ready()` reject(构造不抛,因监听后端允许稍后出现)
26
25
  - SDK 内部按 waltz → handleMessageFromNative 顺序探测,无需手动指定
27
- - 握手会等监听就绪后才开始,避免早到的 ack/hello 丢失
26
+ - 握手会等监听绑定后才开始,避免早到的 ack/hello 丢失
28
27
 
29
28
  ## 安装(两个项目)
30
29
 
@@ -59,14 +58,21 @@ bridge.onMessage((data, id) => {
59
58
 
60
59
  ```js
61
60
  {
62
- __sdk: 'lightAppBridge', // 标识:在 plugin-message 流中过滤出本 SDK 的消息
63
- id: 'r_xxx', // 关联 id:send 生成,回复原样带回
64
- type: 'req' | 'res', // 请求 / 响应
65
- data: any // 任意业务数据
61
+ __sdk: 'LightAppBridge', // SDK 标识:过滤出本 SDK 的消息,隔离其它消息
62
+ v: 1, // 协议版本号
63
+ id: 'r_xxx', // 关联 id:send 生成,回复原样带回
64
+ type: 'hello'|'ack'|'req'|'res', // 握手 hello/ack;业务 req/res
65
+ data: any // 任意业务数据
66
66
  }
67
67
  ```
68
68
 
69
+ ## 连接与生命周期
70
+
71
+ - 构造即自动握手(插件发 hello 每秒重试;轻应用持续等待、不超时)
72
+ - 握手失败后**再次 `connect()` 可重连**(自动重置状态,无需重建实例)
73
+ - `destroy()` 销毁实例:解绑监听、拒绝未完成请求;销毁后 `send`/`connect` 立即 reject
74
+
69
75
  ## 文件
70
76
 
71
- - `src/lightAppBridge.js` —— SDK 源码(ESM,可直接 import)
72
- - `src/index.js` —— barrel re-export
77
+ - `src/lightAppBridge.ts` —— SDK 源码(TypeScript,构建产物为 ESM)
78
+ - `src/index.ts` —— barrel re-export
@@ -39,6 +39,16 @@ export interface BridgeOptions {
39
39
  timeout?: number;
40
40
  /** 握手超时(ms),默认 30000。仅 plugin 角色生效;lightapp 端持续等待,不超时 */
41
41
  handshakeTimeout?: number;
42
+ /**
43
+ * 调试模式:开启后在控制台打印完整通信轨迹,便于排查 plugin ↔ lightapp
44
+ * 联调问题。覆盖:
45
+ * - 构造(角色/配置)、握手(hello/ack/连接建立/超时/重连)
46
+ * - 发送请求(send data + id)、底层投递(目标 webContentsId + 信封)
47
+ * - 收到消息(type/id/from/data)、响应返回(res 数据/超时)
48
+ * - handler 异常、销毁
49
+ * 大 payload 截断到 500 字符防刷屏。
50
+ */
51
+ debug?: boolean;
42
52
  }
43
53
  /** send 单次调用选项 */
44
54
  export interface SendOptions {
@@ -70,12 +80,18 @@ export default class LightAppBridge {
70
80
  private _connectPromise;
71
81
  private _unlisten;
72
82
  private _listenerReady;
83
+ private _connectFailed;
84
+ private _destroyed;
85
+ private _debug;
86
+ private _helloCount;
73
87
  /**
74
88
  * @param role 角色,省略时按 window.worklink.plugin 是否存在自动探测
75
89
  * @param options 超时配置
76
90
  * @throws 依赖未就绪时抛出(window.worklink;监听后端可稍后出现,不算构造失败)
77
91
  */
78
92
  constructor(role?: BridgeRole, options?: BridgeOptions);
93
+ /** debug 日志(options.debug 开启时打印,前缀带角色便于双端对照) */
94
+ private _log;
79
95
  /** 校验依赖是否就绪,缺失抛出明确异常 */
80
96
  private _checkDeps;
81
97
  /** 绑定消息监听:优先 waltz;无 waltz 时定义/包装 window.handleMessageFromNative */
@@ -85,13 +101,21 @@ export default class LightAppBridge {
85
101
  /** 处理一条归一化后的消息 */
86
102
  private _onMessage;
87
103
  /**
88
- * 建立连接(握手)。构造时已自动调用,通常无需手动调;如需重新握手可再次调用。
104
+ * 建立连接(握手)。构造时已自动调用,通常无需手动调。
105
+ * - 已连接 / 握手进行中:幂等,返回同一个 Promise
106
+ * - 上次握手失败:重新发起(内部自动重置状态,无需重建实例——
107
+ * 典型场景:插件先启动、轻应用窗口尚未打开导致超时,对端就绪后再次 connect 即可)
108
+ * - 实例已销毁:reject
109
+ * 行为:
89
110
  * - 插件:发起握手(解析目标 id → 发 hello → 每秒重试直到收到 ack 或超时)
90
- * - 轻应用:被动等待 hello(持续监听,不超时——插件可能任意晚才发起连接)
91
- * 多次调用幂等,返回同一个连接 Promise。
111
+ * - 轻应用:被动等待 hello(持续监听,不超时)
92
112
  * @returns 对端 webContentsId
93
113
  */
94
114
  connect(): Promise<number | null>;
115
+ /** 重置连接状态(用于首次握手或失败后重连) */
116
+ private _resetConnectState;
117
+ /** 连接失败:标记失败并 reject(再次 connect 可重试) */
118
+ private _failConnect;
95
119
  /** 发送 hello(携带自身 webContentsId) */
96
120
  private _sendHello;
97
121
  /** 收到 hello(轻应用侧):记录对端、回 ack、完成握手 */
@@ -132,7 +156,7 @@ export default class LightAppBridge {
132
156
  * @returns 调用以取消监听
133
157
  */
134
158
  onMessage(handler: MessageHandler): () => void;
135
- /** 销毁实例:解绑消息监听、清空待处理请求、停止握手重试 */
159
+ /** 销毁实例:解绑消息监听、清空待处理请求、停止握手重试。销毁后实例不可再用 */
136
160
  destroy(): void;
137
161
  }
138
162
  /**
@@ -26,7 +26,21 @@ const PROTOCOL_VERSION = 1; // 协议版本号
26
26
  const DEFAULT_TIMEOUT = 3000; // send 默认超时(ms)
27
27
  const DEFAULT_HANDSHAKE_TIMEOUT = 30000; // 握手超时(ms)
28
28
  const HELLO_RETRY_INTERVAL = 1000; // 插件 hello 重试间隔(ms)
29
+ const DEBUG_PAYLOAD_LIMIT = 500; // debug 日志 payload 截断长度
29
30
  const genId = () => 'r_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 7);
31
+ /** debug 日志用:安全序列化 payload,超长截断 */
32
+ function debugPayload(data) {
33
+ let s;
34
+ try {
35
+ s = typeof data === 'string' ? data : JSON.stringify(data);
36
+ }
37
+ catch {
38
+ s = String(data);
39
+ }
40
+ if (s === undefined)
41
+ s = 'undefined';
42
+ return s.length > DEBUG_PAYLOAD_LIMIT ? s.slice(0, DEBUG_PAYLOAD_LIMIT) + `…(截断,共${s.length}字符)` : s;
43
+ }
30
44
  /** 判断是否为本 SDK 的消息(按 __sdk 标识过滤) */
31
45
  const isBridgeMsg = (msg) => !!(msg && typeof msg === 'object' && msg.__sdk === SDK_ID);
32
46
  /** 创建一个可外部 resolve 的 Promise */
@@ -163,6 +177,9 @@ export default class LightAppBridge {
163
177
  this._started = false;
164
178
  this._unlisten = null;
165
179
  this._listenerReady = null;
180
+ this._connectFailed = false;
181
+ this._destroyed = false;
182
+ this._helloCount = 0;
166
183
  let r = role === 'plugin' || role === 'lightapp' ? role : 'lightapp';
167
184
  if (role !== 'plugin' && role !== 'lightapp') {
168
185
  const wl = window.worklink;
@@ -171,6 +188,11 @@ export default class LightAppBridge {
171
188
  this.role = r;
172
189
  this.timeout = options.timeout != null ? options.timeout : DEFAULT_TIMEOUT;
173
190
  this.handshakeTimeout = options.handshakeTimeout != null ? options.handshakeTimeout : DEFAULT_HANDSHAKE_TIMEOUT;
191
+ this._debug = !!options.debug;
192
+ this._log('构造: role=' + r + ', timeout=' + this.timeout +
193
+ ', handshakeTimeout=' + this.handshakeTimeout +
194
+ ', 监听后端=' + (hasWaltz() ? 'waltz' : 'handleMessageFromNative') +
195
+ ', 自身id=' + this.getId());
174
196
  // 连接 deferred:构造时即建立,保证轻应用收到 hello 也能 resolve(无需显式 connect)
175
197
  this._connectDeferred = deferred();
176
198
  this._connectPromise = this._connectDeferred.promise.then((peerId) => {
@@ -181,6 +203,12 @@ export default class LightAppBridge {
181
203
  this._startListener(); // 绑定消息监听(waltz 或定义 handleMessageFromNative)
182
204
  this.connect(); // 构造即自动发起握手(内部会等监听就绪)
183
205
  }
206
+ /** debug 日志(options.debug 开启时打印,前缀带角色便于双端对照) */
207
+ _log(...args) {
208
+ if (!this._debug)
209
+ return;
210
+ console.log(`[LightAppBridge:${this.role}]`, ...args);
211
+ }
184
212
  /** 校验依赖是否就绪,缺失抛出明确异常 */
185
213
  _checkDeps() {
186
214
  const w = window;
@@ -204,7 +232,7 @@ export default class LightAppBridge {
204
232
  })
205
233
  .catch((e) => {
206
234
  // 理论上不会发生(无 waltz 时必走定义路径),保险起见让连接失败并给出原因
207
- this._connectDeferred.reject(new Error('LightAppBridge 监听后端绑定失败 (' +
235
+ this._failConnect(new Error('LightAppBridge 监听后端绑定失败 (' +
208
236
  (e && e.message ? e.message : String(e)) + ')'));
209
237
  });
210
238
  }
@@ -216,8 +244,12 @@ export default class LightAppBridge {
216
244
  _onMessage(msg) {
217
245
  const envelope = msg.envelope;
218
246
  const fromId = msg.fromWebContentsId;
219
- if (!isBridgeMsg(envelope))
247
+ if (!isBridgeMsg(envelope)) {
248
+ this._log('recv← 忽略非本SDK消息: from=' + fromId);
220
249
  return;
250
+ }
251
+ this._log('recv← type=' + envelope.type + ' id=' + envelope.id +
252
+ ' from=' + fromId + ' data=' + debugPayload(envelope.data));
221
253
  switch (envelope.type) {
222
254
  case 'hello':
223
255
  this._onHello(fromId);
@@ -236,16 +268,27 @@ export default class LightAppBridge {
236
268
  }
237
269
  // —— 连接 / 握手 —— //
238
270
  /**
239
- * 建立连接(握手)。构造时已自动调用,通常无需手动调;如需重新握手可再次调用。
271
+ * 建立连接(握手)。构造时已自动调用,通常无需手动调。
272
+ * - 已连接 / 握手进行中:幂等,返回同一个 Promise
273
+ * - 上次握手失败:重新发起(内部自动重置状态,无需重建实例——
274
+ * 典型场景:插件先启动、轻应用窗口尚未打开导致超时,对端就绪后再次 connect 即可)
275
+ * - 实例已销毁:reject
276
+ * 行为:
240
277
  * - 插件:发起握手(解析目标 id → 发 hello → 每秒重试直到收到 ack 或超时)
241
- * - 轻应用:被动等待 hello(持续监听,不超时——插件可能任意晚才发起连接)
242
- * 多次调用幂等,返回同一个连接 Promise。
278
+ * - 轻应用:被动等待 hello(持续监听,不超时)
243
279
  * @returns 对端 webContentsId
244
280
  */
245
281
  connect() {
246
282
  var _a;
247
- if (this._connectStarted)
283
+ if (this._destroyed) {
284
+ return Promise.reject(new Error('实例已销毁,请重建实例'));
285
+ }
286
+ if (this._connected)
248
287
  return this._connectPromise;
288
+ if (this._connectStarted && !this._connectFailed)
289
+ return this._connectPromise;
290
+ // 首次调用,或上次握手失败:重置连接状态重新发起
291
+ this._resetConnectState();
249
292
  this._connectStarted = true;
250
293
  // 等监听后端绑定完成后再握手,避免对端的 ack/hello 在监听绑定前到达而丢失
251
294
  const listenerReady = (_a = this._listenerReady) !== null && _a !== void 0 ? _a : Promise.resolve();
@@ -254,7 +297,7 @@ export default class LightAppBridge {
254
297
  if (this.role === 'plugin') {
255
298
  this._handshakeTimer = setTimeout(() => {
256
299
  if (!this._connected) {
257
- this._connectDeferred.reject(new Error(`握手超时 (${this.handshakeTimeout}ms),对端未响应`));
300
+ this._failConnect(new Error(`握手超时 (${this.handshakeTimeout}ms),对端未响应`));
258
301
  this._clearHelloTimer();
259
302
  }
260
303
  }, this.handshakeTimeout);
@@ -280,27 +323,54 @@ export default class LightAppBridge {
280
323
  }
281
324
  // lightapp 被动等待 hello(listener 已绑定),收到后在 _onHello 中回 ack 并 resolve
282
325
  })
283
- .catch((e) => this._connectDeferred.reject(e));
326
+ .catch((e) => this._failConnect(e));
284
327
  return this._connectPromise;
285
328
  }
329
+ /** 重置连接状态(用于首次握手或失败后重连) */
330
+ _resetConnectState() {
331
+ this._connectDeferred = deferred();
332
+ this._connectPromise = this._connectDeferred.promise.then((peerId) => {
333
+ this._onConnected(peerId);
334
+ return peerId;
335
+ });
336
+ this._connectFailed = false;
337
+ this._connectStarted = false;
338
+ this._clearHelloTimer();
339
+ if (this._handshakeTimer) {
340
+ clearTimeout(this._handshakeTimer);
341
+ this._handshakeTimer = null;
342
+ }
343
+ }
344
+ /** 连接失败:标记失败并 reject(再次 connect 可重试) */
345
+ _failConnect(e) {
346
+ if (this._destroyed)
347
+ return;
348
+ this._connectFailed = true;
349
+ this._log('✗ 握手失败: ' + (e && e.message ? e.message : String(e)));
350
+ this._connectDeferred.reject(e instanceof Error ? e : new Error(String(e)));
351
+ }
286
352
  /** 发送 hello(携带自身 webContentsId) */
287
353
  _sendHello() {
288
354
  if (this._target == null)
289
355
  return;
356
+ this._helloCount++;
357
+ this._log('hello→ 目标=' + this._target + (this._helloCount > 1 ? ' (重试#' + this._helloCount + ')' : ''));
290
358
  this._send(this._target, this._envelope('hello', { from: this.getId() }));
291
359
  }
292
360
  /** 收到 hello(轻应用侧):记录对端、回 ack、完成握手 */
293
361
  _onHello(fromId) {
294
- if (fromId != null)
295
- this._recordPeer(fromId);
362
+ if (fromId == null)
363
+ return; // 无法定位对端则忽略(真实环境该字段恒有值),握手不完成
364
+ this._recordPeer(fromId);
296
365
  // 回 ack,携带自身 webContentsId
297
- if (fromId != null)
298
- this._send(fromId, this._envelope('ack', { from: this.getId() }));
366
+ this._send(fromId, this._envelope('ack', { from: this.getId() }));
299
367
  // 握手完成(resolve 幂等,重复 hello 只回 ack)
300
368
  this._connectDeferred.resolve(fromId);
301
369
  }
302
370
  /** 收到 ack(插件侧):确认连接 */
303
371
  _onAck(fromId) {
372
+ if (fromId == null)
373
+ return; // 无法定位对端则忽略
304
374
  this._recordPeer(fromId);
305
375
  this._connectDeferred.resolve(fromId);
306
376
  }
@@ -310,6 +380,7 @@ export default class LightAppBridge {
310
380
  }
311
381
  _onConnected(peerId) {
312
382
  this._connected = true;
383
+ this._log('✓ 连接建立: 对端=' + peerId + (this._helloCount > 1 ? ' (hello共' + this._helloCount + '次)' : ''));
313
384
  this._clearHelloTimer();
314
385
  if (this._handshakeTimer) {
315
386
  clearTimeout(this._handshakeTimer);
@@ -362,19 +433,24 @@ export default class LightAppBridge {
362
433
  Promise.allSettled(results).then((settled) => {
363
434
  for (const s of settled) {
364
435
  if (s.status === 'fulfilled' && s.value !== undefined) {
436
+ this._log('res→ id=' + id + ' data=' + debugPayload(s.value));
365
437
  this._send(this._peer, this._envelope('res', s.value, id));
366
438
  return;
367
439
  }
368
440
  }
441
+ this._log('✗ req 无可回复值(回调全空/异常): id=' + id);
369
442
  });
370
443
  }
371
444
  /** 收到 res:按 id 唤醒等待中的 send */
372
445
  _serveRes(envelope) {
373
446
  const p = this._pending.get(envelope.id);
374
- if (!p)
447
+ if (!p) {
448
+ this._log('recv← res 无等待者(可能已超时): id=' + envelope.id);
375
449
  return;
450
+ }
376
451
  clearTimeout(p.timer);
377
452
  this._pending.delete(envelope.id);
453
+ this._log('✓ 收到响应 id=' + envelope.id + ' data=' + debugPayload(envelope.data));
378
454
  p.resolve(envelope.data);
379
455
  }
380
456
  /** 底层发送:把信封投递到指定 webContentsId */
@@ -402,14 +478,18 @@ export default class LightAppBridge {
402
478
  * @returns 对端回调返回的数据
403
479
  */
404
480
  send(data, options = {}) {
481
+ if (this._destroyed)
482
+ return Promise.reject(new Error('实例已销毁'));
405
483
  const timeout = options.timeout != null ? options.timeout : this.timeout;
406
484
  return this.ready().then((targetId) => {
407
485
  const id = genId();
408
486
  const envelope = this._envelope('req', data, id);
487
+ this._log('send→ id=' + id + ' 目标=' + targetId + ' data=' + debugPayload(data));
409
488
  return new Promise((resolve, reject) => {
410
489
  const timer = setTimeout(() => {
411
490
  if (this._pending.has(id)) {
412
491
  this._pending.delete(id);
492
+ this._log('✗ 超时 id=' + id + ' (' + timeout + 'ms 无响应)');
413
493
  reject(new Error(`请求超时 (${timeout}ms)`));
414
494
  }
415
495
  }, timeout);
@@ -437,8 +517,12 @@ export default class LightAppBridge {
437
517
  this._messageHandlers.splice(i, 1);
438
518
  };
439
519
  }
440
- /** 销毁实例:解绑消息监听、清空待处理请求、停止握手重试 */
520
+ /** 销毁实例:解绑消息监听、清空待处理请求、停止握手重试。销毁后实例不可再用 */
441
521
  destroy() {
522
+ if (this._destroyed)
523
+ return;
524
+ this._destroyed = true;
525
+ this._log('销毁实例: 未完成请求=' + this._pending.size);
442
526
  if (this._unlisten) {
443
527
  try {
444
528
  this._unlisten();
@@ -451,6 +535,8 @@ export default class LightAppBridge {
451
535
  clearTimeout(this._handshakeTimer);
452
536
  this._handshakeTimer = null;
453
537
  }
538
+ // 进行中的握手直接失败(已 resolve 的话 reject 是 no-op)
539
+ this._connectDeferred.reject(new Error('实例已销毁'));
454
540
  // 拒绝所有未完成的请求
455
541
  this._pending.forEach((p) => {
456
542
  clearTimeout(p.timer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhh96/light-app-bridge",
3
- "version": "1.0.5",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "招商随行(WorkLink)插件窗口(plugin)与轻应用窗口(lightapp)之间的双向通信 SDK",
6
6
  "main": "./dist/index.js",