Shopee出品的 Protobuf 转换 TypeScript神器
Why
《深入 ProtoBuf - 简介》一文介绍了Protobuf是一种有别于JSON 或 XML文本传输的新型二进制数据格式,具有安全性好,传输效率高的特点。
Protobuf文件(简称pb文件)在go语言的微服务中被广泛使用,实际开发的情景中,后端同事会丢给我们一个pb文件,也就是接口定义的参数,让前端同事按照结构体传参。这份pb往往成为前后端沟通的桥梁,所以是非常重要的。
定义一个最简单的结构体,通常如下,message 关键字后跟上消息名称,结构体内部则是字段和其数据类型。
message student {
string name = 1;
int32 age=2
}
作为前端,你可能会想起js对象或者typescript的类型定义文件。ts类型定义文件在接口联调的过程中能够提高开发效率,有效缩减联调时间,提高代码质量。回想一下,你是不是有抱怨过后端给你的字段,在没有数据的时候,有可能是undefined,null,空数组,空字符串,这些都不算恶劣,还有"null"
的字符串。这样直接让你的代码里面充满了各种奇怪的判断逻辑,这种“脏代码”直接降低你的代码可读性,严重的还会带给你bug。
var student = {
name: 'Peter',
age: 18
}
interface student {
name: string;
age: number;
}
这时候聪明的同学就会想到,能不能让protobuf直接转换成为typescript的类型定义文件。前端同事在联调的时候直接对请求函数进行类型限制,也可以根据请求返回参数的数据结构mock假数据。这种想法直接形成了有效的工作流程,给页面交互联调提高效率。
How
怎么去实现protobuf到typescript的转换,首先受到开源工具geotho/protobuf-to-typescript的启发,但是它的源码非常简单,原理不过是字符串替换,非常容易出现报错的情况。所以我需要一个标准的解释器能够精确将protobuf文件转化为抽象语法树(AST)。这时候,我阅读了@grpc/proto-loader的源码,它依赖于开源库protobuf.js。但是protobuf.js所提供的命令行工具(命令如下)不能满足我们的需求,它的转换结果更多是服务于nodejs,应用于服务端工作。
pbjs -t static-module file1.proto file2.proto | pbts -o bundle.d.ts -
现在我们的目标很明确,如何将protobuf的内容转换为我们想要的typescript类型文件。首先,引入protobufjs,使用 protobuf.parse(source, { keepCase: true })
将pb进行解析(keepCase是保护参数大小写的选项),得到的是官方提供的protobuf的js对象,可以直接针对它进行解析,或者将它进行json序列化toJSON()
。
Type (T) | Extends | Type-specific properties |
---|---|---|
ReflectionObject | options | |
Namespace | ReflectionObject | nested |
Root | Namespace | nested |
Type | Namespace | fields |
Enum | ReflectionObject | values |
Field | ReflectionObject | rule, type, id |
MapField | Field | keyType |
OneOf | ReflectionObject | oneof (array of field names) |
Service | Namespace | methods |
Method | ReflectionObject | type, requestType, responseType, requestStream, responseStream |
以上是protobuf.js解析出来的数据类型,因为目标主要是面向前端使用的typescript文件,所以我们主要需要针对Field
,Enum
,Method
这三种数据类型进行typescript转换。
Field
Field主要指的就是protobuf中的message,它可以转换为ts里面的interface。其中PB类型和TS类型存在一个转换关系。这里存在一个问题即是int64超出js的number类型,如果强制转换则会丢失精度,默认我们将它转换为string。
repeated
关键字则是数组的一个表现。
Field type | Expected JS type (create, encode) |
---|---|
s-/u-/int32 s-/fixed32 | number |
s-/u-/int64 s-/fixed64 | number或者string |
float double | number |
bool | boolean |
string | string |
bytes | string |
Enum
枚举是Protobuf类型也是Typescript类型,只需做简单转换即可,但是Protobuf中的Enum有可能存在于message当中,而此时,它并非接口参数实现,需要注意。
Method
rpc服务里面的一个方法即是一个method,一般里面包含两个message,分别是入参和出参。
syntax = "proto3";
service MyService {
rpc MyMethod (MyRequest) returns (MyResponse);
}
message MyRequest {
string path = 1;
}
message MyResponse {
int32 status = 1;
}
同样,我们需要把Method转换为typescript中的interface
,如下,这是因为请求一般返回的是Promise对象。
interface MyMethod {
(params: MyRequest): Promise<MyResponse>;
}
类型文件可以应用于你自行封装的request
方法,此时代码的入参出参则被有效限制。
const myMethod: MyMethod = (params) => {
return request('/my_method', params)
}
通过以上说的这些转换,已经可以满足对请求函数的入参出参的typescript类型限制,有效地提高和后台开发这联调的效率。
What
功能实现后,可能有人向我推荐easy-mock。首先,easy-mock暂时没支持protobuf。它更多是一个大而全的系统,功能很齐全,但是同样也带来一些问题----接口维护成本。“mock平台需要谁来维护?”这个问题成为最大的阻碍。接口参数应该是由后端工程师定义,但是他们改了参数往往忘了维护在mock平台上。如果交给前端维护,有时候难免会导致通知不到位,沟通成本上升的情况。因此,我更崇尚于“小而美”的工具。
pb-to-typescript它即可以是运行在nodejs上,也可以跑在浏览器上。更简单的是你可以直接打开页面https://brandonxiang.github.io/pb-to-typescript/example,直接将protobuf文件复制进行转换,右方输出的则是类型定义文件。这里可以输出d.ts
或typescript文件。
d.ts
是类型文件,它的优点在于自动引入。但是重复的名称会带来变量污染,生产的类型文件是没法管控重名的情况,开发者需要自行增加namespace。我个人更推荐ts文件,通过import
或export
解决作用域的问题。
Mock
有同学肯定还是会烦恼于mock的问题。正因为我们是从“小而美”的角度触发,我们只需要mock返回参数即可。
在example页面中同样提供了这个功能,将proto文件整个复制进去,点击需要mock的方法名,mock的结果会根据typescript的类型进行简单的mock处理,基本满足页面UI的编写。以上面栗子为例,mock数据可以直接通过Promise.resolve
填入request方法中,这样保证了类型检查通过,而且返回了一个标准的Promise对象。
const myMethod: MyMethod = (params) => {
//return request('/my_method', params)
return Promise.resolve({
"status": 10
});
}
Conclusion
pb-to-typescript解决的问题是接口联调中的一个痛点,就是接口还没好,后端同事只提供你一个proto文件。前端同事可以通过proto的内容转换为ts的类型定义并简单mock返回数据,将UI和交互工作前置,提高工作效率,早点下班。
祝大家新的一年不用加班。
题外话
Shopee,又称虾皮,是一家腾讯投资的跨境电商平台。这里鼓励提高效率而非加班,技术氛围好。如果想和我并肩作战一起学习,可以找我内推。邮箱
[email protected],非诚勿扰。