Chrome 扩展 RPC 框架 (crx-rpc)
一个轻量级、类型安全的Chrome扩展RPC框架,支持网页、内容脚本和背景脚本之间的通信。基于TypeScript构建,提供最大的类型安全性和开发体验。
特性
- 🔒 类型安全: 完整的TypeScript类型支持,自动代理类型生成
- 🚀 易于使用: 基于接口自动生成客户端代理
- 🔄 双向通信: 支持网页 ↔ 内容脚本 ↔ 背景脚本通信
- 📦 零配置: 无需手动方法绑定
- 🎯 Observable支持: 内置响应式数据流支持,使用RemoteSubject
- 🛡️ 错误处理: 跨边界保留堆栈跟踪和错误类型
- 🧹 资源管理: 内置disposable模式,支持清理资源
安装
npm install crx-rpc
# 或
pnpm add crx-rpc
# 或
yarn add crx-rpc
快速开始
1. 定义服务接口
// services/math.ts
import { createIdentifier } from 'crx-rpc';
interface IMathService {
add(a: number, b: number): Promise<number>;
subtract(a: number, b: number): Promise<number>;
multiply(a: number, b: number): Promise<number>;
divide(a: number, b: number): Promise<number>;
}
// 创建服务标识符
export const IMathService = createIdentifier<IMathService>('MathService');
2. 实现服务(背景脚本)
// background.ts
import { BackgroundRPC } from 'crx-rpc';
import { IMathService } from './services/math';
class MathService implements IMathService {
async add(a: number, b: number): Promise<number> {
return a + b;
}
async subtract(a: number, b: number): Promise<number> {
return a - b;
}
async multiply(a: number, b: number): Promise<number> {
return a * b;
}
async divide(a: number, b: number): Promise<number> {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
}
// 注册服务,可选择启用日志
const rpc = new BackgroundRPC(true); // 启用日志
// const rpc = new BackgroundRPC(); // 禁用日志(默认)
rpc.register(IMathService, new MathService());
3. 初始化内容脚本
内容脚本可以以两种模式工作:
选项A:作为桥接器(用于网页通信)
// content.ts
import { ContentRPC } from 'crx-rpc';
// 为网页 ↔ 背景脚本通信初始化RPC桥接器
const contentRpc = new ContentRPC();
// 需要清理时记得dispose
// contentRpc.dispose();
选项B:作为直接客户端
// content.ts
import { ContentRPCClient } from 'crx-rpc';
import { IMathService } from './services/math';
// 将内容脚本用作直接RPC客户端
const client = new ContentRPCClient();
const mathService = client.createWebRPCService(IMathService);
// 直接调用背景服务
const result = await mathService.add(5, 3);
console.log('内容脚本结果:', result);
// 需要清理时记得dispose
// client.dispose();
选项C:既是桥接器又是客户端
// content.ts
import { ContentRPC, ContentRPCClient } from 'crx-rpc';
import { IMathService } from './services/math';
// 为网页初始化桥接器
const bridge = new ContentRPC();
// 同时用作直接客户端
const client = new ContentRPCClient();
const mathService = client.createWebRPCService(IMathService);
// 内容脚本可以进行自己的RPC调用
const result = await mathService.multiply(2, 3);
console.log('内容脚本计算:', result);
4. 使用客户端(网页)
// web-page.ts
import { WebRPCClient } from 'crx-rpc';
import { IMathService } from './services/math';
async function calculate() {
// 创建RPC客户端
const client = new WebRPCClient();
// 创建类型安全的服务代理
const mathService = client.createWebRPCService(IMathService);
// 类型安全的方法调用
const sum = await mathService.add(1, 2); // TypeScript知道这返回Promise<number>
const difference = await mathService.subtract(10, 5);
const product = await mathService.multiply(3, 4);
const quotient = await mathService.divide(15, 3);
console.log('结果:', { sum, difference, product, quotient });
// 需要清理时记得dispose
// client.dispose();
}
架构
网页 内容脚本 背景脚本
┌─────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ WebRPCClient│──▶│ ContentRPC │──▶│ BackgroundRPC │
│ │ │ (桥接器) │ │ │
│ 代理 │ │ │ │ 服务 │
│ 服务 │ │ MessageAdapter │ │ 注册表 │
│ .add(1, 2) │ │ │ │ │
└─────────────┘ └─────────────────┘ └─────────────────┘
│ │ ▲
│ CustomEvent │ chrome.runtime │
│ │ Messages │
└──────────────────┴──────────────────────┘
│
┌─────────────────┐
│ContentRPCClient │
│ (直接) │
│ │
│ 代理服务 │
│ .subtract(5,2) │
└─────────────────┘
通信流程
- 网页 → 内容脚本: 使用
window.dispatchEvent
和CustomEvent
- 内容脚本 → 背景脚本: 使用
chrome.runtime.sendMessage
- 背景脚本 → 内容脚本: 使用
chrome.tabs.sendMessage
- 内容脚本 → 网页: 使用
window.dispatchEvent
和CustomEvent
- 内容脚本直接: 直接使用
chrome.runtime.sendMessage
(ContentRPCClient)
核心组件
- WebRPCClient: 用于网页的客户端,使用window事件
- ContentRPC: 在网页和背景脚本间转发消息的桥接器
- ContentRPCClient: 内容脚本的直接RPC客户端(绕过桥接器)
- BackgroundRPC: 背景脚本中的服务注册表和处理器
- RPCClient: 具有服务代理生成功能的基础客户端
日志支持
框架包含内置的日志支持,用于调试和监控RPC调用。
启用日志
// 在BackgroundRPC中启用日志
const rpc = new BackgroundRPC(true); // 启用日志
// const rpc = new BackgroundRPC(); // 禁用日志(默认)
// 示例输出:
// [RPC] Call: MathService.add { id: "123", args: [5, 3], senderId: 456, timestamp: "2025-09-01T10:00:00.000Z" }
// [RPC] Success: MathService.add { id: "123", result: 8, timestamp: "2025-09-01T10:00:00.001Z" }
// 对于错误:
// [RPC] Error: MathService.divide { id: "124", error: "Division by zero", timestamp: "2025-09-01T10:00:01.000Z" }
日志输出
启用日志时,会记录以下信息:
- 函数调用: 服务名、方法名、参数、发送者ID和时间戳
- 成功响应: 服务名、方法名、结果和时间戳
- 错误响应: 服务名、方法名、错误消息和时间戳
- 未知服务/方法: 无效服务或方法调用的警告
使用场景
- 开发: 在开发期间调试RPC通信
- 生产监控: 跟踪RPC使用模式和性能
- 故障排除: 识别失败的调用和错误模式
- 安全审计: 监控RPC访问模式
Observable支持
框架包含使用 RemoteSubject
和 Observable
模式的内置响应式数据流支持,采用集中式消息管理系统。
RemoteSubjectManager 和 RemoteSubject(背景脚本)
RemoteSubjectManager
作为集中式消息中心处理所有订阅管理和消息路由,而 RemoteSubject
专注于纯状态管理。
// background.ts
import { BackgroundRPC, RemoteSubjectManager, createIdentifier } from 'crx-rpc';
interface ICounterObservable {
value: number;
}
const ICounterObservable = createIdentifier<ICounterObservable>('Counter');
const rpc = new BackgroundRPC();
// 创建集中式subject管理器
const subjectManager = new RemoteSubjectManager();
// 通过管理器创建远程subject
const counterSubject = subjectManager.createSubject(
ICounterObservable,
'main',
{ value: 0 }
);
// 更新值并广播给所有订阅者
setInterval(() => {
const newValue = { value: Math.floor(Math.random() * 100) };
counterSubject.next(newValue);
}, 1000);
// 管理器处理:
// - 消息路由和订阅管理
// - 在subject创建前到达的订阅排队
// - tab关闭时自动清理
// - 向多个订阅者广播
// 清理
// subjectManager.dispose(); // 这将处理所有subject
RemoteSubjectManager 的核心特性
- 集中式消息中心: 所有observable相关的消息都由管理器处理
- 队列管理: 在subject创建前收到的订阅会被排队并稍后处理
- 资源管理: tab关闭时自动清理订阅
- 类型安全: 完整的TypeScript支持和恰当的类型检查
架构
┌─────────────────┐ ┌─────────────────────────────────────┐ ┌─────────────────┐
│ 网页 │ │ 背景脚本 │ │ 内容脚本 │
├─────────────────┤ ├─────────────────────────────────────┤ ├─────────────────┤
│ WebObservable │ │ RemoteSubjectManager │ │ContentObservable│
│ │ │ ┌─────────────────────────────────┐│ │ │
│ subscribe() ────┼───▶│ │ 消息路由和队列管理 │ │◄──┤ subscribe() │
│ │◄───│ │ │ │ │ │
└─────────────────┘ │ └─────────────────────────────────┘ │ └─────────────────┘
│ │ │
│ ┌─────────────▼─────────────────┐ │
│ │ RemoteSubject │ │
│ │ (纯状态管理) │ │
│ │ │ │
│ │ next() ─────────────────────▶ │ │
│ │ complete() ─────────────────▶ │ │
│ └───────────────────────────────┘ │
└──────────────────────────────────────┘
从网页订阅
// web-page.ts
import { WebObservable, createIdentifier } from 'crx-rpc';
interface ICounterObservable {
value: number;
}
const ICounterObservable = createIdentifier<ICounterObservable>('Counter');
// 订阅远程observable
const observable = new WebObservable(
ICounterObservable,
'main',
(value) => {
console.log('计数器更新:', value.value);
}
);
// 完成时清理
// observable.dispose();
从内容脚本订阅
// content.ts
import { ContentObservable, createIdentifier } from 'crx-rpc';
interface ICounterObservable {
value: number;
}
const ICounterObservable = createIdentifier<ICounterObservable>('Counter');
// 内容脚本可以直接订阅observables
const observable = new ContentObservable(
ICounterObservable,
'main',
(value) => {
console.log('来自内容脚本的计数器:', value.value);
// 内容脚本可以响应实时更新
updateUI(value.value);
}
);
// 完成时清理
// observable.dispose();
Observable通信模式
Observable系统支持多种具有集中式管理的通信模式:
// 模式1: 背景脚本 → 网页 (通过内容脚本桥接器)
// 背景脚本: RemoteSubjectManager创建和管理RemoteSubject
// 背景脚本: RemoteSubject.next() → Manager路由到订阅者
// 网页: WebObservable.subscribe()
// 模式2: 背景脚本 → 内容脚本 (直接)
// 背景脚本: RemoteSubject.next() → Manager直接路由
// 内容脚本: ContentObservable.subscribe()
// 模式3: 背景脚本 → 网页和内容脚本同时
// 背景脚本: RemoteSubject.next() → Manager广播给所有订阅者
// 网页: WebObservable.subscribe()
// 内容脚本: ContentObservable.subscribe()
// 模式4: Subject创建前的订阅 (队列管理)
// 订阅者: WebObservable.subscribe() → Manager将订阅排队
// 背景脚本: 稍后创建RemoteSubject → Manager处理排队的订阅
// 结果: 不会错过初始值,保证订阅顺序
高级用法
使用Disposables进行资源管理
所有RPC组件都继承了 Disposable
类来进行适当的清理:
import { WebRPCClient, ContentRPC, BackgroundRPC } from 'crx-rpc';
const client = new WebRPCClient();
const contentRpc = new ContentRPC();
const backgroundRpc = new BackgroundRPC();
// 适当的清理
function cleanup() {
client.dispose();
contentRpc.dispose();
backgroundRpc.dispose();
}
// 检查是否已经disposed
if (!client.isDisposed()) {
const service = client.createWebRPCService(IMathService);
// 使用服务...
}
内容脚本作为直接客户端
内容脚本具有完整的RPC功能,可以作为直接客户端而无需通过网页桥接:
// content.ts
import { ContentRPCClient, ContentObservable } from 'crx-rpc';
import { IMathService, IUserService } from './services';
const client = new ContentRPCClient();
// 创建服务代理
const mathService = client.createWebRPCService(IMathService);
const userService = client.createWebRPCService(IUserService);
// 直接调用背景服务
const result = await mathService.add(5, 3);
const user = await userService.getUser('123');
// 内容脚本也可以订阅observables
const counterObservable = new ContentObservable(
ICounterObservable,
'main',
(value) => {
// 基于实时数据更新内容脚本UI
updateContentScriptUI(value);
}
);
// 在DOM操作中使用
document.addEventListener('DOMContentLoaded', async () => {
const calculation = await mathService.multiply(2, 3);
document.body.appendChild(
createElement('div', `计算结果: ${calculation}`)
);
});
内容脚本使用场景
内容脚本可以在各种场景中使用RPC:
- 直接通信: 在不涉及网页的情况下进行RPC调用
- 数据处理: 在注入页面之前处理来自背景服务的数据
- 实时更新: 订阅observables获取实时数据更新
- 桥接+客户端: 既作为网页的桥接器又作为直接客户端
- DOM操作: 使用RPC数据修改页面内容
复杂数据类型
interface IUserService {
getUser(id: string): Promise<User>;
createUser(userData: CreateUserRequest): Promise<User>;
updateUser(id: string, updates: Partial<User>): Promise<User>;
}
interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
interface CreateUserRequest {
name: string;
email: string;
}
export const IUserService = createIdentifier<IUserService>('UserService');
// 使用示例
const client = new WebRPCClient();
const userService = client.createWebRPCService(IUserService);
const newUser = await userService.createUser({
name: 'John Doe',
email: 'john@example.com',
});
多服务管理
// 创建RPC客户端
const client = new WebRPCClient();
// 创建多个服务代理
const mathService = client.createWebRPCService(IMathService);
const userService = client.createWebRPCService(IUserService);
const fileService = client.createWebRPCService(IFileService);
// 并行调用不同的服务
const [sum, user, file] = await Promise.all([
mathService.add(1, 2),
userService.getUser('123'),
fileService.readFile('config.json'),
]);
使用场景
场景1: 仅网页
- 网页需要与背景服务通信
- 使用:
WebRPCClient
+ContentRPC
桥接器
场景2: 仅内容脚本
- 内容脚本需要直接访问背景服务
- 使用: 直接使用
ContentRPCClient
(无需桥接器)
场景3: 网页和内容脚本同时
- 两个上下文都需要RPC访问
- 使用:
ContentRPC
桥接器 +ContentRPCClient
进行直接访问
场景4: 实时数据流
- 背景脚本需要向多个上下文推送更新
- 使用:
RemoteSubject
+WebObservable
/ContentObservable
API参考
核心类
BackgroundRPC
: 背景脚本的服务注册表和消息处理器ContentRPC
: 网页和背景脚本间的消息桥接器WebRPCClient
: 网页的RPC客户端ContentRPCClient
: 内容脚本的直接RPC客户端RemoteSubjectManager
: 集中式observable消息管理系统
Observable类
RemoteSubjectManager
: 管理订阅和消息路由的集中式消息中心RemoteSubject<T>
: 与管理器配合进行纯状态管理的Observable subjectWebObservable<T>
: 网页的Observable订阅者ContentObservable<T>
: 内容脚本的Observable订阅者
工具函数
createIdentifier<T>(key: string)
: 创建类型安全的服务标识符
接口
Identifier<T>
: 类型安全的服务标识符接口RpcRequest
: RPC请求消息结构RpcResponse
: RPC响应消息结构IMessageAdapter
: 消息传输抽象接口IDisposable
: 资源管理接口
最佳实践
服务接口设计
- 使用清晰的方法名和适当的TypeScript类型
- 为异步操作支持返回Promise类型
- 定义详细的参数和返回值类型
- 保持接口专注和内聚
资源管理
- 需要清理时始终在RPC实例上调用
dispose()
- 使用已销毁的实例之前检查
isDisposed()
- 在组件卸载/销毁生命周期中进行适当的清理
- 需要清理时始终在RPC实例上调用
错误处理
- 在服务方法中实现适当的错误处理
- 抛出有意义且描述性的错误
- 在客户端适当处理RPC错误
性能优化
- 避免频繁的小数据传输
- 可能时考虑批处理操作
- 对实时数据更新使用Observable模式,通过
RemoteSubjectManager
进行高效消息路由 - 在适当的地方实现缓存策略
- 管理器自动处理订阅排队以防止竞态条件
安全考虑
- 在服务实现中验证输入参数
- 不要通过RPC暴露敏感操作
- 对资源密集型操作考虑速率限制
许可证
MIT