Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

crx-rpc

weird941.1kMIT1.0.8TypeScript support: included

A lightweight RPC framework for Chrome Extension (background <-> content <-> web)

chrome-extension, rpc, content-script, background, typescript

readme

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)  │   │                 │   │                 │
└─────────────┘   └─────────────────┘   └─────────────────┘
        │                  │                       ▲
        │  CustomEventchrome.runtime      │
        │                  │  Messages            │
        └──────────────────┴──────────────────────┘
                           │
                    ┌─────────────────┐
                    │ContentRPCClient │
                    │   (直接)        │
                    │                 │
                    │ 代理服务        │
                    │ .subtract(5,2)  │
                    └─────────────────┘

通信流程

  1. 网页 → 内容脚本: 使用 window.dispatchEventCustomEvent
  2. 内容脚本 → 背景脚本: 使用 chrome.runtime.sendMessage
  3. 背景脚本 → 内容脚本: 使用 chrome.tabs.sendMessage
  4. 内容脚本 → 网页: 使用 window.dispatchEventCustomEvent
  5. 内容脚本直接: 直接使用 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支持

框架包含使用 RemoteSubjectObservable 模式的内置响应式数据流支持,采用集中式消息管理系统。

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:

  1. 直接通信: 在不涉及网页的情况下进行RPC调用
  2. 数据处理: 在注入页面之前处理来自背景服务的数据
  3. 实时更新: 订阅observables获取实时数据更新
  4. 桥接+客户端: 既作为网页的桥接器又作为直接客户端
  5. 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 subject
  • WebObservable<T>: 网页的Observable订阅者
  • ContentObservable<T>: 内容脚本的Observable订阅者

工具函数

  • createIdentifier<T>(key: string): 创建类型安全的服务标识符

接口

  • Identifier<T>: 类型安全的服务标识符接口
  • RpcRequest: RPC请求消息结构
  • RpcResponse: RPC响应消息结构
  • IMessageAdapter: 消息传输抽象接口
  • IDisposable: 资源管理接口

最佳实践

  1. 服务接口设计

    • 使用清晰的方法名和适当的TypeScript类型
    • 为异步操作支持返回Promise类型
    • 定义详细的参数和返回值类型
    • 保持接口专注和内聚
  2. 资源管理

    • 需要清理时始终在RPC实例上调用 dispose()
    • 使用已销毁的实例之前检查 isDisposed()
    • 在组件卸载/销毁生命周期中进行适当的清理
  3. 错误处理

    • 在服务方法中实现适当的错误处理
    • 抛出有意义且描述性的错误
    • 在客户端适当处理RPC错误
  4. 性能优化

    • 避免频繁的小数据传输
    • 可能时考虑批处理操作
    • 对实时数据更新使用Observable模式,通过 RemoteSubjectManager 进行高效消息路由
    • 在适当的地方实现缓存策略
    • 管理器自动处理订阅排队以防止竞态条件
  5. 安全考虑

    • 在服务实现中验证输入参数
    • 不要通过RPC暴露敏感操作
    • 对资源密集型操作考虑速率限制

许可证

MIT