//server.ts
class Controller {
hello(title: string): string {
return 'Hello ' + title
像 "hello "这样的方法通常在服务器上的一个类中实现,然后可以从远程客户端调用。
//client.ts
const client = new RpcClient('localhost');
const controller = client.controller<Controller>();
const result = await controller.hello('World'); // => 'Hello World';
由于RPC从根本上是基于异步通信的,所以通信主要是通过HTTP,但也可以通过TCP或WebSockets发生。这意味着TypeScript本身的所有函数调用都被转换为一个`承诺'。有了相应的`await',可以异步接收结果。
只要一个项目在客户端(通常是前端)和服务器(后端)使用TypeScript,它就被称为Isomorphic TypeScript。那么,基于TypeScript的类型安全的RPC框架对这样的项目来说是特别有利的,因为类型可以在客户端和服务器之间共享。
为了利用这一点,在两边都使用的类型应该被换成一个单独的文件或包。在各自的页面上进行导入,然后再次合并它们。
//shared.ts
export class User {
id: number;
username: string;
interface UserControllerApi {
getUser(id: number): Promise<User>;
//server.ts
import { User } from './shared';
class UserController implements UserControllerApi {
async getUser(id: number): Promise<User> {
return await datbase.query(User).filter({id}).findOne();
//client.ts
import { UserControllerApi } from './shared';
const controller = client.controller<UserControllerApi>();
const user = await controller.getUser(2); // => User
接口`UserControllerApi`在这里充当了客户端和服务器之间的契约。服务器必须正确地实现这一点,客户端可以消费它。
向后兼容可以用与普通本地API相同的方式实现:要么将新的参数标记为可选,要么添加一个新的方法。
虽然也可以通过`import type { UserController } from './server.ts’直接导入`UserController',但这有其他缺点,比如不支持名义类型(这意味着类的实例不能用`instanceof’检查)。
下面是一个基于WebSockets和`@deepkit/rpc`的低级API的全功能例子。一旦使用Deepkit框架,控制器就会通过应用模块提供,而不需要手动实例化RpcKernel。
文件:server.ts_
import { rpc, RpcKernel } from '@deepkit/rpc';
import { RpcWebSocketServer } from '@deepkit/rpc-tcp';
@rpc.controller('myController');
export class Controller {
@rpc.action()
hello(title: string): string {
return 'Hello ' + title;
const kernel = new RpcKernel();
kernel.registerController(Controller);
const server = new RpcWebSocketServer(kernel, 'localhost:8081');
server.start();
档。client.ts_
import { RpcWebSocketClient } from '@deepkit/rpc';
import type { Controller } from './server';
async function main() {
const client = new RpcWebSocketClient('localhost:8081');
const controller = client.controller<Controller>('myController');
const result = await controller.hello('World');
console.log('result', result);
client.disconnect();
main().catch(console.error);
import { RpcKernel, rpc } from '@deepkit/rpc';
import { App } from '@deepkit/app';
import { Database, User } from './database';
@rpc.controller('my')
class Controller {
constructor(private database: Database) {}
@rpc.action()
async getUser(id: number): Promise<User> {
return await this.database.query(User).filter({id}).findOne();
new App({
providers: [{provide: Database, useValue: new Database}]
controllers: [Controller],
}).run();
然而,只要手动实例化一个`RpcKernel',DI容器也可以被传递到那里。然后,RPC控制器通过这个DI容器被实例化。
import { RpcKernel, rpc } from '@deepkit/rpc';
import { InjectorContext } from '@deepkit/injector';
import { Database, User } from './database';
@rpc.controller('my')
class Controller {
constructor(private database: Database) {}
@rpc.action()
async getUser(id: number): Promise<User> {
return await this.database.query(User).filter({id}).findOne();
const injector = InjectorContext.forProviders([
Controller,
{provide: Database, useValue: new Database},
const kernel = new RpcKernel(injector);
kernel.registerController(Controller);
参见依赖注入以了解更多。
当客户端收到来自函数调用的数据时,它先前在服务器上被序列化,然后在客户端被反序列化。如果现在在函数的返回类型中使用了类,它们会在客户端被重构,但会失去它们的名义身份和所有方法。为了抵制这种行为,类可以通过一个唯一的ID注册为名义类型。对于所有在RPC API中使用的类都应该这样做。
要注册一个类,必须使用装饰器`@entity.name('id')`。
import { entity } from '@deepkit/type';
@entity.name('user')
class User {
id!: number;
firstName!: string;
lastName!: string;
get fullName() {
return this.firstName + ' ' + this.lastName;
只要这个类现在被用作一个函数的结果,它的身份就被保留了。
const controller = client.controller<Controller>('controller');
const user = await controller.getUser(2);
user instanceof User; //true when @entity.name is used, and false if not
//client
//[MyError] makes sure the class MyError is known in runtime
const controller = client.controller<Controller>('controller', [MyError]);
try {
await controller.getUser(2);
} catch (e) {
if (e instanceof MyError) {
//ops, could not save user
} else {
//all other errors
import { RpcKernelSecurity, Session, RpcControllerAccess } from '@deepkit/type';
//contains default implementations
class MyKernelSecurity extends RpcKernelSecurity {
async hasControllerAccess(session: Session, controllerAccess: RpcControllerAccess): Promise<boolean> {
return true;
async isAllowedToRegisterAsPeer(session: Session, peerId: string): Promise<boolean> {
return true;
async isAllowedToSendToPeer(session: Session, peerId: string): Promise<boolean> {
return true;
async authenticate(token: any): Promise<Session> {
throw new Error('Authentication not implemented');
transformError(err: Error) {
return err;
Um diese zu nutzen wird entweder dem RpcKernel
der Provider übergeben:
const kernel = new RpcKernel([{provide: RpcKernelSecurity, useClass: MyKernelSecurity, scope: 'rpc'}]);
Oder im Falle einer Deepkit Framework Anwendung die Klasse RpcKernelSecurity
mit einem Provider in der App überschrieben.
import { App } from '@deepkit/type';
import { RpcKernelSecurity } from '@deepkit/rpc';
import { FrameworkModule } from '@deepkit/framework';
new App({
controllers: [MyRpcController],
providers: [
{provide: RpcKernelSecurity, useClass: MyRpcKernelSecurity, scope: 'rpc'}
imports: [new FrameworkModule]
}).run();
对象`session`默认是一个匿名会话,这意味着客户端没有认证自己。一旦它想进行认证,就会调用`authenticate`方法。 `authenticate`方法收到的令牌来自客户端,可以有任何值。
一旦客户端设置了一个令牌,只要调用第一个RPC函数或手动`client.connect()`,就会进行认证。
const client = new RpcWebSocketClient('localhost:8081');
client.token.set('123456789');
const controller = client.controller<Controller>('myController');
这里`RpcKernelSecurity.authenticate`接收到令牌`123456789`并可以相应地返回另一个会话。这个返回的会话将被传递给所有其他的方法,如`hasControllerAccess`。
import { Session, RpcKernelSecurity } from '@deepkit/rpc';
class UserSession extends Session {
class MyKernelSecurity extends RpcKernelSecurity {
async hasControllerAccess(session: Session, controllerAccess: RpcControllerAccess): Promise<boolean> {
if (controllerAccess.controllerClassType instanceof MySecureController) {
//MySecureController requires UserSession
return session instanceof UserSession;
return true;
async authenticate(token: any): Promise<Session> {
if (token === '123456789') {
return new UserSession('username', token);
throw new Error('Authentication failed');
`hasControllerAccess`方法可用于确定客户端是否被允许执行特定的RPC功能。这个方法在每次RPC函数调用时都会执行。如果它返回 "false",则拒绝访问,并向客户抛出一个错误。
在`RpcControllerAccess`中,有几条关于RPC函数的有价值的信息。
interface RpcControllerAccess {
controllerName: string;
controllerClassType: ClassType;
actionName: string;
actionGroups: string[];
actionData: { [name: string]: any };
组和其他数据可以通过装饰器`@rpc.action()`来改变。
class Controller {
@rpc.action().group('secret').data('role', 'admin')
saveUser(user: User): void {
class MyKernelSecurity extends RpcKernelSecurity {
async hasControllerAccess(session: Session, controllerAccess: RpcControllerAccess): Promise<boolean> {
if (controllerAccess.actionGroups.includes('secret')) {
//todo: check
return false;
return true;
class MyKernelSecurity extends RpcKernelSecurity {
transformError(error: Error) {
//wrap in new error
return new Error('Something went wrong: ' + error.message);
请注意,一旦错误被转换为一般的 "错误",完整的堆栈跟踪和错误的身份就会丢失。因此,在客户端的错误上不能使用`instanceof`检查。
如果在两个微服务之间使用Deepkit RPC,并且客户端和服务器因此处于开发者的完全控制之下,那么转变错误就很少有必要。另一方面,如果客户端在一个未知的人的浏览器中运行,那么你应该在`transformError’中非常小心地确定你想披露的信息。如果有疑问,每个错误都应该用一个通用的 "错误 "进行转换,以确保没有内部细节被泄露。在这一点上,记录错误将是一个好主意。
如果直接使用Deepkit RPC库,则要实例化`RpcKernelSecurity`类本身。如果这个类需要一个数据库或一个记录器,这必须被传入本身。
如果使用Deepkit框架,该类由依赖注入容器实例化,从而自动访问应用程序的所有其他提供者。
另见依赖性注入。
Deepkit RPC支持几种传输协议。WebSockets是兼容性最好的协议(因为浏览器支持它),同时也支持流媒体等所有功能。TCP通常更快,非常适合服务器(微服务)或非浏览器客户端之间的通信。
Deepkit的RPC HTTP协议是一个变种,在浏览器中特别容易调试,因为每个函数调用都是一个HTTP请求,但也有其局限性,如不支持RxJS流。
TODO: 还没有实施。
@deepkit/rpc-tcp `RpcWebSocketServer`和浏览器WebSocket或Node `ws`包。
@deepkit/rpc-tcp RpcNetTcpServer`和`RpcNetTcpClientAdapter
。