Protobuf to Typescript Definition

源码github地址在此,记得点星:brandonxiang/pb-to-typescript
playground页面欢迎试玩:https://brandonxiang.github.io/pb-to-typescript/example/

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文件,所以我们主要需要针对FieldEnumMethod这三种数据类型进行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文件,通过importexport解决作用域的问题。

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,又称虾皮,是一家腾讯投资的跨境电商平台。这里加班少,技术氛围好。如果想和我并肩作战一起学习,可以找我内推。邮箱weiping.xiang@shopee.com,非诚勿扰。