源碼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文件,所以我們主要需要針對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,又稱蝦皮,是一家騰訊投資的跨境電商平臺。這里加班少,技術氛圍好。如果想和我并肩作戰一起學習,可以找我內推。郵箱weiping.xiang@shopee.com,非誠勿擾。