字節(jié)都在用的代碼自動生成
背景
如果有一份接口定義,前端和后端都能基于此生成相應(yīng)端的代碼,不僅能降低前后端溝通成本,而且還能提升研發(fā)效率。字節(jié)內(nèi)部的 RPC 定義主要基于 thrift 實(shí)現(xiàn),thrift 定義了數(shù)據(jù)結(jié)構(gòu)和函數(shù),那么是否可以用來作為接口定義提供給前端使用呢?如果可以作為接口定義,是不是也可以通過接口定義自動生成請求接口的代碼呢?答案是肯定的,字節(jié)內(nèi)部已經(jīng)衍生出了多個(gè)基于 thrift 的代碼生成工具,本篇文章主要介紹如何通過 thrift 生成前端接口調(diào)用的代碼。
接口定義
接口定義,顧名思義就是用來定義接口的語言,由于字節(jié)內(nèi)部廣泛使用的 thrift 基本上滿足接口定義的要求,所以我們不妨直接把 thrift 當(dāng)成接口定義。
thrift 是一種跨語言的遠(yuǎn)程過程調(diào)用 (RPC) 框架,如果你對 Typescript 比較熟悉的話,那它的結(jié)構(gòu)看起來應(yīng)該很簡單,看個(gè)例子:
namespace?go?namesapce
//?請求的結(jié)構(gòu)體
struct?GetRandomRequest?{
?1:?optional?i32?min,
?2:?optional?i32?max,
?3:?optional?string?extra
}
//?響應(yīng)的結(jié)構(gòu)體
struct?GetRandomResponse?{
?1:?optional?i64?random_num
}
//?定義服務(wù)
service?RandomService?{
?GetRandomResponse?GetRandom?(1:?GetRandomRequest?req)
}
示例中的 service 可以看成是一組函數(shù),每個(gè)函數(shù)可以看成是一個(gè)接口。我們都知道,對于 restful 接口,還需要定義接口路徑(比如 /getUserInfo)和參數(shù)(query 參數(shù)、body 參數(shù)等),我們可以通過 thrift 注解來表示這些附加信息。
namespace go namesapce
struct GetRandomRequest {
1: optional i32 min (api.source = "query"),
2: optional i32 max (api.source = "query"),
3: optional string extra (api.source = "body"),
}
struct GetRandomResponse {
1: optional i64 random_num,
}
// Service
service RandomService {
GetRandomResponse GetRandom (1: GetRandomRequest req) (api.get = "/api/get-random"),
}
api.source 用來指定參數(shù)的位置,query 表示是 query 參數(shù),body 表示 body 參數(shù);api.get="/api/get-random" 表示接口路徑是 /api/get-random,請求方法是 GET;
生成 Typescript
上面我們已經(jīng)有了接口定義,那么對應(yīng)的 Typescript 應(yīng)該就呼之欲出了,一起來看代碼:
interface?GetRandomRequest?{
??min:?number;
??max:?number;
??extra:?string;
}
interface?GetRandomResponse?{
??random_num:?number;
}
async?function?GetRandom(req:?GetRandomRequest):?Promise<GetRandomResponse>?{
??return?request({
????url:?'/api/get-random',
????method:?'GET',
????query:?{
??????min:?req.min,
??????max:?req.max,
????},
????body:?{
??????extra:?req.extra,
????},
??});
}
生成 Typescript 后,我們無需關(guān)心生成的代碼長什么樣,直接調(diào)用 GetRandom 即可。
架構(gòu)設(shè)計(jì)
要實(shí)現(xiàn)基于 thrift 生成代碼,最核心的架構(gòu)如下:
因?yàn)?thrift 的內(nèi)容我們不能直接拿來用,需要轉(zhuǎn)化成中間代碼(IR),這里的中間代碼通常是 json、AST 或者自定義的 DSL。如果中間代碼是 json,可能的結(jié)構(gòu)如下:
{
??name:?'GetRandom',
??method:?'get',
??path:?'/api/get-random',
??req_schema:?{
????query_params:?[
??????{
????????name:?'min',
????????type:?'int',
????????optional:?true,
??????},
??????{
????????name:?'max',
????????type:?'int',
????????optional:?true,
??????},
????],
????body_params:?[
??????{
????????name:?'extra',
????????type:?'string',
????????optional:?true,
??????},
????],
????header_params:?[],
??},
??resp_schema:?{
????header_params:?[],
????body_params:?[],
??},
};
為了保持架構(gòu)的開放性,我們在核心鏈路上插入了 PrePlugin 和 PostPlugin,其中 PrePlugin 決定了 thrift 如何轉(zhuǎn)化成 IR,PostPlugin 決定 IR 如何生成目標(biāo)代碼。
這里之所以是「目標(biāo)代碼」而不是「Typescript 代碼」,是因?yàn)槲蚁M煌?PostPlugin 可以產(chǎn)生不同的目標(biāo)代碼,比如可以通過 TSPostPlugin 生成 Typescript 代碼,通過 GoPostPlugin 生成 go 語言的代碼。
總結(jié)
代碼生成這塊的內(nèi)容還有很多可以探索的地方,比如如何解析 thrift?是找第三方功能生成 AST 還是通過 pegjs 解析成自定義的 DSL?多文件聯(lián)編如何處理、字段名 case 如何轉(zhuǎn)換、運(yùn)行時(shí)類型校驗(yàn)、生成的代碼如何與 useRequest 或 ReactQuery 集成等。
thrift 其實(shí)可以看成接口定義的具體實(shí)現(xiàn),如果 thrift 不滿足你的業(yè)務(wù)場景,也可以自己實(shí)現(xiàn)一套類似的接口定義語言;接口定義作為前后端的約定,可以降低前后端的溝通成本;代碼生成,可以提升前端代碼的質(zhì)量和研發(fā)效率。
如果本文對你有啟發(fā),歡迎點(diǎn)贊、關(guān)注、留言交流。
作者:探險(xiǎn)家火焱https://juejin.cn/post/7220054775298359351