@jhh96/light-app-bridge 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 +60 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/lightAppBridge.d.ts +133 -0
- package/dist/lightAppBridge.js +335 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# lightAppBridge
|
|
2
|
+
|
|
3
|
+
> 招商随行(WorkLink)**插件窗口(plugin)**与**轻应用窗口(lightapp)**之间的双向通信 SDK。
|
|
4
|
+
|
|
5
|
+
基于 `id` 关联的请求 / 响应消息机制:发消息时生成唯一 `id`,对端回调的返回值原样带相同 `id` 回复,靠 `id` 配对请求与结果。
|
|
6
|
+
|
|
7
|
+
本包是 hkvs-llm-chat(plugin)与 vue-talent-cockpit(lightapp)两端的**共享权威源**,消除各自 copy 的版本漂移。
|
|
8
|
+
|
|
9
|
+
## 依赖(构造时校验,缺失直接抛异常)
|
|
10
|
+
|
|
11
|
+
| 依赖 | 用途 | 来源 |
|
|
12
|
+
|------|------|------|
|
|
13
|
+
| `window.waltz.event.on` | 接收消息 | preload 注入的 waltz SDK |
|
|
14
|
+
| `window.worklink`(含 `invokeModuleMethodSync`) | 发送消息 / 取 webContentsId | preload 注入 |
|
|
15
|
+
| `window.worklink.plugin.getContentContext` | 解析目标轻应用 webContentsId(**仅 plugin 角色**) | pluginLightAppPreload 注入 |
|
|
16
|
+
|
|
17
|
+
## 安装(两个项目)
|
|
18
|
+
|
|
19
|
+
共享包位于仓库根 `packages/lightAppBridge`(与 hkvs-llm-chat、vue-talent-cockpit 同级),两个项目以 `file:` 相对路径引用(npm 7+ 软链式安装,源码改动实时生效)。
|
|
20
|
+
|
|
21
|
+
### vue-talent-cockpit / hkvs-llm-chat(相同)
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
"dependencies": { "lightAppBridge": "file:../packages/lightAppBridge" }
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
注意:本包源码为 ES2019 兼容写法(不含 `?.`/`??`),hkvs 的 webpack4 可直接解析,无需 `transpileDependencies`。
|
|
28
|
+
|
|
29
|
+
## 用法
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
import LightAppBridge, { createBridge } from 'lightAppBridge'
|
|
33
|
+
|
|
34
|
+
// 插件窗口(发起方)
|
|
35
|
+
const bridge = new LightAppBridge('plugin')
|
|
36
|
+
await bridge.ready()
|
|
37
|
+
const result = await bridge.send({ action: 'getUser', id: 1 })
|
|
38
|
+
|
|
39
|
+
// 轻应用窗口(接收方,return 即回复)
|
|
40
|
+
const bridge = new LightAppBridge('lightapp')
|
|
41
|
+
bridge.onMessage((data, id) => {
|
|
42
|
+
return { name: 'tom' } // 返回值 = 回复内容(可 return Promise)
|
|
43
|
+
})
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 消息信封
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
{
|
|
50
|
+
__sdk: 'lightAppBridge', // 标识:在 plugin-message 流中过滤出本 SDK 的消息
|
|
51
|
+
id: 'r_xxx', // 关联 id:send 生成,回复原样带回
|
|
52
|
+
type: 'req' | 'res', // 请求 / 响应
|
|
53
|
+
data: any // 任意业务数据
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 文件
|
|
58
|
+
|
|
59
|
+
- `src/lightAppBridge.js` —— SDK 源码(ESM,可直接 import)
|
|
60
|
+
- `src/index.js` —— barrel re-export
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* LightAppBridge —— 插件窗口(plugin)与轻应用窗口(lightapp)之间的双向通信 SDK
|
|
3
|
+
*
|
|
4
|
+
* 核心特性:
|
|
5
|
+
* 1. 连接建立:插件主动 connect() 发起握手,双向交换 webContentsId,确认连接后才能收发。
|
|
6
|
+
* 2. 请求/响应:基于 id 关联,对端 onMessage 回调的「返回值」带相同 id 回复。
|
|
7
|
+
* 3. SDK 标识:所有消息携带 __sdk 标识 + 版本号,隔离其它消息,防止混淆。
|
|
8
|
+
*
|
|
9
|
+
* 依赖(构造时校验,缺失直接抛异常,请确保依赖注入完成后再 new):
|
|
10
|
+
* - window.waltz.event.on 收消息
|
|
11
|
+
* - window.worklink 发消息 / 取 webContentsId
|
|
12
|
+
* - window.worklink.plugin.getContentContext (仅 plugin 角色需要)
|
|
13
|
+
*
|
|
14
|
+
* 消息信封:
|
|
15
|
+
* { __sdk: 'LightAppBridge', v: 1, id, type, data }
|
|
16
|
+
* type ∈ 'hello' | 'ack' | 'req' | 'res'
|
|
17
|
+
*/
|
|
18
|
+
/** 角色:plugin=插件窗口(发起方),lightapp=轻应用窗口(接收方) */
|
|
19
|
+
export type BridgeRole = 'plugin' | 'lightapp';
|
|
20
|
+
/** 信封类型 */
|
|
21
|
+
export type EnvelopeType = 'hello' | 'ack' | 'req' | 'res';
|
|
22
|
+
/** 消息信封:所有跨窗口通信的统一包装 */
|
|
23
|
+
export interface Envelope {
|
|
24
|
+
__sdk: string;
|
|
25
|
+
v: number;
|
|
26
|
+
id: string;
|
|
27
|
+
type: EnvelopeType;
|
|
28
|
+
data: any;
|
|
29
|
+
}
|
|
30
|
+
/** 构造选项 */
|
|
31
|
+
export interface BridgeOptions {
|
|
32
|
+
/** send 默认超时(ms),默认 3000 */
|
|
33
|
+
timeout?: number;
|
|
34
|
+
/** 握手超时(ms),默认 30000 */
|
|
35
|
+
handshakeTimeout?: number;
|
|
36
|
+
}
|
|
37
|
+
/** send 单次调用选项 */
|
|
38
|
+
export interface SendOptions {
|
|
39
|
+
/** 本次超时(ms),不传用实例默认值 */
|
|
40
|
+
timeout?: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 请求消息处理器。返回值(含 Promise 解析值)会作为回复发回发送方。
|
|
44
|
+
* @param data 请求携带的业务数据
|
|
45
|
+
* @param id 请求 id(与回复配对用)
|
|
46
|
+
* @param fromWebContentsId 发送方 webContentsId
|
|
47
|
+
*/
|
|
48
|
+
export type MessageHandler = (data: any, id: string, fromWebContentsId: number | null) => any | Promise<any>;
|
|
49
|
+
export default class LightAppBridge {
|
|
50
|
+
role: BridgeRole;
|
|
51
|
+
timeout: number;
|
|
52
|
+
handshakeTimeout: number;
|
|
53
|
+
private _pending;
|
|
54
|
+
private _messageHandlers;
|
|
55
|
+
private _target;
|
|
56
|
+
private _peer;
|
|
57
|
+
private _connected;
|
|
58
|
+
private _connectStarted;
|
|
59
|
+
private _connectCbs;
|
|
60
|
+
private _helloTimer;
|
|
61
|
+
private _handshakeTimer;
|
|
62
|
+
private _started;
|
|
63
|
+
private _connectDeferred;
|
|
64
|
+
private _connectPromise;
|
|
65
|
+
/**
|
|
66
|
+
* @param role 角色,省略时按 window.worklink.plugin 是否存在自动探测
|
|
67
|
+
* @param options 超时配置
|
|
68
|
+
* @throws 依赖未就绪时抛出(window.waltz / window.worklink)
|
|
69
|
+
*/
|
|
70
|
+
constructor(role?: BridgeRole, options?: BridgeOptions);
|
|
71
|
+
/** 校验依赖是否就绪,缺失抛出明确异常 */
|
|
72
|
+
private _checkDeps;
|
|
73
|
+
/** 绑定 native-call-js 监听 */
|
|
74
|
+
private _bindListener;
|
|
75
|
+
/** 构造消息信封 */
|
|
76
|
+
private _envelope;
|
|
77
|
+
/** 处理一条原生消息 */
|
|
78
|
+
private _onRaw;
|
|
79
|
+
/**
|
|
80
|
+
* 建立连接(握手)。构造时已自动调用,通常无需手动调;如需重新握手可再次调用。
|
|
81
|
+
* - 插件:发起握手(解析目标 id → 发 hello → 每秒重试直到收到 ack)
|
|
82
|
+
* - 轻应用:启动握手等待(被动等 hello;若插件始终未来则超时)
|
|
83
|
+
* 多次调用幂等,返回同一个连接 Promise。
|
|
84
|
+
* @returns 对端 webContentsId
|
|
85
|
+
*/
|
|
86
|
+
connect(): Promise<number | null>;
|
|
87
|
+
/** 发送 hello(携带自身 webContentsId) */
|
|
88
|
+
private _sendHello;
|
|
89
|
+
/** 收到 hello(轻应用侧):记录对端、回 ack、完成握手 */
|
|
90
|
+
private _onHello;
|
|
91
|
+
/** 收到 ack(插件侧):确认连接 */
|
|
92
|
+
private _onAck;
|
|
93
|
+
private _recordPeer;
|
|
94
|
+
private _onConnected;
|
|
95
|
+
private _clearHelloTimer;
|
|
96
|
+
/** 监听连接建立(握手完成)。若已连接则立即触发 */
|
|
97
|
+
onConnect(cb: (peerId: number | null) => void): this;
|
|
98
|
+
/**
|
|
99
|
+
* 等待连接建立(握手完成)。构造时已自动发起握手,直接 await 即可。
|
|
100
|
+
* @returns 对端 webContentsId
|
|
101
|
+
*/
|
|
102
|
+
ready(): Promise<number | null>;
|
|
103
|
+
/**
|
|
104
|
+
* 收到 req:调用 onMessage 回调,回调的返回值(含 Promise)作为回复内容。
|
|
105
|
+
* 多个回调并发执行,取注册顺序中第一个非 undefined 的返回值回复。
|
|
106
|
+
*/
|
|
107
|
+
private _serveReq;
|
|
108
|
+
/** 收到 res:按 id 唤醒等待中的 send */
|
|
109
|
+
private _serveRes;
|
|
110
|
+
/** 底层发送:把信封投递到指定 webContentsId */
|
|
111
|
+
private _send;
|
|
112
|
+
/** 获取自身 webContentsId */
|
|
113
|
+
getId(): number | null;
|
|
114
|
+
/**
|
|
115
|
+
* 发送消息(请求),自动生成 id,等对端用相同 id 回复。
|
|
116
|
+
* 内部会先等待连接建立(握手完成)。
|
|
117
|
+
* @param data 任意业务数据
|
|
118
|
+
* @param options 本次超时(ms),不传用实例默认值(默认 3000)
|
|
119
|
+
* @returns 对端回调返回的数据
|
|
120
|
+
*/
|
|
121
|
+
send(data: any, options?: SendOptions): Promise<any>;
|
|
122
|
+
/**
|
|
123
|
+
* 监听收到的请求消息。回调的返回值(含 Promise)即回复内容。
|
|
124
|
+
* @returns 调用以取消监听
|
|
125
|
+
*/
|
|
126
|
+
onMessage(handler: MessageHandler): () => void;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 工厂函数
|
|
130
|
+
* @param role 角色,省略时自动探测
|
|
131
|
+
* @param options 超时配置
|
|
132
|
+
*/
|
|
133
|
+
export declare function createBridge(role?: BridgeRole, options?: BridgeOptions): LightAppBridge;
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* LightAppBridge —— 插件窗口(plugin)与轻应用窗口(lightapp)之间的双向通信 SDK
|
|
3
|
+
*
|
|
4
|
+
* 核心特性:
|
|
5
|
+
* 1. 连接建立:插件主动 connect() 发起握手,双向交换 webContentsId,确认连接后才能收发。
|
|
6
|
+
* 2. 请求/响应:基于 id 关联,对端 onMessage 回调的「返回值」带相同 id 回复。
|
|
7
|
+
* 3. SDK 标识:所有消息携带 __sdk 标识 + 版本号,隔离其它消息,防止混淆。
|
|
8
|
+
*
|
|
9
|
+
* 依赖(构造时校验,缺失直接抛异常,请确保依赖注入完成后再 new):
|
|
10
|
+
* - window.waltz.event.on 收消息
|
|
11
|
+
* - window.worklink 发消息 / 取 webContentsId
|
|
12
|
+
* - window.worklink.plugin.getContentContext (仅 plugin 角色需要)
|
|
13
|
+
*
|
|
14
|
+
* 消息信封:
|
|
15
|
+
* { __sdk: 'LightAppBridge', v: 1, id, type, data }
|
|
16
|
+
* type ∈ 'hello' | 'ack' | 'req' | 'res'
|
|
17
|
+
*/
|
|
18
|
+
const SDK_ID = 'LightAppBridge'; // SDK 标识:所有消息携带,用于隔离其它消息
|
|
19
|
+
const PROTOCOL_VERSION = 1; // 协议版本号
|
|
20
|
+
const DEFAULT_TIMEOUT = 3000; // send 默认超时(ms)
|
|
21
|
+
const DEFAULT_HANDSHAKE_TIMEOUT = 30000; // 握手超时(ms)
|
|
22
|
+
const HELLO_RETRY_INTERVAL = 1000; // 插件 hello 重试间隔(ms)
|
|
23
|
+
const genId = () => 'r_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 7);
|
|
24
|
+
/** 判断是否为本 SDK 的消息(按 __sdk 标识过滤) */
|
|
25
|
+
const isBridgeMsg = (msg) => !!(msg && typeof msg === 'object' && msg.__sdk === SDK_ID);
|
|
26
|
+
/** 创建一个可外部 resolve 的 Promise */
|
|
27
|
+
function deferred() {
|
|
28
|
+
let resolve;
|
|
29
|
+
let reject;
|
|
30
|
+
const promise = new Promise((res, rej) => {
|
|
31
|
+
resolve = res;
|
|
32
|
+
reject = rej;
|
|
33
|
+
});
|
|
34
|
+
return { promise, resolve, reject };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 统一获取同步调用方法。
|
|
38
|
+
* 插件窗口挂在 worklink.moduleMethodInvoker 上;轻应用窗口直接挂在 worklink 上。
|
|
39
|
+
*/
|
|
40
|
+
function getInvoke() {
|
|
41
|
+
const wl = window.worklink;
|
|
42
|
+
if (wl && wl.moduleMethodInvoker && wl.moduleMethodInvoker.invokeModuleMethodSync) {
|
|
43
|
+
return (...args) => wl.moduleMethodInvoker.invokeModuleMethodSync(...args);
|
|
44
|
+
}
|
|
45
|
+
if (wl && wl.invokeModuleMethodSync) {
|
|
46
|
+
return (...args) => wl.invokeModuleMethodSync(...args);
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
export default class LightAppBridge {
|
|
51
|
+
/**
|
|
52
|
+
* @param role 角色,省略时按 window.worklink.plugin 是否存在自动探测
|
|
53
|
+
* @param options 超时配置
|
|
54
|
+
* @throws 依赖未就绪时抛出(window.waltz / window.worklink)
|
|
55
|
+
*/
|
|
56
|
+
constructor(role, options = {}) {
|
|
57
|
+
this._pending = new Map();
|
|
58
|
+
this._messageHandlers = [];
|
|
59
|
+
this._target = null;
|
|
60
|
+
this._peer = null;
|
|
61
|
+
this._connected = false;
|
|
62
|
+
this._connectStarted = false;
|
|
63
|
+
this._connectCbs = [];
|
|
64
|
+
this._helloTimer = null;
|
|
65
|
+
this._handshakeTimer = null;
|
|
66
|
+
this._started = false;
|
|
67
|
+
let r = role === 'plugin' || role === 'lightapp' ? role : 'lightapp';
|
|
68
|
+
if (role !== 'plugin' && role !== 'lightapp') {
|
|
69
|
+
const wl = window.worklink;
|
|
70
|
+
r = wl && wl.plugin ? 'plugin' : 'lightapp';
|
|
71
|
+
}
|
|
72
|
+
this.role = r;
|
|
73
|
+
this.timeout = options.timeout != null ? options.timeout : DEFAULT_TIMEOUT;
|
|
74
|
+
this.handshakeTimeout = options.handshakeTimeout != null ? options.handshakeTimeout : DEFAULT_HANDSHAKE_TIMEOUT;
|
|
75
|
+
// 连接 deferred:构造时即建立,保证轻应用收到 hello 也能 resolve(无需显式 connect)
|
|
76
|
+
this._connectDeferred = deferred();
|
|
77
|
+
this._connectPromise = this._connectDeferred.promise.then((peerId) => {
|
|
78
|
+
this._onConnected(peerId);
|
|
79
|
+
return peerId;
|
|
80
|
+
});
|
|
81
|
+
this._checkDeps(); // 依赖校验,缺失直接抛异常
|
|
82
|
+
this._bindListener(); // 绑定 native-call-js 监听
|
|
83
|
+
this.connect(); // 构造即自动发起握手
|
|
84
|
+
}
|
|
85
|
+
/** 校验依赖是否就绪,缺失抛出明确异常 */
|
|
86
|
+
_checkDeps() {
|
|
87
|
+
const w = window;
|
|
88
|
+
if (!(w.waltz && w.waltz.event) || typeof w.waltz.event.on !== 'function') {
|
|
89
|
+
throw new Error('LightAppBridge 依赖 window.waltz.event.on,当前环境不可用');
|
|
90
|
+
}
|
|
91
|
+
if (!getInvoke()) {
|
|
92
|
+
throw new Error('LightAppBridge 依赖 window.worklink.invokeModuleMethodSync,当前环境不可用');
|
|
93
|
+
}
|
|
94
|
+
if (this.role === 'plugin' && typeof (w.worklink.plugin && w.worklink.plugin.getContentContext) !== 'function') {
|
|
95
|
+
throw new Error('LightAppBridge(plugin) 依赖 window.worklink.plugin.getContentContext,当前环境不可用');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** 绑定 native-call-js 监听 */
|
|
99
|
+
_bindListener() {
|
|
100
|
+
if (this._started)
|
|
101
|
+
return;
|
|
102
|
+
this._started = true;
|
|
103
|
+
window.waltz.event.on('native-call-js', (raw) => this._onRaw(raw));
|
|
104
|
+
}
|
|
105
|
+
/** 构造消息信封 */
|
|
106
|
+
_envelope(type, data, id = genId()) {
|
|
107
|
+
return { __sdk: SDK_ID, v: PROTOCOL_VERSION, id, type, data };
|
|
108
|
+
}
|
|
109
|
+
/** 处理一条原生消息 */
|
|
110
|
+
_onRaw(raw) {
|
|
111
|
+
if (!raw || !raw.body || raw.body.eventName !== 'plugin-message')
|
|
112
|
+
return;
|
|
113
|
+
const params = raw.body.params || {};
|
|
114
|
+
const envelope = params.message;
|
|
115
|
+
const fromId = params.fromWebContentsId != null ? params.fromWebContentsId : null;
|
|
116
|
+
if (!isBridgeMsg(envelope))
|
|
117
|
+
return;
|
|
118
|
+
switch (envelope.type) {
|
|
119
|
+
case 'hello':
|
|
120
|
+
this._onHello(fromId);
|
|
121
|
+
break;
|
|
122
|
+
case 'ack':
|
|
123
|
+
this._onAck(fromId);
|
|
124
|
+
break;
|
|
125
|
+
case 'req':
|
|
126
|
+
this._recordPeer(fromId);
|
|
127
|
+
this._serveReq(envelope);
|
|
128
|
+
break;
|
|
129
|
+
case 'res':
|
|
130
|
+
this._serveRes(envelope);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// —— 连接 / 握手 —— //
|
|
135
|
+
/**
|
|
136
|
+
* 建立连接(握手)。构造时已自动调用,通常无需手动调;如需重新握手可再次调用。
|
|
137
|
+
* - 插件:发起握手(解析目标 id → 发 hello → 每秒重试直到收到 ack)
|
|
138
|
+
* - 轻应用:启动握手等待(被动等 hello;若插件始终未来则超时)
|
|
139
|
+
* 多次调用幂等,返回同一个连接 Promise。
|
|
140
|
+
* @returns 对端 webContentsId
|
|
141
|
+
*/
|
|
142
|
+
connect() {
|
|
143
|
+
if (this._connectStarted)
|
|
144
|
+
return this._connectPromise;
|
|
145
|
+
this._connectStarted = true;
|
|
146
|
+
// 握手超时保护
|
|
147
|
+
this._handshakeTimer = setTimeout(() => {
|
|
148
|
+
if (!this._connected) {
|
|
149
|
+
this._connectDeferred.reject(new Error(`握手超时 (${this.handshakeTimeout}ms),对端未响应`));
|
|
150
|
+
this._clearHelloTimer();
|
|
151
|
+
}
|
|
152
|
+
}, this.handshakeTimeout);
|
|
153
|
+
if (this.role === 'plugin') {
|
|
154
|
+
// 插件:解析目标后发 hello,并重试直到收到 ack
|
|
155
|
+
;
|
|
156
|
+
window.worklink.plugin.getContentContext()
|
|
157
|
+
.then((ctx) => {
|
|
158
|
+
if (!ctx || ctx.webContentsId == null) {
|
|
159
|
+
throw new Error('getContentContext 未返回 webContentsId');
|
|
160
|
+
}
|
|
161
|
+
this._target = ctx.webContentsId;
|
|
162
|
+
this._sendHello();
|
|
163
|
+
this._helloTimer = setInterval(() => {
|
|
164
|
+
if (!this._connected)
|
|
165
|
+
this._sendHello();
|
|
166
|
+
}, HELLO_RETRY_INTERVAL);
|
|
167
|
+
})
|
|
168
|
+
.catch((e) => this._connectDeferred.reject(e));
|
|
169
|
+
}
|
|
170
|
+
// lightapp 被动等待 hello(listener 已绑定),收到后在 _onHello 中回 ack 并 resolve
|
|
171
|
+
return this._connectPromise;
|
|
172
|
+
}
|
|
173
|
+
/** 发送 hello(携带自身 webContentsId) */
|
|
174
|
+
_sendHello() {
|
|
175
|
+
if (this._target == null)
|
|
176
|
+
return;
|
|
177
|
+
this._send(this._target, this._envelope('hello', { from: this.getId() }));
|
|
178
|
+
}
|
|
179
|
+
/** 收到 hello(轻应用侧):记录对端、回 ack、完成握手 */
|
|
180
|
+
_onHello(fromId) {
|
|
181
|
+
if (fromId != null)
|
|
182
|
+
this._recordPeer(fromId);
|
|
183
|
+
// 回 ack,携带自身 webContentsId
|
|
184
|
+
if (fromId != null)
|
|
185
|
+
this._send(fromId, this._envelope('ack', { from: this.getId() }));
|
|
186
|
+
// 握手完成(resolve 幂等,重复 hello 只回 ack)
|
|
187
|
+
this._connectDeferred.resolve(fromId);
|
|
188
|
+
}
|
|
189
|
+
/** 收到 ack(插件侧):确认连接 */
|
|
190
|
+
_onAck(fromId) {
|
|
191
|
+
this._recordPeer(fromId);
|
|
192
|
+
this._connectDeferred.resolve(fromId);
|
|
193
|
+
}
|
|
194
|
+
_recordPeer(fromId) {
|
|
195
|
+
if (fromId != null)
|
|
196
|
+
this._peer = fromId;
|
|
197
|
+
}
|
|
198
|
+
_onConnected(peerId) {
|
|
199
|
+
this._connected = true;
|
|
200
|
+
this._clearHelloTimer();
|
|
201
|
+
if (this._handshakeTimer) {
|
|
202
|
+
clearTimeout(this._handshakeTimer);
|
|
203
|
+
this._handshakeTimer = null;
|
|
204
|
+
}
|
|
205
|
+
// 触发 onConnect 回调
|
|
206
|
+
const cbs = this._connectCbs.splice(0);
|
|
207
|
+
cbs.forEach((cb) => {
|
|
208
|
+
try {
|
|
209
|
+
cb(peerId);
|
|
210
|
+
}
|
|
211
|
+
catch { /* ignore */ }
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
_clearHelloTimer() {
|
|
215
|
+
if (this._helloTimer) {
|
|
216
|
+
clearInterval(this._helloTimer);
|
|
217
|
+
this._helloTimer = null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/** 监听连接建立(握手完成)。若已连接则立即触发 */
|
|
221
|
+
onConnect(cb) {
|
|
222
|
+
if (this._connected)
|
|
223
|
+
cb(this._peer);
|
|
224
|
+
else
|
|
225
|
+
this._connectCbs.push(cb);
|
|
226
|
+
return this;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* 等待连接建立(握手完成)。构造时已自动发起握手,直接 await 即可。
|
|
230
|
+
* @returns 对端 webContentsId
|
|
231
|
+
*/
|
|
232
|
+
ready() {
|
|
233
|
+
return this.connect();
|
|
234
|
+
}
|
|
235
|
+
// —— 请求/响应 —— //
|
|
236
|
+
/**
|
|
237
|
+
* 收到 req:调用 onMessage 回调,回调的返回值(含 Promise)作为回复内容。
|
|
238
|
+
* 多个回调并发执行,取注册顺序中第一个非 undefined 的返回值回复。
|
|
239
|
+
*/
|
|
240
|
+
_serveReq(envelope) {
|
|
241
|
+
const { id, data } = envelope;
|
|
242
|
+
if (!this._messageHandlers.length)
|
|
243
|
+
return; // 无回调,发送端超时
|
|
244
|
+
const peerId = this._peer;
|
|
245
|
+
const results = this._messageHandlers.map((h) => Promise.resolve().then(() => h(data, id, peerId)).catch((e) => {
|
|
246
|
+
console.error('[LightAppBridge] onMessage handler error:', e);
|
|
247
|
+
return undefined;
|
|
248
|
+
}));
|
|
249
|
+
Promise.allSettled(results).then((settled) => {
|
|
250
|
+
for (const s of settled) {
|
|
251
|
+
if (s.status === 'fulfilled' && s.value !== undefined) {
|
|
252
|
+
this._send(this._peer, this._envelope('res', s.value, id));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
/** 收到 res:按 id 唤醒等待中的 send */
|
|
259
|
+
_serveRes(envelope) {
|
|
260
|
+
const p = this._pending.get(envelope.id);
|
|
261
|
+
if (!p)
|
|
262
|
+
return;
|
|
263
|
+
clearTimeout(p.timer);
|
|
264
|
+
this._pending.delete(envelope.id);
|
|
265
|
+
p.resolve(envelope.data);
|
|
266
|
+
}
|
|
267
|
+
/** 底层发送:把信封投递到指定 webContentsId */
|
|
268
|
+
_send(webContentsId, envelope) {
|
|
269
|
+
const invoke = getInvoke();
|
|
270
|
+
if (!invoke)
|
|
271
|
+
throw new Error('worklink.invokeModuleMethodSync 不可用');
|
|
272
|
+
if (webContentsId == null)
|
|
273
|
+
throw new Error('目标 webContentsId 为空,无法发送');
|
|
274
|
+
invoke('lightApp.pluginLightApp.sendMessageToPluginLightAppById', webContentsId, envelope);
|
|
275
|
+
}
|
|
276
|
+
/** 获取自身 webContentsId */
|
|
277
|
+
getId() {
|
|
278
|
+
const invoke = getInvoke();
|
|
279
|
+
if (!invoke)
|
|
280
|
+
return null;
|
|
281
|
+
const info = invoke('lightApp.pluginLightApp.getWebContentInfo');
|
|
282
|
+
return info && info.webContentsId != null ? info.webContentsId : null;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* 发送消息(请求),自动生成 id,等对端用相同 id 回复。
|
|
286
|
+
* 内部会先等待连接建立(握手完成)。
|
|
287
|
+
* @param data 任意业务数据
|
|
288
|
+
* @param options 本次超时(ms),不传用实例默认值(默认 3000)
|
|
289
|
+
* @returns 对端回调返回的数据
|
|
290
|
+
*/
|
|
291
|
+
send(data, options = {}) {
|
|
292
|
+
const timeout = options.timeout != null ? options.timeout : this.timeout;
|
|
293
|
+
return this.ready().then((targetId) => {
|
|
294
|
+
const id = genId();
|
|
295
|
+
const envelope = this._envelope('req', data, id);
|
|
296
|
+
return new Promise((resolve, reject) => {
|
|
297
|
+
const timer = setTimeout(() => {
|
|
298
|
+
if (this._pending.has(id)) {
|
|
299
|
+
this._pending.delete(id);
|
|
300
|
+
reject(new Error(`请求超时 (${timeout}ms)`));
|
|
301
|
+
}
|
|
302
|
+
}, timeout);
|
|
303
|
+
this._pending.set(id, { resolve, reject, timer });
|
|
304
|
+
try {
|
|
305
|
+
this._send(targetId, envelope);
|
|
306
|
+
}
|
|
307
|
+
catch (e) {
|
|
308
|
+
clearTimeout(timer);
|
|
309
|
+
this._pending.delete(id);
|
|
310
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* 监听收到的请求消息。回调的返回值(含 Promise)即回复内容。
|
|
317
|
+
* @returns 调用以取消监听
|
|
318
|
+
*/
|
|
319
|
+
onMessage(handler) {
|
|
320
|
+
this._messageHandlers.push(handler);
|
|
321
|
+
return () => {
|
|
322
|
+
const i = this._messageHandlers.indexOf(handler);
|
|
323
|
+
if (i >= 0)
|
|
324
|
+
this._messageHandlers.splice(i, 1);
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* 工厂函数
|
|
330
|
+
* @param role 角色,省略时自动探测
|
|
331
|
+
* @param options 超时配置
|
|
332
|
+
*/
|
|
333
|
+
export function createBridge(role, options) {
|
|
334
|
+
return new LightAppBridge(role, options);
|
|
335
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jhh96/light-app-bridge",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "招商随行(WorkLink)插件窗口(plugin)与轻应用窗口(lightapp)之间的双向通信 SDK",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./lightAppBridge": {
|
|
15
|
+
"types": "./dist/lightAppBridge.d.ts",
|
|
16
|
+
"import": "./dist/lightAppBridge.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -p tsconfig.build.json && node scripts/fix-esm-extensions.mjs",
|
|
25
|
+
"clean": "rm -rf dist"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"typescript": "~5.6.3"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
}
|
|
33
|
+
}
|