crx-rpc 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 +417 -0
- package/README.zh-CN.md +533 -0
- package/dist/background.d.ts +21 -0
- package/dist/background.js +146 -0
- package/dist/client.d.ts +27 -0
- package/dist/client.js +88 -0
- package/dist/const.d.ts +5 -0
- package/dist/const.js +5 -0
- package/dist/content.d.ts +14 -0
- package/dist/content.js +55 -0
- package/dist/disposable.d.ts +7 -0
- package/dist/disposable.js +16 -0
- package/dist/id.d.ts +12 -0
- package/dist/id.js +8 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/types.d.ts +43 -0
- package/dist/types.js +1 -0
- package/dist/web.d.ts +10 -0
- package/dist/web.js +25 -0
- package/package.json +27 -0
- package/src/background.ts +162 -0
- package/src/client.ts +110 -0
- package/src/const.ts +6 -0
- package/src/content.ts +71 -0
- package/src/disposable.ts +18 -0
- package/src/id.ts +17 -0
- package/src/index.ts +6 -0
- package/src/types.ts +49 -0
- package/src/web.ts +34 -0
- package/tsconfig.json +16 -0
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
# Chrome 扩展 RPC 框架 (@weird94/crx-rpc)
|
|
2
|
+
|
|
3
|
+
一个轻量级、类型安全的Chrome扩展RPC框架,支持网页、内容脚本和背景脚本之间的通信。基于TypeScript构建,提供最大的类型安全性和开发体验。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- 🔒 **类型安全**: 完整的TypeScript类型支持,自动代理类型生成
|
|
8
|
+
- 🚀 **易于使用**: 基于接口自动生成客户端代理
|
|
9
|
+
- 🔄 **双向通信**: 支持网页 ↔ 内容脚本 ↔ 背景脚本通信
|
|
10
|
+
- 📦 **零配置**: 无需手动方法绑定
|
|
11
|
+
- 🎯 **Observable支持**: 内置响应式数据流支持,使用RemoteSubject
|
|
12
|
+
- 🛡️ **错误处理**: 跨边界保留堆栈跟踪和错误类型
|
|
13
|
+
- 🧹 **资源管理**: 内置disposable模式,支持清理资源
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @weird94/crx-rpc
|
|
19
|
+
# 或
|
|
20
|
+
pnpm add @weird94/crx-rpc
|
|
21
|
+
# 或
|
|
22
|
+
yarn add @weird94/crx-rpc
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 快速开始
|
|
26
|
+
|
|
27
|
+
### 1. 定义服务接口
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// services/math.ts
|
|
31
|
+
import { createIdentifier } from '@weird94/crx-rpc';
|
|
32
|
+
|
|
33
|
+
interface IMathService {
|
|
34
|
+
add(a: number, b: number): Promise<number>;
|
|
35
|
+
subtract(a: number, b: number): Promise<number>;
|
|
36
|
+
multiply(a: number, b: number): Promise<number>;
|
|
37
|
+
divide(a: number, b: number): Promise<number>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 创建服务标识符
|
|
41
|
+
export const IMathService = createIdentifier<IMathService>('MathService');
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 2. 实现服务(背景脚本)
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// background.ts
|
|
48
|
+
import { BackgroundRPC } from '@weird94/crx-rpc';
|
|
49
|
+
import { IMathService } from './services/math';
|
|
50
|
+
|
|
51
|
+
class MathService implements IMathService {
|
|
52
|
+
async add(a: number, b: number): Promise<number> {
|
|
53
|
+
return a + b;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async subtract(a: number, b: number): Promise<number> {
|
|
57
|
+
return a - b;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async multiply(a: number, b: number): Promise<number> {
|
|
61
|
+
return a * b;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async divide(a: number, b: number): Promise<number> {
|
|
65
|
+
if (b === 0) throw new Error('除零错误');
|
|
66
|
+
return a / b;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 注册服务
|
|
71
|
+
const rpc = new BackgroundRPC();
|
|
72
|
+
rpc.register(IMathService, new MathService());
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 3. 初始化内容脚本
|
|
76
|
+
|
|
77
|
+
内容脚本可以以两种模式工作:
|
|
78
|
+
|
|
79
|
+
#### 选项A:作为桥接器(用于网页通信)
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// content.ts
|
|
83
|
+
import { ContentRPC } from '@weird94/crx-rpc';
|
|
84
|
+
|
|
85
|
+
// 为网页 ↔ 背景脚本通信初始化RPC桥接器
|
|
86
|
+
const contentRpc = new ContentRPC();
|
|
87
|
+
|
|
88
|
+
// 需要清理时记得dispose
|
|
89
|
+
// contentRpc.dispose();
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
#### 选项B:作为直接客户端
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
// content.ts
|
|
96
|
+
import { ContentRPCClient } from '@weird94/crx-rpc';
|
|
97
|
+
import { IMathService } from './services/math';
|
|
98
|
+
|
|
99
|
+
// 将内容脚本用作直接RPC客户端
|
|
100
|
+
const client = new ContentRPCClient();
|
|
101
|
+
const mathService = client.createWebRPCService(IMathService);
|
|
102
|
+
|
|
103
|
+
// 直接调用背景服务
|
|
104
|
+
const result = await mathService.add(5, 3);
|
|
105
|
+
console.log('内容脚本结果:', result);
|
|
106
|
+
|
|
107
|
+
// 需要清理时记得dispose
|
|
108
|
+
// client.dispose();
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### 选项C:既是桥接器又是客户端
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// content.ts
|
|
115
|
+
import { ContentRPC, ContentRPCClient } from '@weird94/crx-rpc';
|
|
116
|
+
import { IMathService } from './services/math';
|
|
117
|
+
|
|
118
|
+
// 为网页初始化桥接器
|
|
119
|
+
const bridge = new ContentRPC();
|
|
120
|
+
|
|
121
|
+
// 同时用作直接客户端
|
|
122
|
+
const client = new ContentRPCClient();
|
|
123
|
+
const mathService = client.createWebRPCService(IMathService);
|
|
124
|
+
|
|
125
|
+
// 内容脚本可以进行自己的RPC调用
|
|
126
|
+
const result = await mathService.multiply(2, 3);
|
|
127
|
+
console.log('内容脚本计算:', result);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 4. 使用客户端(网页)
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
// web-page.ts
|
|
134
|
+
import { WebRPCClient } from '@weird94/crx-rpc';
|
|
135
|
+
import { IMathService } from './services/math';
|
|
136
|
+
|
|
137
|
+
async function calculate() {
|
|
138
|
+
// 创建RPC客户端
|
|
139
|
+
const client = new WebRPCClient();
|
|
140
|
+
|
|
141
|
+
// 创建类型安全的服务代理
|
|
142
|
+
const mathService = client.createWebRPCService(IMathService);
|
|
143
|
+
|
|
144
|
+
// 类型安全的方法调用
|
|
145
|
+
const sum = await mathService.add(1, 2); // TypeScript知道这返回Promise<number>
|
|
146
|
+
const difference = await mathService.subtract(10, 5);
|
|
147
|
+
const product = await mathService.multiply(3, 4);
|
|
148
|
+
const quotient = await mathService.divide(15, 3);
|
|
149
|
+
|
|
150
|
+
console.log('结果:', { sum, difference, product, quotient });
|
|
151
|
+
|
|
152
|
+
// 需要清理时记得dispose
|
|
153
|
+
// client.dispose();
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## 架构
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
网页 内容脚本 背景脚本
|
|
161
|
+
┌─────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
162
|
+
│ WebRPCClient│──▶│ ContentRPC │──▶│ BackgroundRPC │
|
|
163
|
+
│ │ │ (桥接器) │ │ │
|
|
164
|
+
│ 代理 │ │ │ │ 服务 │
|
|
165
|
+
│ 服务 │ │ MessageAdapter │ │ 注册表 │
|
|
166
|
+
│ .add(1, 2) │ │ │ │ │
|
|
167
|
+
└─────────────┘ └─────────────────┘ └─────────────────┘
|
|
168
|
+
│ │ ▲
|
|
169
|
+
│ CustomEvent │ chrome.runtime │
|
|
170
|
+
│ │ Messages │
|
|
171
|
+
└──────────────────┴──────────────────────┘
|
|
172
|
+
│
|
|
173
|
+
┌─────────────────┐
|
|
174
|
+
│ContentRPCClient │
|
|
175
|
+
│ (直接) │
|
|
176
|
+
│ │
|
|
177
|
+
│ 代理服务 │
|
|
178
|
+
│ .subtract(5,2) │
|
|
179
|
+
└─────────────────┘
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 通信流程
|
|
183
|
+
|
|
184
|
+
1. **网页 → 内容脚本**: 使用 `window.dispatchEvent` 和 `CustomEvent`
|
|
185
|
+
2. **内容脚本 → 背景脚本**: 使用 `chrome.runtime.sendMessage`
|
|
186
|
+
3. **背景脚本 → 内容脚本**: 使用 `chrome.tabs.sendMessage`
|
|
187
|
+
4. **内容脚本 → 网页**: 使用 `window.dispatchEvent` 和 `CustomEvent`
|
|
188
|
+
5. **内容脚本直接**: 直接使用 `chrome.runtime.sendMessage` (ContentRPCClient)
|
|
189
|
+
|
|
190
|
+
### 核心组件
|
|
191
|
+
|
|
192
|
+
- **WebRPCClient**: 用于网页的客户端,使用window事件
|
|
193
|
+
- **ContentRPC**: 在网页和背景脚本间转发消息的桥接器
|
|
194
|
+
- **ContentRPCClient**: 内容脚本的直接RPC客户端(绕过桥接器)
|
|
195
|
+
- **BackgroundRPC**: 背景脚本中的服务注册表和处理器
|
|
196
|
+
- **RPCClient**: 具有服务代理生成功能的基础客户端
|
|
197
|
+
|
|
198
|
+
## 错误处理
|
|
199
|
+
|
|
200
|
+
框架保留错误详细信息,包括堆栈跟踪和错误类型:
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
const client = new WebRPCClient();
|
|
204
|
+
const mathService = client.createWebRPCService(IMathService);
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const result = await mathService.divide(10, 0);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
console.error('RPC错误:', error.message);
|
|
210
|
+
console.error('堆栈跟踪:', error.stack);
|
|
211
|
+
console.error('错误名称:', error.name);
|
|
212
|
+
// 错误保留了来自背景脚本的原始堆栈跟踪和错误类型
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### 错误结构
|
|
217
|
+
|
|
218
|
+
错误会传输完整的详细信息:
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
interface RpcErrorDetails {
|
|
222
|
+
message: string;
|
|
223
|
+
stack?: string;
|
|
224
|
+
name?: string;
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Observable支持
|
|
229
|
+
|
|
230
|
+
框架包含使用 `RemoteSubject` 和 `Observable` 模式的内置响应式数据流支持。
|
|
231
|
+
|
|
232
|
+
### 远程Subject(背景脚本)
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
// background.ts
|
|
236
|
+
import { BackgroundRPC, RemoteSubject, createIdentifier } from '@weird94/crx-rpc';
|
|
237
|
+
|
|
238
|
+
interface ICounterObservable {
|
|
239
|
+
value: number;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const ICounterObservable = createIdentifier<ICounterObservable>('Counter');
|
|
243
|
+
|
|
244
|
+
const rpc = new BackgroundRPC();
|
|
245
|
+
|
|
246
|
+
// 创建可以向多个订阅者广播的远程subject
|
|
247
|
+
const counterSubject = new RemoteSubject(ICounterObservable, 'main', { value: 0 });
|
|
248
|
+
|
|
249
|
+
// 更新值并广播给所有订阅者
|
|
250
|
+
setInterval(() => {
|
|
251
|
+
const newValue = { value: Math.floor(Math.random() * 100) };
|
|
252
|
+
counterSubject.next(newValue);
|
|
253
|
+
}, 1000);
|
|
254
|
+
|
|
255
|
+
// 清理
|
|
256
|
+
// counterSubject.dispose();
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### 从网页订阅
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
// web-page.ts
|
|
263
|
+
import { WebObservable, createIdentifier } from '@weird94/crx-rpc';
|
|
264
|
+
|
|
265
|
+
interface ICounterObservable {
|
|
266
|
+
value: number;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const ICounterObservable = createIdentifier<ICounterObservable>('Counter');
|
|
270
|
+
|
|
271
|
+
// 订阅远程observable
|
|
272
|
+
const observable = new WebObservable(
|
|
273
|
+
ICounterObservable,
|
|
274
|
+
'main',
|
|
275
|
+
(value) => {
|
|
276
|
+
console.log('计数器更新:', value.value);
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
// 完成时清理
|
|
281
|
+
// observable.dispose();
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### 从内容脚本订阅
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
// content.ts
|
|
288
|
+
import { ContentObservable, createIdentifier } from '@weird94/crx-rpc';
|
|
289
|
+
|
|
290
|
+
interface ICounterObservable {
|
|
291
|
+
value: number;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const ICounterObservable = createIdentifier<ICounterObservable>('Counter');
|
|
295
|
+
|
|
296
|
+
// 内容脚本可以直接订阅observables
|
|
297
|
+
const observable = new ContentObservable(
|
|
298
|
+
ICounterObservable,
|
|
299
|
+
'main',
|
|
300
|
+
(value) => {
|
|
301
|
+
console.log('来自内容脚本的计数器:', value.value);
|
|
302
|
+
// 内容脚本可以响应实时更新
|
|
303
|
+
updateUI(value.value);
|
|
304
|
+
}
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
// 完成时清理
|
|
308
|
+
// observable.dispose();
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Observable通信模式
|
|
312
|
+
|
|
313
|
+
Observable系统支持多种通信模式:
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
// 模式1: 背景脚本 → 网页 (通过内容脚本桥接器)
|
|
317
|
+
// 背景脚本: RemoteSubject.next()
|
|
318
|
+
// 网页: WebObservable.subscribe()
|
|
319
|
+
|
|
320
|
+
// 模式2: 背景脚本 → 内容脚本 (直接)
|
|
321
|
+
// 背景脚本: RemoteSubject.next()
|
|
322
|
+
// 内容脚本: ContentObservable.subscribe()
|
|
323
|
+
|
|
324
|
+
// 模式3: 背景脚本 → 网页和内容脚本同时
|
|
325
|
+
// 背景脚本: RemoteSubject.next() (广播给所有订阅者)
|
|
326
|
+
// 网页: WebObservable.subscribe()
|
|
327
|
+
// 内容脚本: ContentObservable.subscribe()
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## 高级用法
|
|
331
|
+
|
|
332
|
+
### 使用Disposables进行资源管理
|
|
333
|
+
|
|
334
|
+
所有RPC组件都继承了 `Disposable` 类来进行适当的清理:
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
import { WebRPCClient, ContentRPC, BackgroundRPC } from '@weird94/crx-rpc';
|
|
338
|
+
|
|
339
|
+
const client = new WebRPCClient();
|
|
340
|
+
const contentRpc = new ContentRPC();
|
|
341
|
+
const backgroundRpc = new BackgroundRPC();
|
|
342
|
+
|
|
343
|
+
// 适当的清理
|
|
344
|
+
function cleanup() {
|
|
345
|
+
client.dispose();
|
|
346
|
+
contentRpc.dispose();
|
|
347
|
+
backgroundRpc.dispose();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// 检查是否已经disposed
|
|
351
|
+
if (!client.isDisposed()) {
|
|
352
|
+
const service = client.createWebRPCService(IMathService);
|
|
353
|
+
// 使用服务...
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### 内容脚本作为直接客户端
|
|
358
|
+
|
|
359
|
+
内容脚本具有完整的RPC功能,可以作为直接客户端而无需通过网页桥接:
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
// content.ts
|
|
363
|
+
import { ContentRPCClient, ContentObservable } from '@weird94/crx-rpc';
|
|
364
|
+
import { IMathService, IUserService } from './services';
|
|
365
|
+
|
|
366
|
+
const client = new ContentRPCClient();
|
|
367
|
+
|
|
368
|
+
// 创建服务代理
|
|
369
|
+
const mathService = client.createWebRPCService(IMathService);
|
|
370
|
+
const userService = client.createWebRPCService(IUserService);
|
|
371
|
+
|
|
372
|
+
// 直接调用背景服务
|
|
373
|
+
const result = await mathService.add(5, 3);
|
|
374
|
+
const user = await userService.getUser('123');
|
|
375
|
+
|
|
376
|
+
// 内容脚本也可以订阅observables
|
|
377
|
+
const counterObservable = new ContentObservable(
|
|
378
|
+
ICounterObservable,
|
|
379
|
+
'main',
|
|
380
|
+
(value) => {
|
|
381
|
+
// 基于实时数据更新内容脚本UI
|
|
382
|
+
updateContentScriptUI(value);
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
// 在DOM操作中使用
|
|
387
|
+
document.addEventListener('DOMContentLoaded', async () => {
|
|
388
|
+
const calculation = await mathService.multiply(2, 3);
|
|
389
|
+
document.body.appendChild(
|
|
390
|
+
createElement('div', `计算结果: ${calculation}`)
|
|
391
|
+
);
|
|
392
|
+
});
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### 内容脚本使用场景
|
|
396
|
+
|
|
397
|
+
内容脚本可以在各种场景中使用RPC:
|
|
398
|
+
|
|
399
|
+
1. **直接通信**: 在不涉及网页的情况下进行RPC调用
|
|
400
|
+
2. **数据处理**: 在注入页面之前处理来自背景服务的数据
|
|
401
|
+
3. **实时更新**: 订阅observables获取实时数据更新
|
|
402
|
+
4. **桥接+客户端**: 既作为网页的桥接器又作为直接客户端
|
|
403
|
+
5. **DOM操作**: 使用RPC数据修改页面内容
|
|
404
|
+
|
|
405
|
+
### 复杂数据类型
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
interface IUserService {
|
|
409
|
+
getUser(id: string): Promise<User>;
|
|
410
|
+
createUser(userData: CreateUserRequest): Promise<User>;
|
|
411
|
+
updateUser(id: string, updates: Partial<User>): Promise<User>;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
interface User {
|
|
415
|
+
id: string;
|
|
416
|
+
name: string;
|
|
417
|
+
email: string;
|
|
418
|
+
createdAt: Date;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
interface CreateUserRequest {
|
|
422
|
+
name: string;
|
|
423
|
+
email: string;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export const IUserService = createIdentifier<IUserService>('UserService');
|
|
427
|
+
|
|
428
|
+
// 使用示例
|
|
429
|
+
const client = new WebRPCClient();
|
|
430
|
+
const userService = client.createWebRPCService(IUserService);
|
|
431
|
+
|
|
432
|
+
const newUser = await userService.createUser({
|
|
433
|
+
name: 'John Doe',
|
|
434
|
+
email: 'john@example.com',
|
|
435
|
+
});
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### 多服务管理
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
// 创建RPC客户端
|
|
442
|
+
const client = new WebRPCClient();
|
|
443
|
+
|
|
444
|
+
// 创建多个服务代理
|
|
445
|
+
const mathService = client.createWebRPCService(IMathService);
|
|
446
|
+
const userService = client.createWebRPCService(IUserService);
|
|
447
|
+
const fileService = client.createWebRPCService(IFileService);
|
|
448
|
+
|
|
449
|
+
// 并行调用不同的服务
|
|
450
|
+
const [sum, user, file] = await Promise.all([
|
|
451
|
+
mathService.add(1, 2),
|
|
452
|
+
userService.getUser('123'),
|
|
453
|
+
fileService.readFile('config.json'),
|
|
454
|
+
]);
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
## 使用场景
|
|
458
|
+
|
|
459
|
+
### 场景1: 仅网页
|
|
460
|
+
- 网页需要与背景服务通信
|
|
461
|
+
- 使用: `WebRPCClient` + `ContentRPC` 桥接器
|
|
462
|
+
|
|
463
|
+
### 场景2: 仅内容脚本
|
|
464
|
+
- 内容脚本需要直接访问背景服务
|
|
465
|
+
- 使用: 直接使用 `ContentRPCClient`(无需桥接器)
|
|
466
|
+
|
|
467
|
+
### 场景3: 网页和内容脚本同时
|
|
468
|
+
- 两个上下文都需要RPC访问
|
|
469
|
+
- 使用: `ContentRPC` 桥接器 + `ContentRPCClient` 进行直接访问
|
|
470
|
+
|
|
471
|
+
### 场景4: 实时数据流
|
|
472
|
+
- 背景脚本需要向多个上下文推送更新
|
|
473
|
+
- 使用: `RemoteSubject` + `WebObservable`/`ContentObservable`
|
|
474
|
+
|
|
475
|
+
## API参考
|
|
476
|
+
|
|
477
|
+
### 核心类
|
|
478
|
+
|
|
479
|
+
- **`BackgroundRPC`**: 背景脚本的服务注册表和消息处理器
|
|
480
|
+
- **`ContentRPC`**: 网页和背景脚本间的消息桥接器
|
|
481
|
+
- **`WebRPCClient`**: 网页的RPC客户端
|
|
482
|
+
- **`ContentRPCClient`**: 内容脚本的直接RPC客户端
|
|
483
|
+
|
|
484
|
+
### Observable类
|
|
485
|
+
|
|
486
|
+
- **`RemoteSubject<T>`**: 可以向多个订阅者广播的Observable subject
|
|
487
|
+
- **`WebObservable<T>`**: 网页的Observable订阅者
|
|
488
|
+
- **`ContentObservable<T>`**: 内容脚本的Observable订阅者
|
|
489
|
+
|
|
490
|
+
### 工具函数
|
|
491
|
+
|
|
492
|
+
- **`createIdentifier<T>(key: string)`**: 创建类型安全的服务标识符
|
|
493
|
+
|
|
494
|
+
### 接口
|
|
495
|
+
|
|
496
|
+
- **`Identifier<T>`**: 类型安全的服务标识符接口
|
|
497
|
+
- **`RpcRequest`**: RPC请求消息结构
|
|
498
|
+
- **`RpcResponse`**: RPC响应消息结构
|
|
499
|
+
- **`IMessageAdapter`**: 消息传输抽象接口
|
|
500
|
+
- **`IDisposable`**: 资源管理接口
|
|
501
|
+
|
|
502
|
+
## 最佳实践
|
|
503
|
+
|
|
504
|
+
1. **服务接口设计**
|
|
505
|
+
- 使用清晰的方法名和适当的TypeScript类型
|
|
506
|
+
- 为异步操作支持返回Promise类型
|
|
507
|
+
- 定义详细的参数和返回值类型
|
|
508
|
+
- 保持接口专注和内聚
|
|
509
|
+
|
|
510
|
+
2. **资源管理**
|
|
511
|
+
- 需要清理时始终在RPC实例上调用 `dispose()`
|
|
512
|
+
- 使用已销毁的实例之前检查 `isDisposed()`
|
|
513
|
+
- 在组件卸载/销毁生命周期中进行适当的清理
|
|
514
|
+
|
|
515
|
+
3. **错误处理**
|
|
516
|
+
- 在服务方法中实现适当的错误处理
|
|
517
|
+
- 抛出有意义且描述性的错误
|
|
518
|
+
- 在客户端适当处理RPC错误
|
|
519
|
+
|
|
520
|
+
4. **性能优化**
|
|
521
|
+
- 避免频繁的小数据传输
|
|
522
|
+
- 可能时考虑批处理操作
|
|
523
|
+
- 对实时数据更新使用Observable模式
|
|
524
|
+
- 在适当的地方实现缓存策略
|
|
525
|
+
|
|
526
|
+
5. **安全考虑**
|
|
527
|
+
- 在服务实现中验证输入参数
|
|
528
|
+
- 不要通过RPC暴露敏感操作
|
|
529
|
+
- 对资源密集型操作考虑速率限制
|
|
530
|
+
|
|
531
|
+
## 许可证
|
|
532
|
+
|
|
533
|
+
MIT
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Identifier } from './id';
|
|
2
|
+
import type { SubjectLike } from './types';
|
|
3
|
+
import { Disposable } from './disposable';
|
|
4
|
+
export declare class BackgroundRPC extends Disposable {
|
|
5
|
+
private services;
|
|
6
|
+
constructor();
|
|
7
|
+
register<T>(service: Identifier<T>, serviceInstance: T): void;
|
|
8
|
+
}
|
|
9
|
+
export declare class RemoteSubject<T> extends Disposable implements SubjectLike<T> {
|
|
10
|
+
private identifier;
|
|
11
|
+
private _key;
|
|
12
|
+
private initialValue;
|
|
13
|
+
private completed;
|
|
14
|
+
private get _finalKey();
|
|
15
|
+
private senders;
|
|
16
|
+
constructor(identifier: Identifier<T>, _key: string, initialValue: T);
|
|
17
|
+
private _sendMessage;
|
|
18
|
+
next(value: T): void;
|
|
19
|
+
complete(): void;
|
|
20
|
+
subscribe(): () => void;
|
|
21
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { OBSERVABLE_EVENT, RPC_EVENT_NAME, RPC_RESPONSE_EVENT_NAME, SUBSCRIBABLE_OBSERVABLE, UNSUBSCRIBE_OBSERVABLE } from './const';
|
|
2
|
+
import { Disposable } from './disposable';
|
|
3
|
+
export class BackgroundRPC extends Disposable {
|
|
4
|
+
services = {};
|
|
5
|
+
constructor() {
|
|
6
|
+
super();
|
|
7
|
+
const handler = ((msg, sender) => {
|
|
8
|
+
if (msg.type !== RPC_EVENT_NAME)
|
|
9
|
+
return;
|
|
10
|
+
const senderId = sender.tab.id;
|
|
11
|
+
const sendResponse = (response) => {
|
|
12
|
+
chrome.tabs.sendMessage(senderId, {
|
|
13
|
+
...response,
|
|
14
|
+
type: RPC_RESPONSE_EVENT_NAME
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
const { id, method, args, service } = msg;
|
|
18
|
+
const serviceInstance = this.services[service];
|
|
19
|
+
if (!serviceInstance) {
|
|
20
|
+
const resp = {
|
|
21
|
+
id,
|
|
22
|
+
error: { message: `Unknown service: ${service}` },
|
|
23
|
+
service,
|
|
24
|
+
method,
|
|
25
|
+
};
|
|
26
|
+
sendResponse(resp);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (!(method in serviceInstance)) {
|
|
30
|
+
const resp = {
|
|
31
|
+
id,
|
|
32
|
+
error: { message: `Unknown method: ${method}` },
|
|
33
|
+
service,
|
|
34
|
+
method,
|
|
35
|
+
};
|
|
36
|
+
sendResponse(resp);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
Promise.resolve()
|
|
40
|
+
.then(() => serviceInstance[method](...args))
|
|
41
|
+
.then((result) => sendResponse({
|
|
42
|
+
id,
|
|
43
|
+
result,
|
|
44
|
+
service,
|
|
45
|
+
method
|
|
46
|
+
}))
|
|
47
|
+
.catch((err) => sendResponse({
|
|
48
|
+
id,
|
|
49
|
+
error: {
|
|
50
|
+
message: err.message,
|
|
51
|
+
stack: err.stack,
|
|
52
|
+
name: err.name
|
|
53
|
+
},
|
|
54
|
+
service,
|
|
55
|
+
method
|
|
56
|
+
}));
|
|
57
|
+
return true; // 异步 sendResponse
|
|
58
|
+
});
|
|
59
|
+
chrome.runtime.onMessage.addListener(handler);
|
|
60
|
+
this.disposeWithMe(() => {
|
|
61
|
+
chrome.runtime.onMessage.removeListener(handler);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
register(service, serviceInstance) {
|
|
65
|
+
this.services[service.key] = serviceInstance;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export class RemoteSubject extends Disposable {
|
|
69
|
+
identifier;
|
|
70
|
+
_key;
|
|
71
|
+
initialValue;
|
|
72
|
+
completed = false;
|
|
73
|
+
get _finalKey() {
|
|
74
|
+
return `${this.identifier.key}-${this._key}`;
|
|
75
|
+
}
|
|
76
|
+
senders = new Set();
|
|
77
|
+
constructor(identifier, _key, initialValue) {
|
|
78
|
+
super();
|
|
79
|
+
this.identifier = identifier;
|
|
80
|
+
this._key = _key;
|
|
81
|
+
this.initialValue = initialValue;
|
|
82
|
+
// 初始化时立即广播一次
|
|
83
|
+
const handleMessage = (msg, sender) => {
|
|
84
|
+
const senderId = sender.tab.id;
|
|
85
|
+
if (!senderId)
|
|
86
|
+
return;
|
|
87
|
+
if (msg.type === SUBSCRIBABLE_OBSERVABLE) {
|
|
88
|
+
const { key } = msg;
|
|
89
|
+
if (key === this._finalKey) {
|
|
90
|
+
this.senders.add(senderId);
|
|
91
|
+
chrome.tabs.sendMessage(senderId, {
|
|
92
|
+
operation: 'next',
|
|
93
|
+
key: this._finalKey,
|
|
94
|
+
value: this.initialValue,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (msg.type === UNSUBSCRIBE_OBSERVABLE) {
|
|
99
|
+
const { key } = msg;
|
|
100
|
+
if (key === this._finalKey) {
|
|
101
|
+
this.senders.delete(senderId);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
chrome.runtime.onMessage.addListener(handleMessage);
|
|
106
|
+
this.disposeWithMe(() => {
|
|
107
|
+
chrome.runtime.onMessage.removeListener(handleMessage);
|
|
108
|
+
});
|
|
109
|
+
const handleRemove = (tabId) => {
|
|
110
|
+
this.senders.delete(tabId);
|
|
111
|
+
};
|
|
112
|
+
chrome.tabs.onRemoved.addListener(handleRemove);
|
|
113
|
+
this.disposeWithMe(() => {
|
|
114
|
+
chrome.tabs.onRemoved.removeListener(handleRemove);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
_sendMessage(message) {
|
|
118
|
+
chrome.runtime.sendMessage(message);
|
|
119
|
+
this.senders.forEach(senderId => {
|
|
120
|
+
chrome.tabs.sendMessage(senderId, message);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
next(value) {
|
|
124
|
+
if (this.completed)
|
|
125
|
+
return;
|
|
126
|
+
this._sendMessage({
|
|
127
|
+
operation: 'next',
|
|
128
|
+
key: this._finalKey,
|
|
129
|
+
value,
|
|
130
|
+
type: OBSERVABLE_EVENT
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
complete() {
|
|
134
|
+
if (this.completed)
|
|
135
|
+
return;
|
|
136
|
+
this.completed = true;
|
|
137
|
+
this._sendMessage({
|
|
138
|
+
operation: 'complete',
|
|
139
|
+
key: this._finalKey,
|
|
140
|
+
type: OBSERVABLE_EVENT
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
subscribe() {
|
|
144
|
+
throw new Error('RemoteSubject should not be subscribed locally.');
|
|
145
|
+
}
|
|
146
|
+
}
|