Cap'n Web: 轻量级、双向对象能力RPC
Cap'n Web 是一个专为现代 Web 环境设计的 JavaScript 原生 RPC(Remote Procedure Call)系统。它是 Cap'n Proto 的精神继承者,但专为 Web 技术栈优化,摒弃了复杂的 Schema,拥抱 JSON 和 JavaScript 的灵活性,同时在核心保留了强大的对象能力(Object-Capability)模型。
核心优势在于其极致的表达能力和轻量化设计:仅 10kB (压缩后),无依赖,支持所有主流 JavaScript 运行时,让跨进程、跨网络通信如同调用本地方法一样简单、强大且安全。
功能特性
Cap'n Web 的核心优势在于其对对象能力模型的实现,这使其显著区别于传统 RPC 框架。
-
✨ 真正的双向调用 (Bidirectional RPC):协议本身是全然双向的。客户端可以调用服务器的方法,服务器同样可以调用客户端暴露的接口。这在实现回调、事件推送等场景时无需复杂的额外配置。
-
🔗 按引用传递函数与对象 (Pass-by-Reference):
- 函数引用:将客户端的一个函数作为参数传递给服务器,服务器获得的是一个“桩 (Stub)”。当服务器调用这个桩时,实际执行的代码仍在客户端。
- 对象引用:任何继承自
RpcTarget的类实例,在 RPC 中都会以引用的方式传递。对该对象的方法调用会通过网络路由回对象创建的原端。
-
⏳ Promise 管道 (Promise Pipelining):在发起一个异步 RPC 调用后,可以立即在其返回的 Promise 上继续链式调用,而无需等待第一个 Promise 完成。这极大地减少了网络往返延迟,提升了分布式应用的性能。
-
📦 零配置、无模式 (Schema-less):无需学习 IDL(接口定义语言)或生成胶水代码。直接使用 TypeScript/JavaScript 的类和函数定义接口,开箱即用。同时,TypeScript 类型定义确保了优秀的类型安全性。
-
🌐 多传输层支持 (Multi-Transport):内置了对主流传输协议的支持,可轻松扩展。
- WebSocket: 适用于需要全双工、低延迟通信的场景。
- HTTP 批处理 (HTTP Batch): 将多个请求合并到一个 HTTP 请求中,适合无状态的 Request-Response 模式。
postMessage(): 在浏览器环境的不同 Worker (Web Worker, Service Worker) 或 iframe 间进行通信。- 自定义: 通过实现简单的
RpcTransport接口,可以接入任何自定义的双工消息通道。
-
🛡️ 对象能力安全模型:基于引用的安全性是其核心。一个对象只有持有对另一个对象的引用才能与之交互,这种“最小权限”原则天然地构建了安全边界,简化了复杂分布式系统的安全保障。
-
⚡️ 跨运行时兼容:经过精心设计,可在所有主流浏览器、Node.js 和 Cloudflare Workers 中完美运行,拥有统一的 API 体验。
安装指南
Cap'n Web 是一个标准的 ES Module 包,可以通过 npm 或 yarn 进行安装。
系统要求
- JavaScript 运行时: 支持 ES2023 (或通过 polyfill 支持
Symbol.dispose) 的任何环境,包括:- 现代浏览器 (Chrome, Firefox, Safari, Edge)
- Node.js 18+
- Cloudflare Workers
- Deno (理论上支持)
- 包管理器: npm, yarn 或 pnpm
安装步骤
-
在你的项目目录中,使用 npm 或 yarn 安装:
npm install capnweb或
yarn add capnweb -
在你的代码中导入所需的功能:
// 导入核心类和函数 import { RpcTarget, RpcStub, newWebSocketRpcSession, newHttpBatchRpcSession } from 'capnweb';
使用说明
Cap'n Web 的设计理念是让 RPC 调用尽可能接近本地函数调用。以下是几个核心场景的示例。
基础示例:定义和暴露服务
首先,定义一个服务类,继承自 RpcTarget。这个类的实例可以被远程访问。
// server.js (或 server.ts)
import { RpcTarget } from 'capnweb';
// 一个简单的计数器,将作为远程对象暴露
export class Counter extends RpcTarget {
constructor(private value: number = 0) {
super();
}
// 公共方法将可供远程调用
increment(amount: number = 1): number {
this.value += amount;
return this.value;
}
// Getter 同样可以远程调用
get currentValue(): number {
return this.value;
}
}
WebSocket 全双工通信
此示例展示了如何通过 WebSocket 建立一个双向 RPC 连接,服务端可以调用客户端提供的回调函数。
服务端 (Node.js 或 Workers)
import { WebSocketServer } from 'ws'; // Node.js 环境
import { newWebSocketRpcSession, RpcTarget } from 'capnweb';
import { Counter } from './server.js'; // 引入上面的 Counter 类
// 假设已创建 WebSocket 服务器 wss
wss.on('connection', (ws) => {
// 创建一个 Counter 实例作为服务端主入口点
const mainCounter = new Counter(10);
// 启动 RPC 会话,将 mainCounter 暴露给客户端
// 返回的 remoteStub 代表客户端的 'main' 接口
const remoteClientMain = newWebSocketRpcSession(ws, mainCounter);
// 3秒后,通过 remoteClientMain 调用客户端的方法
setTimeout(async () => {
// 假设客户端有一个 'report' 函数
if (remoteClientMain.report) {
try {
const response = await remoteClientMain.report('Server count is now: ' + await mainCounter.currentValue);
console.log('Client reported back:', response);
} catch (err) {
console.error('Failed to call client report:', err);
}
}
}, 3000);
});
客户端 (浏览器)
import { newWebSocketRpcSession, RpcTarget } from 'capnweb';
// 客户端也可以暴露自己的服务给服务器调用
class ClientReporter extends RpcTarget {
report(message: string): string {
console.log('Report from server:', message);
return 'Message received by client!';
}
}
const ws = new WebSocket('ws://localhost:8080');
// 启动 RPC 会话,将 ClientReporter 实例暴露给服务器
// remoteServerStub 代表服务端的 'main' 接口 (即 Counter 实例)
const remoteServerStub = newWebSocketRpcSession(ws, new ClientReporter());
// 可以直接调用服务器上的 Counter 方法
async function run() {
console.log(await remoteServerStub.currentValue); // 输出: 10
console.log(await remoteServerStub.increment(5)); // 输出: 15
}
run();
HTTP 批处理通信
适用于请求-响应模式,将多个调用合并到一个 HTTP 请求中。
服务端 (Node.js)
import http from 'node:http';
import { nodeHttpBatchRpcResponse } from 'capnweb';
import { Counter } from './server.js';
const server = http.createServer((req, res) => {
// 假设我们将 '/api' 路径用于 RPC
if (req.url?.startsWith('/api')) {
// 处理批处理 RPC 请求,暴露一个新的 Counter 实例
nodeHttpBatchRpcResponse(req, res, new Counter(100));
} else {
res.writeHead(404).end();
}
});
server.listen(3000);
客户端 (浏览器或 Node.js)
import { newHttpBatchRpcSession } from 'capnweb';
// 创建一个指向服务端端点的会话
const remoteCounter = newHttpBatchRpcSession('http://localhost:3000/api');
async function run() {
// 尽管是 HTTP 批处理,但使用方式与 WebSocket 一致
console.log(await remoteCounter.currentValue); // 输出: 100
console.log(await remoteCounter.increment(10)); // 输出: 110
// 注意:这里的两个调用可能会被合并到同一个 HTTP 请求中(取决于内部批处理策略)
}
run();
核心代码
1. 核心类型定义与序列化 (src/serialize.ts 节选)
该代码片段展示了 Cap'n Web 如何对非 JSON 原生类型(如 Date, BigInt, Error)进行编码。它使用一个带类型标记的数组来表示这些特殊类型,从而在保持 JSON 可读性的同时扩展了其能力。
// src/serialize.ts (节选)
// 判断一个值在 RPC 中应被视为何种类型
export function typeForRpc(value: unknown): TypeForRpc {
// ... 基础类型判断 ...
// 对于对象和函数,通过其原型链来判断
let prototype = Object.getPrototypeOf(value);
switch (prototype) {
case Object.prototype:
return "object";
case Function.prototype:
return "function";
case Array.prototype:
return "array";
case Date.prototype:
return "date";
case Error.prototype: // 检查是否是 Error 或其子类
// 特别检查常见的错误类型
if (value instanceof TypeError) return "error";
if (value instanceof RangeError) return "error";
// ... 其他错误类型
return "error";
// ... 处理 RpcTarget, Promise 等
}
// ...
}
// 在序列化时,对于非基本类型,会调用类似的方法将其转换为可传输的格式
// 例如,一个 Date 对象会被转换为:["date", 1749342170815]
2. 可远程调用的目标基类 (src/core.ts 节选)
任何希望被远程调用的类都应该继承 RpcTarget。这个基类为 RPC 系统提供了一个身份标记,确保该对象在网络上是以引用而非值的方式传递。
// src/core.ts (节选)
// 一个独特的 Symbol 品牌,用于在类型系统中标识 RpcTarget
export const __RPC_TARGET_BRAND: unique symbol = Symbol.for('capnweb.rpcTarget');
// RpcTarget 类的类型定义,它使用品牌来防止结构类型误用
export interface RpcTarget {
[__RPC_TARGET_BRAND]: never;
};
// RpcTarget 的具体实现。在支持 cloudflare:workers 的环境中,
// 可能会使用其原生实现以获得更好的集成。
export let RpcTarget = workersModule ? workersModule.RpcTarget : class {
// 一个空的基类,其主要作用是提供品牌标记。
// 实际逻辑由 RPC 系统通过 Proxy 和内部表处理。
};
3. WebSocket 传输层实现 (src/websocket.ts 节选)
该代码展示了 WebSocketTransport 如何实现 RpcTransport 接口,将 WebSocket 的消息事件适配成 RPC 核心引擎所需的简单 send() 和 receive() 方法。
// src/websocket.ts (节选)
class WebSocketTransport implements RpcTransport {
#webSocket: WebSocket;
#sendQueue?: string[]; // 连接建立前暂存消息
#receiveResolver?: (message: string) => void;
#receiveQueue: string[] = [];
#error?: any;
async send(message: string): Promise<void> {
// 如果连接已就绪,直接发送;否则加入队列等待 open 事件
if (this.#sendQueue === undefined) {
this.#webSocket.send(message);
} else {
this.#sendQueue.push(message);
}
}
async receive(): Promise<string> {
// 如果有缓存的消息,立即返回
if (this.#receiveQueue.length > 0) {
return this.#receiveQueue.shift()!;
} else if (this.#error) {
throw this.#error;
} else {
// 否则,等待下一个 message 事件
return new Promise<string>((resolve, reject) => {
this.#receiveResolver = resolve;
this.#receiveRejecter = reject;
});
}
}
// 当 RPC 会话需要中止时调用,尝试通知对方并清理资源
abort?(reason: any): void {
let message = reason instanceof Error ? reason.message : `${reason}`;
this.#webSocket.close(3000, message);
// ... 清理内部状态
}
// 构造函数中设置事件监听器,将 WebSocket 事件转换为 Promise 的 resolve/reject
constructor(webSocket: WebSocket) {
this.#webSocket = webSocket;
// ... 处理 open, message, close, error 事件
webSocket.addEventListener("message", (event) => {
if (typeof event.data === "string") {
if (this.#receiveResolver) {
this.#receiveResolver(event.data);
this.#receiveResolver = undefined;
} else {
this.#receiveQueue.push(event.data);
}
}
});
// ...
}
}
```FINISHED
P14CvGTT35tVv2ry9KnGB1EihlQqe/flwBmfOGjBF1c=