@northsea4/tab-proxy-service 0.2.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/LICENSE +21 -0
- package/README.md +627 -0
- package/lib/index.d.ts +247 -0
- package/lib/index.js +329 -0
- package/lib/index.js.map +1 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Northsea4
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
# @northsea4/tab-proxy-service
|
|
2
|
+
|
|
3
|
+
一个类型安全的 WebExtension 工具库,支持在 content script 中注册 service,并允许从 background 或其他 tab 调用这些 service。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
✅ **在 Content Script 中注册 Service** - 与 `@webext-core/proxy-service` 相反,支持在 tab 中注册服务
|
|
8
|
+
✅ **Background 调用 Tab Service** - 从 background script 调用任意 tab 中的服务
|
|
9
|
+
✅ **Tab 间通信** - 支持一个 tab 调用另一个 tab 中的服务(通过 background 转发)
|
|
10
|
+
✅ **完整的类型安全** - 使用 TypeScript 泛型确保类型安全
|
|
11
|
+
✅ **自动类型推断** - 使用 `defineTabProxyService` 自动获得类型提示
|
|
12
|
+
✅ **嵌套对象支持** - 支持深层嵌套的对象和方法调用
|
|
13
|
+
✅ **自动清理** - Tab 关闭时自动注销服务
|
|
14
|
+
✅ **查询功能** - 可查询所有已注册的 tab services
|
|
15
|
+
|
|
16
|
+
## 安装
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# 使用 pnpm
|
|
20
|
+
pnpm add @northsea4/tab-proxy-service
|
|
21
|
+
|
|
22
|
+
# 使用 npm
|
|
23
|
+
npm install @northsea4/tab-proxy-service
|
|
24
|
+
|
|
25
|
+
# 使用 yarn
|
|
26
|
+
yarn add @northsea4/tab-proxy-service
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 快速开始
|
|
30
|
+
|
|
31
|
+
> **💡 推荐**: 使用 `defineTabProxyService` API,它会自动提供完整的类型提示,无需手动传递泛型类型。
|
|
32
|
+
|
|
33
|
+
### 步骤 1: 定义服务
|
|
34
|
+
|
|
35
|
+
支持三种服务定义方式:
|
|
36
|
+
|
|
37
|
+
#### 方式 A: 使用 Class (推荐)
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// services/hello-service.ts
|
|
41
|
+
import { defineTabProxyService } from '@northsea4/tab-proxy-service';
|
|
42
|
+
|
|
43
|
+
// 使用 class 定义服务
|
|
44
|
+
class HelloService {
|
|
45
|
+
async sayHello(name: string): Promise<string> {
|
|
46
|
+
return `Hello, ${name}! From tab: ${document.title}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async getTabInfo(): Promise<{ title: string; url: string }> {
|
|
50
|
+
return {
|
|
51
|
+
title: document.title,
|
|
52
|
+
url: window.location.href
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 定义服务 - 自动获得类型提示! 🎉
|
|
58
|
+
export const [registerHelloService, getHelloService] = defineTabProxyService(
|
|
59
|
+
'hello-service',
|
|
60
|
+
() => new HelloService()
|
|
61
|
+
);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
#### 方式 B: 使用对象字面量
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
export const [registerPageService, getPageService] = defineTabProxyService(
|
|
68
|
+
'page-service',
|
|
69
|
+
() => ({
|
|
70
|
+
async getTitle() {
|
|
71
|
+
return document.title;
|
|
72
|
+
},
|
|
73
|
+
async getUrl() {
|
|
74
|
+
return window.location.href;
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
#### 方式 C: 带参数的服务
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
class ConfigService {
|
|
84
|
+
constructor(private apiUrl: string) {}
|
|
85
|
+
|
|
86
|
+
async fetchConfig() {
|
|
87
|
+
return fetch(`${this.apiUrl}/config`).then(r => r.json());
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 支持初始化参数
|
|
92
|
+
export const [registerConfigService, getConfigService] = defineTabProxyService(
|
|
93
|
+
'config-service',
|
|
94
|
+
(apiUrl: string) => new ConfigService(apiUrl)
|
|
95
|
+
);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 步骤 2: 在 Content Script 中注册服务
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// content-script.ts
|
|
102
|
+
import { registerHelloService } from './services/hello-service';
|
|
103
|
+
|
|
104
|
+
// 注册服务
|
|
105
|
+
const cleanup = await registerHelloService();
|
|
106
|
+
|
|
107
|
+
// 页面卸载时清理
|
|
108
|
+
window.addEventListener('beforeunload', cleanup);
|
|
109
|
+
|
|
110
|
+
// 如果服务需要参数
|
|
111
|
+
// const cleanup = await registerConfigService('https://api.example.com');
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 步骤 3: 在 Background 中调用服务
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
// background.ts
|
|
118
|
+
import { getHelloService } from './services/hello-service';
|
|
119
|
+
|
|
120
|
+
// 获取服务 - 自动有完整的类型提示和自动补全! 🚀
|
|
121
|
+
const helloService = getHelloService({ targetTabId: 123 });
|
|
122
|
+
|
|
123
|
+
// sayHello 方法会有自动补全和类型检查 ✨
|
|
124
|
+
const greeting = await helloService.sayHello('World');
|
|
125
|
+
const info = await helloService.getTabInfo();
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 传统方式 (不推荐)
|
|
131
|
+
|
|
132
|
+
> **⚠️ 注意**: 以下方式需要手动传递类型参数,建议使用上面的 `defineTabProxyService` 方式。
|
|
133
|
+
|
|
134
|
+
使用 `registerTabService` 和 `createTabProxyService` (需要手动传递类型参数):
|
|
135
|
+
|
|
136
|
+
### 1. 在 Background 中初始化管理器
|
|
137
|
+
|
|
138
|
+
首先,必须在 background script 中初始化 tab service 管理器:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
// background.ts
|
|
142
|
+
import { initTabServiceManager } from '@northsea4/tab-proxy-service';
|
|
143
|
+
|
|
144
|
+
// 启动时初始化
|
|
145
|
+
initTabServiceManager({
|
|
146
|
+
logger: console, // 可选:启用日志
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### 2. 在 Content Script 中注册 Service
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
// content-script.ts
|
|
154
|
+
import { registerTabService } from '@northsea4/tab-proxy-service';
|
|
155
|
+
|
|
156
|
+
// 定义你的 service
|
|
157
|
+
const pageService = {
|
|
158
|
+
async getPageTitle(): Promise<string> {
|
|
159
|
+
return document.title;
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
async getPageUrl(): Promise<string> {
|
|
163
|
+
return window.location.href;
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
async extractData(): Promise<any[]> {
|
|
167
|
+
// 提取页面数据
|
|
168
|
+
return Array.from(document.querySelectorAll('.item')).map(el => ({
|
|
169
|
+
text: el.textContent,
|
|
170
|
+
href: el.getAttribute('href'),
|
|
171
|
+
}));
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// 注册 service
|
|
176
|
+
const cleanup = await registerTabService('page-service', pageService);
|
|
177
|
+
|
|
178
|
+
// 页面卸载时清理
|
|
179
|
+
window.addEventListener('beforeunload', cleanup);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 3. 从 Background 调用 Tab Service
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
// background.ts
|
|
186
|
+
import { createTabProxyService, type TabProxyServiceKey } from '@northsea4/tab-proxy-service';
|
|
187
|
+
|
|
188
|
+
// 定义类型
|
|
189
|
+
interface PageService {
|
|
190
|
+
getPageTitle(): Promise<string>;
|
|
191
|
+
getPageUrl(): Promise<string>;
|
|
192
|
+
extractData(): Promise<any[]>;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 创建代理(会调用任意一个注册了该 service 的 tab)
|
|
196
|
+
const pageService = createTabProxyService<PageService>('page-service');
|
|
197
|
+
|
|
198
|
+
// 使用
|
|
199
|
+
const title = await pageService.getPageTitle();
|
|
200
|
+
const data = await pageService.extractData();
|
|
201
|
+
|
|
202
|
+
console.log('Page title:', title);
|
|
203
|
+
console.log('Extracted data:', data);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 4. 调用特定 Tab 中的 Service
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
// background.ts 或 另一个 content-script.ts
|
|
210
|
+
import { createTabProxyService } from '@northsea4/tab-proxy-service';
|
|
211
|
+
|
|
212
|
+
// 指定目标 tab ID
|
|
213
|
+
const specificTabService = createTabProxyService<PageService>('page-service', {
|
|
214
|
+
targetTabId: 123, // 目标 tab 的 ID
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const title = await specificTabService.getPageTitle();
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### 5. Tab 间通信示例
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
// content-script-a.ts (Tab A)
|
|
224
|
+
import { registerTabService } from '@northsea4/tab-proxy-service';
|
|
225
|
+
|
|
226
|
+
const dataService = {
|
|
227
|
+
async shareData() {
|
|
228
|
+
return { data: 'from tab A', timestamp: Date.now() };
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
await registerTabService('data-service', dataService);
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
// content-script-b.ts (Tab B)
|
|
237
|
+
import { createTabProxyService } from '@northsea4/tab-proxy-service';
|
|
238
|
+
|
|
239
|
+
// Tab B 调用 Tab A 中的服务
|
|
240
|
+
const dataService = createTabProxyService('data-service', {
|
|
241
|
+
targetTabId: tabAId, // Tab A 的 ID
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const data = await dataService.shareData();
|
|
245
|
+
console.log('Data from Tab A:', data);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## 类型安全最佳实践
|
|
249
|
+
|
|
250
|
+
### 方式 1: 使用 `defineTabProxyService` (推荐)
|
|
251
|
+
|
|
252
|
+
这是最简单的方式,自动获得类型提示:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
// services/page-service.ts
|
|
256
|
+
import { defineTabProxyService } from '@northsea4/tab-proxy-service';
|
|
257
|
+
|
|
258
|
+
class PageService {
|
|
259
|
+
async getTitle(): Promise<string> {
|
|
260
|
+
return document.title;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async extractLinks(): Promise<string[]> {
|
|
264
|
+
return Array.from(document.querySelectorAll('a'))
|
|
265
|
+
.map(a => a.href)
|
|
266
|
+
.filter(Boolean);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// 导出注册和获取函数
|
|
271
|
+
export const [registerPageService, getPageService] = defineTabProxyService(
|
|
272
|
+
'page-service',
|
|
273
|
+
() => new PageService()
|
|
274
|
+
);
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**使用:**
|
|
278
|
+
```ts
|
|
279
|
+
// content-script.ts
|
|
280
|
+
import { registerPageService } from './services/page-service';
|
|
281
|
+
await registerPageService();
|
|
282
|
+
|
|
283
|
+
// background.ts
|
|
284
|
+
import { getPageService } from './services/page-service';
|
|
285
|
+
const pageService = getPageService({ targetTabId: 123 });
|
|
286
|
+
const title = await pageService.getTitle(); // ✅ 自动类型提示
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### 方式 2: 使用 `TabProxyServiceKey`
|
|
290
|
+
|
|
291
|
+
如果使用传统方式,推荐使用类型约束的 service key:
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
// service-keys.ts
|
|
295
|
+
import type { TabProxyServiceKey } from '@northsea4/tab-proxy-service';
|
|
296
|
+
import type { PageService } from './page-service';
|
|
297
|
+
|
|
298
|
+
// 使用类型约束的 key
|
|
299
|
+
export const PAGE_SERVICE_KEY = 'page-service' as TabProxyServiceKey<PageService>;
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
// content-script.ts
|
|
304
|
+
import { registerTabService } from '@northsea4/tab-proxy-service';
|
|
305
|
+
import { PAGE_SERVICE_KEY } from './service-keys';
|
|
306
|
+
import { createPageService } from './page-service';
|
|
307
|
+
|
|
308
|
+
const pageService = createPageService();
|
|
309
|
+
await registerTabService(PAGE_SERVICE_KEY, pageService);
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
// background.ts
|
|
314
|
+
import { createTabProxyService } from '@northsea4/tab-proxy-service';
|
|
315
|
+
import { PAGE_SERVICE_KEY } from './service-keys';
|
|
316
|
+
|
|
317
|
+
// 自动推断类型,无需手动指定泛型
|
|
318
|
+
const pageService = createTabProxyService(PAGE_SERVICE_KEY);
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## 查询已注册的 Services
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
import { queryTabServices } from '@northsea4/tab-proxy-service';
|
|
325
|
+
|
|
326
|
+
const services = await queryTabServices();
|
|
327
|
+
console.log('Registered services:', services);
|
|
328
|
+
// [
|
|
329
|
+
// { tabId: 123, serviceKey: 'page-service', registeredAt: 1234567890 },
|
|
330
|
+
// { tabId: 456, serviceKey: 'data-service', registeredAt: 1234567891 },
|
|
331
|
+
// ]
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## 支持的 Service 类型
|
|
335
|
+
|
|
336
|
+
### 1. Class(推荐)
|
|
337
|
+
|
|
338
|
+
使用 class 定义服务提供了最好的代码组织和可维护性:
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
// services/data-service.ts
|
|
342
|
+
import { defineTabProxyService } from '@northsea4/tab-proxy-service';
|
|
343
|
+
|
|
344
|
+
class DataService {
|
|
345
|
+
private cache = new Map<string, any>();
|
|
346
|
+
|
|
347
|
+
constructor(private apiUrl?: string) {}
|
|
348
|
+
|
|
349
|
+
async getData(key: string): Promise<any> {
|
|
350
|
+
if (this.cache.has(key)) {
|
|
351
|
+
return this.cache.get(key);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const data = await fetch(`${this.apiUrl || ''}/data/${key}`).then(r => r.json());
|
|
355
|
+
this.cache.set(key, data);
|
|
356
|
+
return data;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async setData(key: string, value: any): Promise<void> {
|
|
360
|
+
this.cache.set(key, value);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async clearCache(): Promise<void> {
|
|
364
|
+
this.cache.clear();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// 定义服务
|
|
369
|
+
export const [registerDataService, getDataService] = defineTabProxyService(
|
|
370
|
+
'data-service',
|
|
371
|
+
(apiUrl?: string) => new DataService(apiUrl)
|
|
372
|
+
);
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**优点:**
|
|
376
|
+
- ✅ 支持私有成员和状态管理
|
|
377
|
+
- ✅ 支持构造函数参数
|
|
378
|
+
- ✅ 良好的代码组织
|
|
379
|
+
- ✅ 完整的 IDE 支持
|
|
380
|
+
|
|
381
|
+
**使用:**
|
|
382
|
+
```ts
|
|
383
|
+
// content-script.ts
|
|
384
|
+
const cleanup = await registerDataService('https://api.example.com');
|
|
385
|
+
|
|
386
|
+
// background.ts
|
|
387
|
+
const dataService = getDataService({ targetTabId: 123 });
|
|
388
|
+
const data = await dataService.getData('users');
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### 2. 对象字面量
|
|
392
|
+
|
|
393
|
+
简单场景可以使用对象字面量:
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
export const [registerMyService, getMyService] = defineTabProxyService(
|
|
397
|
+
'my-service',
|
|
398
|
+
() => ({
|
|
399
|
+
async getData(): Promise<Data> {
|
|
400
|
+
// ...
|
|
401
|
+
},
|
|
402
|
+
async setData(data: Data): Promise<void> {
|
|
403
|
+
// ...
|
|
404
|
+
},
|
|
405
|
+
})
|
|
406
|
+
);
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### 3. 函数
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
async function myFunction(arg: string): Promise<number> {
|
|
413
|
+
// ...
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export const [registerMyFunction, getMyFunction] = defineTabProxyService(
|
|
417
|
+
'my-function',
|
|
418
|
+
() => myFunction
|
|
419
|
+
);
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### 4. 嵌套对象
|
|
423
|
+
|
|
424
|
+
```ts
|
|
425
|
+
### 4. 嵌套对象
|
|
426
|
+
|
|
427
|
+
支持深层嵌套的对象和方法调用:
|
|
428
|
+
|
|
429
|
+
```ts
|
|
430
|
+
export const [registerApiService, getApiService] = defineTabProxyService(
|
|
431
|
+
'api-service',
|
|
432
|
+
() => ({
|
|
433
|
+
users: {
|
|
434
|
+
async list(): Promise<User[]> { /* ... */ },
|
|
435
|
+
async get(id: string): Promise<User> { /* ... */ },
|
|
436
|
+
},
|
|
437
|
+
posts: {
|
|
438
|
+
async list(): Promise<Post[]> { /* ... */ },
|
|
439
|
+
async create(post: Post): Promise<void> { /* ... */ },
|
|
440
|
+
},
|
|
441
|
+
})
|
|
442
|
+
);
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**使用:**
|
|
446
|
+
```ts
|
|
447
|
+
const api = getApiService({ targetTabId: 123 });
|
|
448
|
+
const users = await api.users.list();
|
|
449
|
+
const posts = await api.posts.list();
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
## 辅助工具
|
|
453
|
+
|
|
454
|
+
### flattenPromise
|
|
455
|
+
|
|
456
|
+
用于简化处理 `Promise<Dependency>`:
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
import { flattenPromise } from '@northsea4/tab-proxy-service';
|
|
460
|
+
|
|
461
|
+
function createService(dbPromise: Promise<Database>) {
|
|
462
|
+
const db = flattenPromise(dbPromise);
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
async getData() {
|
|
466
|
+
// 不需要 await (await dbPromise).query()
|
|
467
|
+
return await db.query('SELECT * FROM table');
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
## 与 @webext-core/proxy-service 的对比
|
|
474
|
+
|
|
475
|
+
| 功能 | @webext-core/proxy-service | @northsea4/tab-proxy-service |
|
|
476
|
+
|------|----------------------------|------------------------------|
|
|
477
|
+
| 在 Background 注册服务 | ✅ | ❌ |
|
|
478
|
+
| 在 Content Script 注册服务 | ❌ | ✅ |
|
|
479
|
+
| Background → Content 调用 | ❌ | ✅ |
|
|
480
|
+
| Content → Background 调用 | ✅ | 使用 @webext-core/proxy-service |
|
|
481
|
+
| Tab → Tab 调用 | ❌ | ✅ |
|
|
482
|
+
| 类型安全 | ✅ | ✅ |
|
|
483
|
+
| 嵌套对象支持 | ✅ | ✅ |
|
|
484
|
+
|
|
485
|
+
**建议**: 两个库可以配合使用:
|
|
486
|
+
- 使用 `@webext-core/proxy-service` 在 background 注册服务,供其他上下文调用
|
|
487
|
+
- 使用 `@northsea4/tab-proxy-service` 在 content script 注册服务,供 background 或其他 tab 调用
|
|
488
|
+
|
|
489
|
+
## 完整示例
|
|
490
|
+
|
|
491
|
+
```ts
|
|
492
|
+
// ===== services/page-service.ts =====
|
|
493
|
+
import { defineTabProxyService } from '@northsea4/tab-proxy-service';
|
|
494
|
+
|
|
495
|
+
class PageService {
|
|
496
|
+
async getTitle(): Promise<string> {
|
|
497
|
+
return document.title;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async extractLinks(): Promise<string[]> {
|
|
501
|
+
return Array.from(document.querySelectorAll('a'))
|
|
502
|
+
.map(a => a.href)
|
|
503
|
+
.filter(Boolean);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export const [registerPageService, getPageService] = defineTabProxyService(
|
|
508
|
+
'page-service',
|
|
509
|
+
() => new PageService()
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
// ===== content-script.ts =====
|
|
513
|
+
import { registerPageService } from './services/page-service';
|
|
514
|
+
|
|
515
|
+
// 注册服务
|
|
516
|
+
const cleanup = await registerPageService();
|
|
517
|
+
|
|
518
|
+
// 清理
|
|
519
|
+
window.addEventListener('beforeunload', cleanup);
|
|
520
|
+
|
|
521
|
+
// ===== background.ts =====
|
|
522
|
+
import { initTabServiceManager } from '@northsea4/tab-proxy-service';
|
|
523
|
+
import { getPageService } from './services/page-service';
|
|
524
|
+
|
|
525
|
+
// 初始化管理器
|
|
526
|
+
initTabServiceManager({ logger: console });
|
|
527
|
+
|
|
528
|
+
// 监听扩展图标点击
|
|
529
|
+
browser.action.onClicked.addListener(async (tab) => {
|
|
530
|
+
if (!tab.id) return;
|
|
531
|
+
|
|
532
|
+
// 调用当前 tab 中的服务
|
|
533
|
+
const pageService = getPageService({ targetTabId: tab.id });
|
|
534
|
+
|
|
535
|
+
try {
|
|
536
|
+
const title = await pageService.getTitle();
|
|
537
|
+
const links = await pageService.extractLinks();
|
|
538
|
+
|
|
539
|
+
console.log('Page title:', title);
|
|
540
|
+
console.log('Links found:', links.length);
|
|
541
|
+
} catch (error) {
|
|
542
|
+
console.error('Failed to call service:', error);
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
## API 参考
|
|
548
|
+
|
|
549
|
+
### `defineTabProxyService(name, init, config?)` (推荐)
|
|
550
|
+
|
|
551
|
+
定义一个 tab proxy service 的辅助函数,自动提供类型提示。
|
|
552
|
+
|
|
553
|
+
**参数:**
|
|
554
|
+
- `name: string` - Service 的唯一名称
|
|
555
|
+
- `init: (...args: TArgs) => TService` - 初始化函数,返回服务实例
|
|
556
|
+
- `config?: TabProxyServiceConfig` - 可选配置
|
|
557
|
+
|
|
558
|
+
**返回:** `[registerService, getService]` - 返回元组
|
|
559
|
+
- `registerService: (...args: TArgs) => Promise<() => void>` - 注册服务函数
|
|
560
|
+
- `getService: (options?) => TabProxyService<TService>` - 获取服务代理函数
|
|
561
|
+
|
|
562
|
+
**示例:**
|
|
563
|
+
```ts
|
|
564
|
+
export const [registerHelloService, getHelloService] = defineTabProxyService(
|
|
565
|
+
'hello-service',
|
|
566
|
+
() => new HelloService()
|
|
567
|
+
);
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### `initTabServiceManager(config?)`
|
|
571
|
+
|
|
572
|
+
在 background 中初始化 tab service 管理器。必须在使用其他功能前调用。
|
|
573
|
+
|
|
574
|
+
**参数:**
|
|
575
|
+
- `config?: TabProxyServiceConfig` - 可选配置
|
|
576
|
+
- `logger?: Console` - 日志记录器
|
|
577
|
+
- `timeout?: number` - 消息超时时间(毫秒)
|
|
578
|
+
|
|
579
|
+
### `registerTabService(key, service, config?)`
|
|
580
|
+
|
|
581
|
+
在 content script 中注册一个服务。
|
|
582
|
+
|
|
583
|
+
**参数:**
|
|
584
|
+
- `key: string | TabProxyServiceKey<T>` - Service key
|
|
585
|
+
- `service: T` - 服务实例
|
|
586
|
+
- `config?: TabProxyServiceConfig` - 可选配置
|
|
587
|
+
|
|
588
|
+
**返回:** `Promise<() => void>` - 返回清理函数
|
|
589
|
+
|
|
590
|
+
### `createTabProxyService(key, options?)`
|
|
591
|
+
|
|
592
|
+
创建一个 tab service 的代理。
|
|
593
|
+
|
|
594
|
+
**参数:**
|
|
595
|
+
- `key: string | TabProxyServiceKey<T>` - Service key
|
|
596
|
+
- `options?`:
|
|
597
|
+
- `targetTabId?: number` - 目标 tab ID(可选)
|
|
598
|
+
- `config?: TabProxyServiceConfig` - 配置(可选)
|
|
599
|
+
|
|
600
|
+
**返回:** `TabProxyService<T>` - Service 代理
|
|
601
|
+
|
|
602
|
+
### `queryTabServices()`
|
|
603
|
+
|
|
604
|
+
查询所有已注册的 tab services。
|
|
605
|
+
|
|
606
|
+
**返回:** `Promise<TabServiceInfo[]>` - Service 信息列表
|
|
607
|
+
|
|
608
|
+
### `flattenPromise(promise)`
|
|
609
|
+
|
|
610
|
+
扁平化 Promise,用于简化处理 `Promise<Dependency>`。
|
|
611
|
+
|
|
612
|
+
**参数:**
|
|
613
|
+
- `promise: Promise<T>` - 要扁平化的 Promise
|
|
614
|
+
|
|
615
|
+
**返回:** `TabProxyService<T>` - 扁平化后的代理
|
|
616
|
+
|
|
617
|
+
## License
|
|
618
|
+
|
|
619
|
+
MIT
|
|
620
|
+
|
|
621
|
+
## 作者
|
|
622
|
+
|
|
623
|
+
nornorlunn
|
|
624
|
+
|
|
625
|
+
## 致谢
|
|
626
|
+
|
|
627
|
+
本项目受 [@webext-core/proxy-service](https://github.com/aklinker1/webext-core) 启发,实现了相反方向的代理服务功能。
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 将类型转换为 Promise 类型(如果还不是)
|
|
3
|
+
*/
|
|
4
|
+
type Promisify<T> = T extends Promise<any> ? T : Promise<T>;
|
|
5
|
+
/**
|
|
6
|
+
* Service 可以是函数、对象或嵌套的服务集合
|
|
7
|
+
*/
|
|
8
|
+
type Service = ((...args: any[]) => Promise<any>) | {
|
|
9
|
+
[key: string]: any | Service;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* 递归地将 service 中的所有方法转换为异步方法
|
|
13
|
+
*/
|
|
14
|
+
type DeepAsync<TService> = TService extends (...args: any) => any ? ToAsyncFunction<TService> : TService extends {
|
|
15
|
+
[key: string]: any;
|
|
16
|
+
} ? {
|
|
17
|
+
[fn in keyof TService]: DeepAsync<TService[fn]>;
|
|
18
|
+
} : never;
|
|
19
|
+
type ToAsyncFunction<T extends (...args: any) => any> = (...args: Parameters<T>) => Promisify<ReturnType<T>>;
|
|
20
|
+
/**
|
|
21
|
+
* Tab Proxy Service 类型,确保所有方法都是异步的
|
|
22
|
+
*/
|
|
23
|
+
type TabProxyService<T> = T extends DeepAsync<T> ? T : DeepAsync<T>;
|
|
24
|
+
/**
|
|
25
|
+
* 约束 service key 的类型
|
|
26
|
+
*/
|
|
27
|
+
interface TabProxyServiceConstraint<_> {
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 带有类型约束的 service key,用于确保类型安全
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* import type { TabProxyServiceKey } from '@northsea4/tab-proxy-service';
|
|
35
|
+
* import type { MyService } from './my-service';
|
|
36
|
+
*
|
|
37
|
+
* export const MY_SERVICE_KEY = 'my-service' as TabProxyServiceKey<MyService>;
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
type TabProxyServiceKey<T> = string & TabProxyServiceConstraint<T>;
|
|
41
|
+
/**
|
|
42
|
+
* Service 方法调用时的上下文信息
|
|
43
|
+
* 作为服务方法的最后一个可选参数传递
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* class MyService {
|
|
48
|
+
* async doSomething(arg1: string, context?: ServiceCallContext) {
|
|
49
|
+
* console.log('Called by:', context?.originalSender?.tab?.title)
|
|
50
|
+
* console.log('Request ID:', context?.requestId)
|
|
51
|
+
* }
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
interface ServiceCallContext {
|
|
56
|
+
/** 当前消息的 sender (转发消息时是 background 的 sender) */
|
|
57
|
+
sender: any;
|
|
58
|
+
/** 原始调用方的 sender (真正发起调用的地方) */
|
|
59
|
+
originalSender?: any;
|
|
60
|
+
/** 调用时间戳 */
|
|
61
|
+
timestamp?: number;
|
|
62
|
+
/** 请求 ID,用于追踪和调试 */
|
|
63
|
+
requestId?: string;
|
|
64
|
+
/** 其他元数据 */
|
|
65
|
+
metadata?: Record<string, any>;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 消息协议定义
|
|
69
|
+
*/
|
|
70
|
+
interface ProxyMessage {
|
|
71
|
+
/** 调用路径(用于嵌套对象) */
|
|
72
|
+
path?: string[];
|
|
73
|
+
/** 函数参数 */
|
|
74
|
+
args: any[];
|
|
75
|
+
/** 目标 tab ID(可选,用于 tab-to-tab 通信) */
|
|
76
|
+
targetTabId?: number;
|
|
77
|
+
/** 原始调用方的 sender */
|
|
78
|
+
originalSender?: any;
|
|
79
|
+
/** 调用时间戳 */
|
|
80
|
+
timestamp?: number;
|
|
81
|
+
/** 请求 ID */
|
|
82
|
+
requestId?: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 消息响应类型
|
|
86
|
+
*/
|
|
87
|
+
type ProxyResponse = any;
|
|
88
|
+
/**
|
|
89
|
+
* Tab Service 注册信息
|
|
90
|
+
*/
|
|
91
|
+
interface TabServiceInfo {
|
|
92
|
+
/** Tab ID */
|
|
93
|
+
tabId: number;
|
|
94
|
+
/** Service key */
|
|
95
|
+
serviceKey: string;
|
|
96
|
+
/** 注册时间 */
|
|
97
|
+
registeredAt: number;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Service 配置选项
|
|
101
|
+
*/
|
|
102
|
+
interface TabProxyServiceConfig {
|
|
103
|
+
/** 日志记录器 */
|
|
104
|
+
logger?: Pick<Console, 'log' | 'warn' | 'error' | 'debug'>;
|
|
105
|
+
/** 消息超时时间(毫秒) */
|
|
106
|
+
timeout?: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 在 background 中初始化 tab service 管理器
|
|
111
|
+
* 必须在 background script 启动时调用
|
|
112
|
+
*/
|
|
113
|
+
declare function initTabServiceManager(config?: TabProxyServiceConfig): void;
|
|
114
|
+
/**
|
|
115
|
+
* 在 content script 中注册一个 service
|
|
116
|
+
*
|
|
117
|
+
* @param key Service key
|
|
118
|
+
* @param realService 实际的 service 实例
|
|
119
|
+
* @param config 配置选项
|
|
120
|
+
* @returns 返回清理函数,调用后注销 service
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```ts
|
|
124
|
+
* // content-script.ts
|
|
125
|
+
* import { registerTabService } from '@northsea4/tab-proxy-service';
|
|
126
|
+
*
|
|
127
|
+
* const myService = {
|
|
128
|
+
* async getData() {
|
|
129
|
+
* return [1, 2, 3];
|
|
130
|
+
* }
|
|
131
|
+
* };
|
|
132
|
+
*
|
|
133
|
+
* const cleanup = await registerTabService('my-service', myService);
|
|
134
|
+
*
|
|
135
|
+
* // 页面卸载时清理
|
|
136
|
+
* window.addEventListener('beforeunload', cleanup);
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
declare function registerTabService<T extends Service, K extends string = TabProxyServiceKey<T> | string>(key: K, realService: T, config?: TabProxyServiceConfig): Promise<() => void>;
|
|
140
|
+
/**
|
|
141
|
+
* 创建一个 tab service 的代理
|
|
142
|
+
* 可以在 background、popup 或其他 content script 中调用
|
|
143
|
+
*
|
|
144
|
+
* @param key Service key
|
|
145
|
+
* @param options 可选参数:targetTabId 指定目标 tab,config 配置
|
|
146
|
+
* @returns 返回 service 代理
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```ts
|
|
150
|
+
* // background.ts - 调用任意 tab 中的 service
|
|
151
|
+
* import { createTabProxyService } from '@northsea4/tab-proxy-service';
|
|
152
|
+
*
|
|
153
|
+
* const myService = createTabProxyService<MyService>('my-service');
|
|
154
|
+
* const data = await myService.getData();
|
|
155
|
+
*
|
|
156
|
+
* // 或者指定特定的 tab
|
|
157
|
+
* const myServiceInTab = createTabProxyService<MyService>('my-service', { targetTabId: 123 });
|
|
158
|
+
* ```
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* ```ts
|
|
162
|
+
* // popup.ts - 从 popup 调用 tab 中的 service
|
|
163
|
+
* const myService = createTabProxyService<MyService>('my-service', { targetTabId: 123 });
|
|
164
|
+
* const result = await myService.someMethod();
|
|
165
|
+
* ```
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* ```ts
|
|
169
|
+
* // content-script-a.ts - 调用另一个 tab 中的 service
|
|
170
|
+
* const serviceInOtherTab = createTabProxyService<OtherService>('other-service', { targetTabId: 456 });
|
|
171
|
+
* await serviceInOtherTab.doSomething();
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
declare function createTabProxyService<T extends Service>(key: TabProxyServiceKey<T> | string, options?: {
|
|
175
|
+
targetTabId?: number;
|
|
176
|
+
config?: TabProxyServiceConfig;
|
|
177
|
+
}): TabProxyService<T>;
|
|
178
|
+
/**
|
|
179
|
+
* 查询所有已注册的 tab services
|
|
180
|
+
*
|
|
181
|
+
* @returns 返回所有已注册的 service 信息
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```ts
|
|
185
|
+
* const services = await queryTabServices();
|
|
186
|
+
* console.log('Registered services:', services);
|
|
187
|
+
* // [{ tabId: 123, serviceKey: 'my-service', registeredAt: 1234567890 }]
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
declare function queryTabServices(): Promise<TabServiceInfo[]>;
|
|
191
|
+
/**
|
|
192
|
+
* 定义一个 tab proxy service 的辅助函数
|
|
193
|
+
* 类似于 @northsea4/proxy-service 的 defineProxyService
|
|
194
|
+
*
|
|
195
|
+
* @param name Service 的唯一名称
|
|
196
|
+
* @param init 初始化函数,返回实际的 service 实例
|
|
197
|
+
* @param config 配置选项
|
|
198
|
+
* @returns 返回 [registerService, getService] 元组
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```ts
|
|
202
|
+
* // hello-service.ts
|
|
203
|
+
* export const [registerHelloService, getHelloService] = defineTabProxyService(
|
|
204
|
+
* 'hello-service',
|
|
205
|
+
* () => ({
|
|
206
|
+
* async sayHello(name: string) {
|
|
207
|
+
* return `Hello, ${name}!`;
|
|
208
|
+
* }
|
|
209
|
+
* })
|
|
210
|
+
* );
|
|
211
|
+
*
|
|
212
|
+
* // content-script.ts - 注册服务
|
|
213
|
+
* const cleanup = await registerHelloService();
|
|
214
|
+
*
|
|
215
|
+
* // background.ts - 使用服务(自动有类型提示)
|
|
216
|
+
* const helloService = getHelloService({ targetTabId: 123 });
|
|
217
|
+
* const greeting = await helloService.sayHello('World'); // 类型安全!
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
declare function defineTabProxyService<TService extends Service, TArgs extends any[]>(name: string, init: (...args: TArgs) => TService, config?: TabProxyServiceConfig): [
|
|
221
|
+
registerService: (...args: TArgs) => Promise<() => void>,
|
|
222
|
+
getService: (options?: {
|
|
223
|
+
targetTabId?: number;
|
|
224
|
+
config?: TabProxyServiceConfig;
|
|
225
|
+
}) => TabProxyService<TService>
|
|
226
|
+
];
|
|
227
|
+
/**
|
|
228
|
+
* 辅助函数:扁平化 Promise
|
|
229
|
+
* 用于简化处理 Promise<Dependency>
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* ```ts
|
|
233
|
+
* function createService(dependencyPromise: Promise<SomeDependency>) {
|
|
234
|
+
* const dependency = flattenPromise(dependencyPromise);
|
|
235
|
+
*
|
|
236
|
+
* return {
|
|
237
|
+
* async doSomething() {
|
|
238
|
+
* await dependency.someAsyncWork();
|
|
239
|
+
* // 而不是 await (await dependencyPromise).someAsyncWork();
|
|
240
|
+
* }
|
|
241
|
+
* }
|
|
242
|
+
* }
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
declare function flattenPromise<T>(promise: Promise<T>): TabProxyService<T>;
|
|
246
|
+
|
|
247
|
+
export { type DeepAsync, type Promisify, type ProxyMessage, type ProxyResponse, type Service, type ServiceCallContext, type TabProxyService, type TabProxyServiceConfig, type TabProxyServiceKey, type TabServiceInfo, createTabProxyService, defineTabProxyService, flattenPromise, initTabServiceManager, queryTabServices, registerTabService };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import Browser from 'webextension-polyfill';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
var MESSAGE_TYPES = {
|
|
5
|
+
/** Content script 向 background 注册 service */
|
|
6
|
+
REGISTER_TAB_SERVICE: "tab-proxy:register",
|
|
7
|
+
/** Content script 向 background 注销 service */
|
|
8
|
+
UNREGISTER_TAB_SERVICE: "tab-proxy:unregister",
|
|
9
|
+
/** 调用 tab 中的 service */
|
|
10
|
+
CALL_TAB_SERVICE: "tab-proxy:call",
|
|
11
|
+
/** Background 转发消息到目标 tab */
|
|
12
|
+
FORWARD_TO_TAB: "tab-proxy:forward",
|
|
13
|
+
/** 查询已注册的 tab services */
|
|
14
|
+
QUERY_TAB_SERVICES: "tab-proxy:query"
|
|
15
|
+
};
|
|
16
|
+
var TabServiceRegistry = class {
|
|
17
|
+
constructor() {
|
|
18
|
+
/** 存储所有已注册的 tab services: serviceKey -> Set<tabId> */
|
|
19
|
+
this.services = /* @__PURE__ */ new Map();
|
|
20
|
+
/** 存储 service 监听器: tabId -> Map<serviceKey, listener> */
|
|
21
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 注册一个 tab service
|
|
25
|
+
*/
|
|
26
|
+
register(tabId, serviceKey) {
|
|
27
|
+
if (!this.services.has(serviceKey)) {
|
|
28
|
+
this.services.set(serviceKey, /* @__PURE__ */ new Set());
|
|
29
|
+
}
|
|
30
|
+
this.services.get(serviceKey).add(tabId);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 注销一个 tab service
|
|
34
|
+
*/
|
|
35
|
+
unregister(tabId, serviceKey) {
|
|
36
|
+
const tabs = this.services.get(serviceKey);
|
|
37
|
+
if (tabs) {
|
|
38
|
+
tabs.delete(tabId);
|
|
39
|
+
if (tabs.size === 0) {
|
|
40
|
+
this.services.delete(serviceKey);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 注销某个 tab 的所有 services
|
|
46
|
+
*/
|
|
47
|
+
unregisterAll(tabId) {
|
|
48
|
+
for (const [serviceKey, tabs] of this.services.entries()) {
|
|
49
|
+
tabs.delete(tabId);
|
|
50
|
+
if (tabs.size === 0) {
|
|
51
|
+
this.services.delete(serviceKey);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
this.listeners.delete(tabId);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 获取某个 service 的所有 tab IDs
|
|
58
|
+
*/
|
|
59
|
+
getTabs(serviceKey) {
|
|
60
|
+
return Array.from(this.services.get(serviceKey) || []);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 获取所有已注册的 services 信息
|
|
64
|
+
*/
|
|
65
|
+
getAllServices() {
|
|
66
|
+
const result = [];
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
for (const [serviceKey, tabs] of this.services.entries()) {
|
|
69
|
+
for (const tabId of tabs) {
|
|
70
|
+
result.push({
|
|
71
|
+
tabId,
|
|
72
|
+
serviceKey,
|
|
73
|
+
registeredAt: now
|
|
74
|
+
// 简化处理,实际可以记录真实注册时间
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 存储监听器
|
|
82
|
+
*/
|
|
83
|
+
setListener(tabId, serviceKey, listener) {
|
|
84
|
+
if (!this.listeners.has(tabId)) {
|
|
85
|
+
this.listeners.set(tabId, /* @__PURE__ */ new Map());
|
|
86
|
+
}
|
|
87
|
+
this.listeners.get(tabId).set(serviceKey, listener);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 获取监听器
|
|
91
|
+
*/
|
|
92
|
+
getListener(tabId, serviceKey) {
|
|
93
|
+
return this.listeners.get(tabId)?.get(serviceKey);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
var registry = new TabServiceRegistry();
|
|
97
|
+
var isInBackground = false;
|
|
98
|
+
var globalLogger;
|
|
99
|
+
function initTabServiceManager(config) {
|
|
100
|
+
isInBackground = true;
|
|
101
|
+
const logger = config?.logger;
|
|
102
|
+
globalLogger = logger;
|
|
103
|
+
Browser.runtime.onMessage.addListener((message, sender) => {
|
|
104
|
+
switch (message.type) {
|
|
105
|
+
case MESSAGE_TYPES.REGISTER_TAB_SERVICE: {
|
|
106
|
+
if (!sender.tab?.id) {
|
|
107
|
+
return Promise.reject(
|
|
108
|
+
new Error("REGISTER_TAB_SERVICE must be called from a content script")
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const tabId = sender.tab.id;
|
|
112
|
+
const serviceKey = message.serviceKey;
|
|
113
|
+
registry.register(tabId, serviceKey);
|
|
114
|
+
logger?.debug(`[TabProxyService] Service "${serviceKey}" registered in tab ${tabId}`);
|
|
115
|
+
return Promise.resolve({ success: true });
|
|
116
|
+
}
|
|
117
|
+
case MESSAGE_TYPES.UNREGISTER_TAB_SERVICE: {
|
|
118
|
+
if (!sender.tab?.id) {
|
|
119
|
+
return Promise.reject(
|
|
120
|
+
new Error("UNREGISTER_TAB_SERVICE must be called from a content script")
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const tabId = sender.tab.id;
|
|
124
|
+
const serviceKey = message.serviceKey;
|
|
125
|
+
registry.unregister(tabId, serviceKey);
|
|
126
|
+
logger?.debug(`[TabProxyService] Service "${serviceKey}" unregistered from tab ${tabId}`);
|
|
127
|
+
return Promise.resolve({ success: true });
|
|
128
|
+
}
|
|
129
|
+
case MESSAGE_TYPES.CALL_TAB_SERVICE: {
|
|
130
|
+
const { serviceKey, targetTabId, payload } = message;
|
|
131
|
+
const callerId = sender.tab?.id ?? "popup/background";
|
|
132
|
+
logger?.debug(
|
|
133
|
+
`[TabProxyService] CALL_TAB_SERVICE for service "${serviceKey}" from ${callerId}`
|
|
134
|
+
);
|
|
135
|
+
const requestId = `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
136
|
+
const enrichedPayload = {
|
|
137
|
+
...payload,
|
|
138
|
+
originalSender: sender,
|
|
139
|
+
timestamp: Date.now(),
|
|
140
|
+
requestId
|
|
141
|
+
};
|
|
142
|
+
if (targetTabId !== void 0) {
|
|
143
|
+
return forwardToTab(targetTabId, serviceKey, enrichedPayload, logger);
|
|
144
|
+
}
|
|
145
|
+
const tabs = registry.getTabs(serviceKey);
|
|
146
|
+
if (tabs.length === 0) {
|
|
147
|
+
return Promise.reject(new Error(`No tab found with service "${serviceKey}" registered`));
|
|
148
|
+
}
|
|
149
|
+
return forwardToTab(tabs[0], serviceKey, enrichedPayload, logger);
|
|
150
|
+
}
|
|
151
|
+
case MESSAGE_TYPES.QUERY_TAB_SERVICES: {
|
|
152
|
+
return Promise.resolve(registry.getAllServices());
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
Browser.tabs.onRemoved.addListener((tabId) => {
|
|
157
|
+
registry.unregisterAll(tabId);
|
|
158
|
+
logger?.debug(`[TabProxyService] All services unregistered from tab ${tabId}`);
|
|
159
|
+
});
|
|
160
|
+
logger?.debug("[TabProxyService] Manager initialized");
|
|
161
|
+
}
|
|
162
|
+
async function forwardToTab(tabId, serviceKey, payload, logger) {
|
|
163
|
+
try {
|
|
164
|
+
const response = await Browser.tabs.sendMessage(tabId, {
|
|
165
|
+
type: MESSAGE_TYPES.FORWARD_TO_TAB,
|
|
166
|
+
timestamp: Date.now(),
|
|
167
|
+
serviceKey,
|
|
168
|
+
payload
|
|
169
|
+
});
|
|
170
|
+
return response;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
logger?.error(`[TabProxyService] Failed to forward message to tab ${tabId}:`, error);
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function registerTabService(key, realService, config) {
|
|
177
|
+
const logger = config?.logger;
|
|
178
|
+
await Browser.runtime.sendMessage({
|
|
179
|
+
type: MESSAGE_TYPES.REGISTER_TAB_SERVICE,
|
|
180
|
+
timestamp: Date.now(),
|
|
181
|
+
serviceKey: key
|
|
182
|
+
});
|
|
183
|
+
logger?.debug(`[TabProxyService] Registered service "${key}" in content script`);
|
|
184
|
+
const messageListener = (message, sender) => {
|
|
185
|
+
logger?.debug(`[TabProxyService] Received message in service "${key}":`, message, sender);
|
|
186
|
+
if (message.type !== MESSAGE_TYPES.FORWARD_TO_TAB || message.serviceKey !== key) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
return (async () => {
|
|
190
|
+
const payload = message.payload;
|
|
191
|
+
try {
|
|
192
|
+
const method = payload.path == null ? realService : get(realService, payload.path);
|
|
193
|
+
if (typeof method !== "function") {
|
|
194
|
+
throw new Error(`Method not found: ${payload.path?.join(".")}`);
|
|
195
|
+
}
|
|
196
|
+
const context = {
|
|
197
|
+
sender,
|
|
198
|
+
originalSender: payload.originalSender,
|
|
199
|
+
timestamp: payload.timestamp,
|
|
200
|
+
requestId: payload.requestId,
|
|
201
|
+
metadata: {}
|
|
202
|
+
};
|
|
203
|
+
const result = await Promise.resolve(method.bind(realService)(...payload.args, context));
|
|
204
|
+
return result;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
logger?.error(`[TabProxyService] Error executing service "${key}":`, error);
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
})();
|
|
210
|
+
};
|
|
211
|
+
Browser.runtime.onMessage.addListener(messageListener);
|
|
212
|
+
return () => {
|
|
213
|
+
Browser.runtime.onMessage.removeListener(messageListener);
|
|
214
|
+
Browser.runtime.sendMessage({
|
|
215
|
+
type: MESSAGE_TYPES.UNREGISTER_TAB_SERVICE,
|
|
216
|
+
serviceKey: key
|
|
217
|
+
}).catch(() => {
|
|
218
|
+
});
|
|
219
|
+
logger?.debug(`[TabProxyService] Unregistered service "${key}"`);
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function createTabProxyService(key, options) {
|
|
223
|
+
const targetTabId = options?.targetTabId;
|
|
224
|
+
const config = options?.config;
|
|
225
|
+
return createProxy(key, targetTabId, config);
|
|
226
|
+
}
|
|
227
|
+
function createProxy(serviceKey, targetTabId, config, path) {
|
|
228
|
+
const wrapped = (() => {
|
|
229
|
+
});
|
|
230
|
+
const proxy = new Proxy(wrapped, {
|
|
231
|
+
async apply(_target, _thisArg, args) {
|
|
232
|
+
const payload = {
|
|
233
|
+
path,
|
|
234
|
+
args
|
|
235
|
+
};
|
|
236
|
+
if (isInBackground) {
|
|
237
|
+
const logger = config?.logger || globalLogger;
|
|
238
|
+
const requestId = `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
239
|
+
const enrichedPayload = {
|
|
240
|
+
...payload,
|
|
241
|
+
// 在 background 中直接调用时,originalSender 为 undefined
|
|
242
|
+
originalSender: void 0,
|
|
243
|
+
timestamp: Date.now(),
|
|
244
|
+
requestId
|
|
245
|
+
};
|
|
246
|
+
let actualTabId = targetTabId;
|
|
247
|
+
if (!actualTabId) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`targetTabId is required when calling service "${serviceKey}" from background. Please specify the target tab ID explicitly.`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
logger?.debug(
|
|
253
|
+
`[TabProxyService] Direct call from background to service "${serviceKey}" in tab ${actualTabId}`
|
|
254
|
+
);
|
|
255
|
+
return await forwardToTab(actualTabId, serviceKey, enrichedPayload, logger);
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
const response = await Browser.runtime.sendMessage({
|
|
259
|
+
type: MESSAGE_TYPES.CALL_TAB_SERVICE,
|
|
260
|
+
timestamp: Date.now(),
|
|
261
|
+
serviceKey,
|
|
262
|
+
targetTabId,
|
|
263
|
+
payload
|
|
264
|
+
});
|
|
265
|
+
return response;
|
|
266
|
+
} catch (error) {
|
|
267
|
+
config?.logger?.error(`[TabProxyService] Error calling service "${serviceKey}":`, error);
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
get(target, propertyName, receiver) {
|
|
272
|
+
if (typeof propertyName === "symbol") {
|
|
273
|
+
return Reflect.get(target, propertyName, receiver);
|
|
274
|
+
}
|
|
275
|
+
return createProxy(
|
|
276
|
+
serviceKey,
|
|
277
|
+
targetTabId,
|
|
278
|
+
config,
|
|
279
|
+
path == null ? [propertyName] : path.concat([propertyName])
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
return proxy;
|
|
284
|
+
}
|
|
285
|
+
async function queryTabServices() {
|
|
286
|
+
const response = await Browser.runtime.sendMessage({
|
|
287
|
+
type: MESSAGE_TYPES.QUERY_TAB_SERVICES
|
|
288
|
+
});
|
|
289
|
+
return response;
|
|
290
|
+
}
|
|
291
|
+
function get(obj, path) {
|
|
292
|
+
if (path.length === 0) {
|
|
293
|
+
return obj;
|
|
294
|
+
}
|
|
295
|
+
return path.reduce((acc, key) => acc?.[key], obj);
|
|
296
|
+
}
|
|
297
|
+
function defineTabProxyService(name, init, config) {
|
|
298
|
+
const key = name;
|
|
299
|
+
return [
|
|
300
|
+
// registerService
|
|
301
|
+
async (...args) => {
|
|
302
|
+
const service = init(...args);
|
|
303
|
+
return await registerTabService(key, service, config);
|
|
304
|
+
},
|
|
305
|
+
// getService
|
|
306
|
+
(options) => createTabProxyService(key, options)
|
|
307
|
+
];
|
|
308
|
+
}
|
|
309
|
+
function flattenPromise(promise) {
|
|
310
|
+
return new Proxy({}, {
|
|
311
|
+
get(_, prop) {
|
|
312
|
+
if (typeof prop === "symbol") {
|
|
313
|
+
return void 0;
|
|
314
|
+
}
|
|
315
|
+
return async (...args) => {
|
|
316
|
+
const resolved = await promise;
|
|
317
|
+
const method = resolved[prop];
|
|
318
|
+
if (typeof method === "function") {
|
|
319
|
+
return method.apply(resolved, args);
|
|
320
|
+
}
|
|
321
|
+
return method;
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export { createTabProxyService, defineTabProxyService, flattenPromise, initTabServiceManager, queryTabServices, registerTabService };
|
|
328
|
+
//# sourceMappingURL=index.js.map
|
|
329
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import Browser from 'webextension-polyfill'\n\nimport type {\n ProxyMessage,\n ProxyResponse,\n Service,\n ServiceCallContext,\n TabProxyService,\n TabProxyServiceConfig,\n TabProxyServiceKey,\n TabServiceInfo\n} from './types'\n\n// 重新导出类型,供外部使用\nexport type {\n Service,\n ServiceCallContext,\n TabProxyService,\n TabProxyServiceKey,\n TabProxyServiceConfig,\n TabServiceInfo,\n ProxyMessage,\n ProxyResponse,\n Promisify,\n DeepAsync\n} from './types'\n\n/**\n * 消息类型枚举\n */\nconst MESSAGE_TYPES = {\n /** Content script 向 background 注册 service */\n REGISTER_TAB_SERVICE: 'tab-proxy:register',\n /** Content script 向 background 注销 service */\n UNREGISTER_TAB_SERVICE: 'tab-proxy:unregister',\n /** 调用 tab 中的 service */\n CALL_TAB_SERVICE: 'tab-proxy:call',\n /** Background 转发消息到目标 tab */\n FORWARD_TO_TAB: 'tab-proxy:forward',\n /** 查询已注册的 tab services */\n QUERY_TAB_SERVICES: 'tab-proxy:query'\n} as const\n\n/**\n * 全局状态管理\n */\nclass TabServiceRegistry {\n /** 存储所有已注册的 tab services: serviceKey -> Set<tabId> */\n private services = new Map<string, Set<number>>()\n\n /** 存储 service 监听器: tabId -> Map<serviceKey, listener> */\n private listeners = new Map<number, Map<string, (message: any) => any>>()\n\n /**\n * 注册一个 tab service\n */\n register(tabId: number, serviceKey: string): void {\n if (!this.services.has(serviceKey)) {\n this.services.set(serviceKey, new Set())\n }\n this.services.get(serviceKey)!.add(tabId)\n }\n\n /**\n * 注销一个 tab service\n */\n unregister(tabId: number, serviceKey: string): void {\n const tabs = this.services.get(serviceKey)\n if (tabs) {\n tabs.delete(tabId)\n if (tabs.size === 0) {\n this.services.delete(serviceKey)\n }\n }\n }\n\n /**\n * 注销某个 tab 的所有 services\n */\n unregisterAll(tabId: number): void {\n for (const [serviceKey, tabs] of this.services.entries()) {\n tabs.delete(tabId)\n if (tabs.size === 0) {\n this.services.delete(serviceKey)\n }\n }\n this.listeners.delete(tabId)\n }\n\n /**\n * 获取某个 service 的所有 tab IDs\n */\n getTabs(serviceKey: string): number[] {\n return Array.from(this.services.get(serviceKey) || [])\n }\n\n /**\n * 获取所有已注册的 services 信息\n */\n getAllServices(): TabServiceInfo[] {\n const result: TabServiceInfo[] = []\n const now = Date.now()\n\n for (const [serviceKey, tabs] of this.services.entries()) {\n for (const tabId of tabs) {\n result.push({\n tabId,\n serviceKey,\n registeredAt: now // 简化处理,实际可以记录真实注册时间\n })\n }\n }\n\n return result\n }\n\n /**\n * 存储监听器\n */\n setListener(tabId: number, serviceKey: string, listener: (message: any) => any): void {\n if (!this.listeners.has(tabId)) {\n this.listeners.set(tabId, new Map())\n }\n this.listeners.get(tabId)!.set(serviceKey, listener)\n }\n\n /**\n * 获取监听器\n */\n getListener(tabId: number, serviceKey: string): ((message: any) => any) | undefined {\n return this.listeners.get(tabId)?.get(serviceKey)\n }\n}\n\n// 全局 registry 实例(仅在 background 中使用)\nconst registry = new TabServiceRegistry()\n\n// 标记当前是否在 background 环境中\nlet isInBackground = false\n\n// 全局 logger 实例\nlet globalLogger: Pick<Console, 'log' | 'warn' | 'error' | 'debug'> | undefined\n\n/**\n * 在 background 中初始化 tab service 管理器\n * 必须在 background script 启动时调用\n */\nexport function initTabServiceManager(config?: TabProxyServiceConfig): void {\n isInBackground = true\n\n const logger = config?.logger\n\n globalLogger = logger\n\n // 监听来自 content scripts 的注册/注销消息\n Browser.runtime.onMessage.addListener((message, sender) => {\n switch (message.type) {\n case MESSAGE_TYPES.REGISTER_TAB_SERVICE: {\n // 注册操作必须来自 content script(需要 tab.id)\n if (!sender.tab?.id) {\n return Promise.reject(\n new Error('REGISTER_TAB_SERVICE must be called from a content script')\n )\n }\n const tabId = sender.tab.id\n const serviceKey = message.serviceKey\n registry.register(tabId, serviceKey)\n logger?.debug(`[TabProxyService] Service \"${serviceKey}\" registered in tab ${tabId}`)\n return Promise.resolve({ success: true })\n }\n\n case MESSAGE_TYPES.UNREGISTER_TAB_SERVICE: {\n // 注销操作必须来自 content script(需要 tab.id)\n if (!sender.tab?.id) {\n return Promise.reject(\n new Error('UNREGISTER_TAB_SERVICE must be called from a content script')\n )\n }\n const tabId = sender.tab.id\n const serviceKey = message.serviceKey\n registry.unregister(tabId, serviceKey)\n logger?.debug(`[TabProxyService] Service \"${serviceKey}\" unregistered from tab ${tabId}`)\n return Promise.resolve({ success: true })\n }\n\n case MESSAGE_TYPES.CALL_TAB_SERVICE: {\n // 调用操作可以来自任何地方(popup、background、content script)\n const { serviceKey, targetTabId, payload } = message\n const callerId = sender.tab?.id ?? 'popup/background'\n logger?.debug(\n `[TabProxyService] CALL_TAB_SERVICE for service \"${serviceKey}\" from ${callerId}`\n )\n\n // 生成请求 ID 用于追踪\n const requestId = `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`\n\n // 添加原始 sender 信息到 payload\n const enrichedPayload: ProxyMessage = {\n ...payload,\n originalSender: sender,\n timestamp: Date.now(),\n requestId\n }\n\n // 如果指定了目标 tab,直接转发\n if (targetTabId !== undefined) {\n return forwardToTab(targetTabId, serviceKey, enrichedPayload, logger)\n }\n\n // 否则选择任意一个可用的 tab\n const tabs = registry.getTabs(serviceKey)\n if (tabs.length === 0) {\n return Promise.reject(new Error(`No tab found with service \"${serviceKey}\" registered`))\n }\n\n // 使用第一个可用的 tab\n return forwardToTab(tabs[0], serviceKey, enrichedPayload, logger)\n }\n\n case MESSAGE_TYPES.QUERY_TAB_SERVICES: {\n // 查询操作可以来自任何地方\n return Promise.resolve(registry.getAllServices())\n }\n }\n })\n\n // 监听 tab 关闭事件,清理注册信息\n Browser.tabs.onRemoved.addListener((tabId) => {\n registry.unregisterAll(tabId)\n logger?.debug(`[TabProxyService] All services unregistered from tab ${tabId}`)\n })\n\n logger?.debug('[TabProxyService] Manager initialized')\n}\n\n/**\n * 转发消息到指定 tab\n */\nasync function forwardToTab(\n tabId: number,\n serviceKey: string,\n payload: ProxyMessage,\n logger?: Pick<Console, 'log' | 'warn' | 'error' | 'debug'>\n): Promise<ProxyResponse> {\n try {\n const response = await Browser.tabs.sendMessage(tabId, {\n type: MESSAGE_TYPES.FORWARD_TO_TAB,\n timestamp: Date.now(),\n serviceKey,\n payload\n })\n return response\n } catch (error) {\n logger?.error(`[TabProxyService] Failed to forward message to tab ${tabId}:`, error)\n throw error\n }\n}\n\n/**\n * 在 content script 中注册一个 service\n *\n * @param key Service key\n * @param realService 实际的 service 实例\n * @param config 配置选项\n * @returns 返回清理函数,调用后注销 service\n *\n * @example\n * ```ts\n * // content-script.ts\n * import { registerTabService } from '@northsea4/tab-proxy-service';\n *\n * const myService = {\n * async getData() {\n * return [1, 2, 3];\n * }\n * };\n *\n * const cleanup = await registerTabService('my-service', myService);\n *\n * // 页面卸载时清理\n * window.addEventListener('beforeunload', cleanup);\n * ```\n */\nexport async function registerTabService<\n T extends Service,\n K extends string = TabProxyServiceKey<T> | string\n>(key: K, realService: T, config?: TabProxyServiceConfig): Promise<() => void> {\n const logger = config?.logger\n\n // 向 background 注册\n await Browser.runtime.sendMessage({\n type: MESSAGE_TYPES.REGISTER_TAB_SERVICE,\n timestamp: Date.now(),\n serviceKey: key\n })\n\n logger?.debug(`[TabProxyService] Registered service \"${key}\" in content script`)\n\n // 监听来自 background 的转发消息\n const messageListener = (message: any, sender: Browser.Runtime.MessageSender) => {\n logger?.debug(`[TabProxyService] Received message in service \"${key}\":`, message, sender)\n\n // 先检查消息类型,如果不匹配,立即返回 undefined(不是 Promise)\n if (message.type !== MESSAGE_TYPES.FORWARD_TO_TAB || message.serviceKey !== key) {\n return // 返回 undefined,不是 Promise<undefined>\n }\n\n return (async () => {\n const payload: ProxyMessage = message.payload\n\n try {\n // 获取要调用的方法\n const method = payload.path == null ? realService : get(realService, payload.path)\n\n if (typeof method !== 'function') {\n throw new Error(`Method not found: ${payload.path?.join('.')}`)\n }\n\n // 构造 ServiceCallContext 对象\n const context: ServiceCallContext = {\n sender,\n originalSender: payload.originalSender,\n timestamp: payload.timestamp,\n requestId: payload.requestId,\n metadata: {}\n }\n\n // 调用方法并返回结果,将 context 作为最后一个参数传递\n const result = await Promise.resolve(method.bind(realService)(...payload.args, context))\n return result\n } catch (error) {\n logger?.error(`[TabProxyService] Error executing service \"${key}\":`, error)\n throw error\n }\n })()\n }\n\n Browser.runtime.onMessage.addListener(messageListener)\n\n // 返回清理函数\n return () => {\n Browser.runtime.onMessage.removeListener(messageListener)\n Browser.runtime\n .sendMessage({\n type: MESSAGE_TYPES.UNREGISTER_TAB_SERVICE,\n serviceKey: key\n })\n .catch(() => {\n // 忽略错误,可能 background 已经关闭\n })\n logger?.debug(`[TabProxyService] Unregistered service \"${key}\"`)\n }\n}\n\n/**\n * 创建一个 tab service 的代理\n * 可以在 background、popup 或其他 content script 中调用\n *\n * @param key Service key\n * @param options 可选参数:targetTabId 指定目标 tab,config 配置\n * @returns 返回 service 代理\n *\n * @example\n * ```ts\n * // background.ts - 调用任意 tab 中的 service\n * import { createTabProxyService } from '@northsea4/tab-proxy-service';\n *\n * const myService = createTabProxyService<MyService>('my-service');\n * const data = await myService.getData();\n *\n * // 或者指定特定的 tab\n * const myServiceInTab = createTabProxyService<MyService>('my-service', { targetTabId: 123 });\n * ```\n *\n * @example\n * ```ts\n * // popup.ts - 从 popup 调用 tab 中的 service\n * const myService = createTabProxyService<MyService>('my-service', { targetTabId: 123 });\n * const result = await myService.someMethod();\n * ```\n *\n * @example\n * ```ts\n * // content-script-a.ts - 调用另一个 tab 中的 service\n * const serviceInOtherTab = createTabProxyService<OtherService>('other-service', { targetTabId: 456 });\n * await serviceInOtherTab.doSomething();\n * ```\n */\nexport function createTabProxyService<T extends Service>(\n key: TabProxyServiceKey<T> | string,\n options?: {\n targetTabId?: number\n config?: TabProxyServiceConfig\n }\n): TabProxyService<T> {\n const targetTabId = options?.targetTabId\n const config = options?.config\n\n return createProxy(key, targetTabId, config)\n}\n\n/**\n * 创建深层代理对象\n * 所有属性访问都返回新的代理,函数调用时发送消息到 background\n */\nfunction createProxy<T>(\n serviceKey: string,\n targetTabId: number | undefined,\n config: TabProxyServiceConfig | undefined,\n path?: string[]\n): TabProxyService<T> {\n const wrapped = (() => {}) as TabProxyService<T>\n\n const proxy = new Proxy(wrapped, {\n async apply(_target, _thisArg, args) {\n const payload: ProxyMessage = {\n path,\n args\n }\n\n // 如果在 background 环境中,直接调用内部转发函数,不需要通过消息传递\n if (isInBackground) {\n const logger = config?.logger || globalLogger\n\n // 生成请求 ID 用于追踪\n const requestId = `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`\n\n const enrichedPayload: ProxyMessage = {\n ...payload,\n // 在 background 中直接调用时,originalSender 为 undefined\n originalSender: undefined,\n timestamp: Date.now(),\n requestId\n }\n\n // 确定目标 tab\n let actualTabId = targetTabId\n if (!actualTabId) {\n // 在 background 中必须明确指定 targetTabId,不能随机选择\n throw new Error(\n `targetTabId is required when calling service \"${serviceKey}\" from background. ` +\n `Please specify the target tab ID explicitly.`\n )\n }\n\n logger?.debug(\n `[TabProxyService] Direct call from background to service \"${serviceKey}\" in tab ${actualTabId}`\n )\n return await forwardToTab(actualTabId, serviceKey, enrichedPayload, logger)\n }\n\n // 发送消息到 background,由 background 转发到对应的 tab\n try {\n const response = await Browser.runtime.sendMessage({\n type: MESSAGE_TYPES.CALL_TAB_SERVICE,\n timestamp: Date.now(),\n serviceKey,\n targetTabId,\n payload\n })\n return response\n } catch (error) {\n config?.logger?.error(`[TabProxyService] Error calling service \"${serviceKey}\":`, error)\n throw error\n }\n },\n\n get(target, propertyName, receiver) {\n // 返回 symbol 属性的值\n if (typeof propertyName === 'symbol') {\n return Reflect.get(target, propertyName, receiver)\n }\n\n // 为常规属性返回新的代理\n return createProxy(\n serviceKey,\n targetTabId,\n config,\n path == null ? [propertyName] : path.concat([propertyName])\n )\n }\n })\n\n return proxy\n}\n\n/**\n * 查询所有已注册的 tab services\n *\n * @returns 返回所有已注册的 service 信息\n *\n * @example\n * ```ts\n * const services = await queryTabServices();\n * console.log('Registered services:', services);\n * // [{ tabId: 123, serviceKey: 'my-service', registeredAt: 1234567890 }]\n * ```\n */\nexport async function queryTabServices(): Promise<TabServiceInfo[]> {\n const response = await Browser.runtime.sendMessage({\n type: MESSAGE_TYPES.QUERY_TAB_SERVICES\n })\n return response\n}\n\n/**\n * 从对象中按路径获取值\n */\nfunction get(obj: any, path: string[]): any {\n if (path.length === 0) {\n return obj\n }\n return path.reduce((acc, key) => acc?.[key], obj)\n}\n\n/**\n * 定义一个 tab proxy service 的辅助函数\n * 类似于 @northsea4/proxy-service 的 defineProxyService\n *\n * @param name Service 的唯一名称\n * @param init 初始化函数,返回实际的 service 实例\n * @param config 配置选项\n * @returns 返回 [registerService, getService] 元组\n *\n * @example\n * ```ts\n * // hello-service.ts\n * export const [registerHelloService, getHelloService] = defineTabProxyService(\n * 'hello-service',\n * () => ({\n * async sayHello(name: string) {\n * return `Hello, ${name}!`;\n * }\n * })\n * );\n *\n * // content-script.ts - 注册服务\n * const cleanup = await registerHelloService();\n *\n * // background.ts - 使用服务(自动有类型提示)\n * const helloService = getHelloService({ targetTabId: 123 });\n * const greeting = await helloService.sayHello('World'); // 类型安全!\n * ```\n */\nexport function defineTabProxyService<TService extends Service, TArgs extends any[]>(\n name: string,\n init: (...args: TArgs) => TService,\n config?: TabProxyServiceConfig\n): [\n registerService: (...args: TArgs) => Promise<() => void>,\n getService: (options?: {\n targetTabId?: number\n config?: TabProxyServiceConfig\n }) => TabProxyService<TService>\n] {\n const key = name as TabProxyServiceKey<TService>\n\n return [\n // registerService\n async (...args: TArgs): Promise<() => void> => {\n const service = init(...args)\n return await registerTabService(key, service, config)\n },\n // getService\n (options?: { targetTabId?: number; config?: TabProxyServiceConfig }) =>\n createTabProxyService<TService>(key, options)\n ]\n}\n\n/**\n * 辅助函数:扁平化 Promise\n * 用于简化处理 Promise<Dependency>\n *\n * @example\n * ```ts\n * function createService(dependencyPromise: Promise<SomeDependency>) {\n * const dependency = flattenPromise(dependencyPromise);\n *\n * return {\n * async doSomething() {\n * await dependency.someAsyncWork();\n * // 而不是 await (await dependencyPromise).someAsyncWork();\n * }\n * }\n * }\n * ```\n */\nexport function flattenPromise<T>(promise: Promise<T>): TabProxyService<T> {\n return new Proxy({} as TabProxyService<T>, {\n get(_, prop) {\n if (typeof prop === 'symbol') {\n return undefined\n }\n return async (...args: any[]) => {\n const resolved = await promise\n const method = (resolved as any)[prop]\n if (typeof method === 'function') {\n return method.apply(resolved, args)\n }\n return method\n }\n }\n })\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@northsea4/tab-proxy-service",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A type-safe wrapper for calling services registered in content scripts from background or other tabs, with automatic type inference",
|
|
5
|
+
"author": "nornorlunn",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./lib/index.js",
|
|
9
|
+
"module": "./lib/index.js",
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./lib/index.d.ts",
|
|
14
|
+
"import": "./lib/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./types": {
|
|
17
|
+
"types": "./lib/types.d.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"lib"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsup",
|
|
25
|
+
"dev": "tsup --watch",
|
|
26
|
+
"test": "vitest"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@webext-core/messaging": "^2.3.0",
|
|
30
|
+
"webextension-polyfill": "^0.10.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/webextension-polyfill": "^0.10.7",
|
|
34
|
+
"tsup": "^8.0.1",
|
|
35
|
+
"typescript": "^5.1.6",
|
|
36
|
+
"vitest": "^1.0.0"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"webextension",
|
|
40
|
+
"chrome-extension",
|
|
41
|
+
"firefox-addon",
|
|
42
|
+
"proxy",
|
|
43
|
+
"service",
|
|
44
|
+
"content-script",
|
|
45
|
+
"tab",
|
|
46
|
+
"messaging"
|
|
47
|
+
]
|
|
48
|
+
}
|