TSRPC 是什么
TSRPC 是一个 TypeScript 的 RPC 框架,适用于浏览器 Web 应用、WebSocket 实时应用、NodeJS 微服务等场景。 是目前世界上唯一支持 TypeScript 复杂类型运行时检测和二进制序列化的 RPC 框架
中文文档:https://tsrpc.cn GitHub:https://github.com/k8w/tsrpc 视频教程:https://www.bilibili.com/video/BV1hM4y1u7B4
目前,大多数项目仍在使用传统的 Restful API 进行前后端通信,这存在一些痛点。
- 依赖文档进行协议定义,前后端联调常被低级错误困扰(如字段名大小写错误,字段类型错误等)。
- 一些框架虽然实现了协议定义规范,但需要引入 Decorator 或第三方 IDL 语言。
- 一些框架虽然实现了类型校验,但无法支持 TypeScript 的高级类型,例如业务中常见的 Union Type:
// 用户信息
interface UserInfo {
// 来源渠道
from: { type: '老用户邀请', fromUserId: string }
| { type: '推广链接', url: string }
| { type: '直接进入' },
// 注册时间
createTime: Date
}
- JSON 支持的类型有限,例如不支持
ArrayBuffer,实现文件上传会非常麻烦。 - 请求和响应都是明文,破解门槛太低,字符串加密方式有限且强度不够。
- 等等…
我们无法找到一个能完美解决这些问题的现成框架,于是我们全新设计和创造了 TSRPC 。
概览
一个名为 Hello 的协议,从定义、实现到浏览器调用。
协议定义
直接使用 type 或 interface 定义协议,无需 Decorator 和第三方 IDL 语言。
export interface ReqHello {
name: string;
}
export interface ResHello {
reply: string;
}
服务端实现
运行时自动校验类型,请求参数一定类型安全。
import { ApiCall } from "tsrpc";
export async function ApiHello(call: ApiCall<ReqHello, ResHello>) {
call.succ({
reply: 'Hello, ' + call.req.name
});
}
客户端调用
跨项目复用协议定义,全程代码提示,不需要接口文档。
let ret = await client.callApi('Hello', {
name: 'World'
});
console.log(ret); // { isSucc: true, res: { reply: 'Hello, World' } }
特性
TSRPC 具有一些前所未有的强大特性,给您带来极致的开发体验。
- 🥤 原汁原味 TypeScript
- 直接基于 TypeScript
type和interface定义协议 - 无需额外注释,无需 Decorator,无需第三方 IDL 语言
- 直接基于 TypeScript
- 👓 自动类型检查
- 在编译时刻和运行时刻,自动进行输入输出的类型检查
- 总是类型安全,放心编写业务代码
- 💾 二进制序列化
- 比 JSON 更小的传输体积
- 比 JSON 更多的数据类型:如
Date,ArrayBuffer,Uint8Array等 - 方便地实现二进制加密
- 🔥 史上最强大的 TypeScript 序列化算法
- 无需任何注解,直接实现将 TypeScript 源码中的类型定义序列化
- 首个也是目前唯一支持 TypeScript 高级类型的二进制序列化算法,包括:
- ☎ 多协议
- 同时支持 HTTP / WebSocket
- 💻 多平台
- NodeJS / 浏览器 / App / 小程序
- ⚡️ 高性能
- 单核单进程 5000+ QPS 吞吐量(测试于 Macbook Air M1, 2020)
- 单元测试、压力测试、DevOps 方案齐备
- 经过数个千万用户级项目验证
兼容性
完全可以在 Server 端使用 TSRPC,同时兼容传统前端。
- 兼容 JSON 形式的 Restful API 调用
- 可自行使用
XMLHttpRequest、fetch或其它 AJAX 框架以 JSON 方式调用接口
- 可自行使用
- 兼容纯 JavaScript 的项目使用
- 可在纯 JavaScript 项目中使用 TSRPC Client,也能享受类型检查和序列化特性
- 兼容 Internet Explorer 10 浏览器
- 浏览器端兼容至 IE 10 ,Chrome 30
运行时类型检测的实现原理
众所周知,TypeScript 的类型检测仅发生在编译时刻,这是因为类型信息(如 type、interface)会在编译时刻被抹除。而 TSRPC 竟然能在运行时刻也检测这些被抹除的类型信息?
况且 TypeScript 编译器有大几 MB,而 TSRPC 才几十 KB……
其实,这是因为我们遵循 TypeScript 类型系统,独立实现了一套轻量的类型系统,可以在运行时完成类型检测,甚至是二进制序列化。它支持了绝大多数常用的 TypeScript 类型。
上手试试
使用 create-tsrpc-app 工具,可以快速创建 TSRPC 项目。
npx create-tsrpc-app@latest
创建过程是交互式的,在菜单上选择相应的配置,即可轻松创建包含前后端的 TSRPC 全栈应用项目。
如果你选择创建 HTTP 短连接服务,则会创建一个留言板的演示项目;如果选择 WebSocket 长连接服务,则会创建一个实时聊天室的演示项目。
参考资料
GitHub:https://github.com/k8w/tsrpc 中文文档:https://tsrpc.cn 视频教程:https://www.bilibili.com/video/BV1hM4y1u7B4
vue2.x都行?? 2.x ts支持的也不行啊,,
是自己操纵AST实现的吗,认可你的能力
但实现成这样,实在是太花里胡哨了,难以推广,大概率只能自用了。
TypeScript的类型系统好就好在它是可选的,运行时可以不按他来, 现在搞得这么严格,失去了灵活性,也就没意义了; 和GraghQL那套一样,兼具了js的动态缺点和java的静态缺点,完蛋。
@nomagick 运行时不按它来,如果是后端,会有多少安全的风险呀!
举个例子,你有一个用户表:
然后你实现了一个,更新用户信息接口 UpdateUser,请求参数为:
接口实现为:
在编译时刻,如果你在
UpdateUser时传了money,编译器会自动报错,OK 这当然没问题。但问题在于,这个类型,在运行时是不安全的,这就埋下了很大的安全隐患。
如果前端构造了一个恶意的请求呢:
由于在运行时缺少类型安全机制,则导致敏感字段没有被严格检查,用户可以直接更新自己的余额! 今天一个小坑,明天一个小坑,串联起来就是系统性的风险。
当然,你可以手动编写逻辑代码,去保证类型安全,但是:
是人总会出错的,所以更好的办法是,在运行时也有一套自动运行的机制,确保输入、输出的类型一定安全。 这可以从根本上,无代价的规避很多系统性的风险。 这也是 TSRPC 出现的根本意义。
@ganshiqingyuan Vue 2.x 可以的哈,不用 TS,纯 JS 项目也能用哈 (只是少了代码提示而已),但仍然有运行时类型检测。
@k8w 你这偷换概念了啊, 我可没说运行时类型检查或者说参数验证是没意义的;
你这个轮子,机巧是足够的,但有几个基本的点是没法被接受的,这和实现关系不大;
TypeScript的定位是开发辅助工具,尤其是类型系统,它的影响应当止于设计时,但你这个用法,使得TypeScript的类型系统对运行时产生了重大影响,但这部分代码,又不在项目的管理范围之内;即程序员不能清楚地意识到何时何地,以何种方式进行了参数验证,这是一个巨坑。
JS的动态性是优势而不是需要去除的缺点。
这还不要说,TypeScript的类型系统还会向前发展,持续变动。
像你回复的举例,是应用系统,是程序员需要解决的问题,而不是编译器和运行时,甚至通讯库需要解决的问题。
再说,参数验证就说参数验证,但你项目不叫RPC么, 这完全就是两码事啊。
这个RPC协议,不要为了二进制而二进制, 包括base128也好,压缩也好,都是为了提升性能,要么就是省点CPU时间要么省点流量,这些都是在超大规模实施的时候才会凸显出来的成本,但它们的代价却是需要在设计时支付的。
而且二进制协议也没必要静态决定结构,静态结构就会涉及到协议版本,这都是处理起来很棘手的问题。
如果说是TypeScript全栈开发的情况,大部分都是原型期的项目,没有必要为了性能,过早支付这些设计时成本。 额外支付着成本,却又享受不到显著的好处,同时又收集到一系列的掣肘,实在是本末倒置,毫无必要。
这就是为什么说,你这个轮子,难以推广。
@nomagick
只会在 RPC 的请求和响应时刻,针对输入输出做参数验证。
运行时类型检测并没有去除 JS 的动态性。
TypeScript 本身的类型系统就已经兼顾到了 JS 的动态性,例如
any类型,又例如:TS 类型系统本身的灵活性,让你可以在需要动态的时候动态,需要严谨的时候严谨。运行时类型检测同样也遵循了这一规则。
类型安全对于跨项目调用,确实是一个需要解决的问题。 例如 NestJS 通过 Decorator 来解决类似的问题。 是否应当在 RPC 框架内完成,这个可能见仁见智,对于二进制序列化的场景它会打包在框架内(例如 gRPC)。
您说的没错,没有必要为了二进制而二进制,当在游戏、金融等一些需要进行传输加密的场景会更有用。 所以序列化是一个可选项,它兼容 HTTP/JSON 调用。 但类型安全是刚需所以它是一个必选项。(否则现有的其它框架已经能很好的满足需要)
如果使用 ProtoBuf、Thrift 这样的第三方 IDL 静态类型语言定义,确实会引入相当的额外成本。 如果使用 Decorator 来注解类型,也会引入一些额外的成本。 但 TSRPC 不需要这些,只是需要通过 TS 源代码定义协议就可以(协议总是需要定义的)。 当然代价就是需要手动生成一下(也可以通过
watch来自动生成更新),至于协议版本的问题,也在生成协议的工具里自动处理,大部分时间不需要关心的。 例如 ProtoBuf 中需要关心协议、接口的 ID 序号。 TSRPC 是不需要关心的这些细节的,已经由工具自动完成了。大部分情况下,只需要像没有序列化那样关注新旧类型本身是否兼容,而不需要考虑编码细节。例如,旧协议:
新协议:
类似上面的例子,只要新旧协议在类型层面兼容,则编码层面是自动兼容的。 即便前端是旧版协议,也可以连接新版协议的后端服务正常使用。
一点点自动生成协议的成本,换来输入输出的类型安全,提升安全性并且可以放心的开发业务逻辑,这个交换是值得的。
非常感谢您的反馈,您的建议很具有代表性。 我们在推进的过程中也确实受到了很多类似的担忧和疑问,文档也不够详尽,对这些问题缺少足够的预见。 这也让我们思考,应该如何更好的包装概念,包括 “RPC” 是不是一个很适应的名词。
❤ 感恩的心 ❤
@nomagick 我理解它这个是一个RPC框架,自然就会包含系列化和运行时强类型检查,其他RPC框架也都是这么做的。而用TS来做IDL,恰恰就是为了屏蔽掉使用RPC框架的复杂性,无需额外编写服务定义代码
@k8w 我要对参数在类型以外进行额外的验证的话,通信层这步检查就更鸡肋了, 问题挺多的啊。 我觉得还是装饰器方案,并且专门做参数验证最靠谱
@leizongmin 哪里自然了老哥,不能gRPC是这么干的,别的RPC也得这么干啊, 毕竟单单RPC并不代表二进制协议和静态结构啊 对于RPC来讲,强类型不是它的特性而是它所受的的限制,是为了性能而做出的牺牲啊
@nomagick 你说得没错,无法反驳
没看明白 被调方? 远端怎么实现?
@netwjx 远端就是 NodeJS 上的一个异步函数。
@k8w 不用抽象到那个高度 -_-b
介绍应倾向于 技术方案评估者, 而不是技术使用者
因为新人多数不会主动探索不知名的东西, 只有资深的同学才会探索新的可能, 而且资深的同学在自己的工作领域是有决定权的
我想了解的是
上面这些都是基本功能, 还有什么吸引力特性/ 计划?
@netwjx
调用远端是通过 HTTP协议/WebSocket? 为什么这么选型, 会有什么优劣
类型检测 发现问题后, 如何做失败处理?
框架会自动返回类型错误信息给调用方(错误信息类似 TS 的类型错误信息),处理方式就跟处理普通网络错误、业务错误一致。
body部分的编码形式?
可选择为 二进制(application/octet-stream) 或 JSON(application/json)
压缩比率? 定量对比?
编解码性能测试? 定量对比?
如何在现有服务做小规模实验? 通过
npx create-tsrpc-app@latest创建出的工程,已经带了一些简单例子。https://github.com/k8w/tsrpc-examples 和文档里也有少量示例。
近期会整理一下我们的过往项目,公开一些较完整的项目例子(例如带权限验证的管理后台)
如何让现有服务平滑迁移?
就跟前端从
jQuery过渡到React和Vue一样,旧项目对于新的技术框架总是一个包袱。 新框架往往最早是从新项目中开始使用的。如果说衡量利弊后,真的要将旧的后台服务重构一份……
后端:
前端:
tsrpc-browser客户端,如此调用协议处有强类型代码提示,重构更容易集群如何容灾 防止单点故障?
这个部署层面的问题,与使用其它框架如 ExpressJS、EggJS 一致。
如果服务是无状态服务,那么就非常简单:
PM2来保证高可用有状态服务:
上面这些都是基本功能, 还有什么吸引力特性/ 计划?
主要吸引力特性其实是 3 点:
1. 直接基于 TS 源码定义协议
2. 运行时,自动校验参数类型
使用这些复杂 TS 类型,并自动校验类型,能带来什么收益? 可以参考这个主题分享:
https://www.bilibili.com/video/BV1hM4y1u7B4 ↑↑↑ 跳转到 35:00 :《以 MongoDB 下的 CRUD 为例,如何更高效的定义 TS 类型》 ↑↑↑
3. 二进制序列化
二进制序列化不只是为了减少包体,它比 JSON 支持更多类型,能满足更丰富的业务需要
Date、Uint8Array等类型。multipart/form-data的繁琐细节;但如果你只是像调用一个本地异步函数一样,传递一个Uint8Array、ArrayBuffer呢?例如:
别再用强弱类型了,用静态类型或者动态类型:
FYI:
作为 RPC,调用两端确定输入输出类型是基本,动态性也可以通过
any做平衡沿用 TS 的类型,可以不必维护两份类型描述,OP 这样的实现常见且优雅(王者荣誉ID?)。知不知道的问题通过文档和团队内贯宣,这个成本是大家都一样的,选什么方案都得贯宣