字節都在用的代碼自動生成

背景

如果有一份接口定義,前端和後端都能基於此生成相應端的代碼,不僅能降低前後端溝通成本,而且還能提升研發效率。

字節內部的 RPC 定義主要基於 thrift 實現,thrift 定義了數據結構和函數,那麼是否可以用來作爲接口定義提供給前端使用呢?如果可以作爲接口定義,是不是也可以通過接口定義自動生成請求接口的代碼呢?答案是肯定的,字節內部已經衍生出了多個基於 thrift 的代碼生成工具,本篇文章主要介紹如何通過 thrift 生成前端接口調用的代碼。

接口定義

接口定義,顧名思義就是用來定義接口的語言,由於字節內部廣泛使用的 thrift 基本上滿足接口定義的要求,所以我們不妨直接把 thrift 當成接口定義。

thrift 是一種跨語言的遠程過程調用 (RPC) 框架,如果你對 Typescript 比較熟悉的話,那它的結構看起來應該很簡單,看個例子:

namespace go namesapce
// 請求的結構體
struct GetRandomRequest {
 1: optional i32 min,
 2: optional i32 max,
 3: optional string extra
}
// 響應的結構體
struct GetRandomResponse {
 1: optional i64 random_num
}
// 定義服務
service RandomService {
 GetRandomResponse GetRandom (1: GetRandomRequest req)
}

示例中的 service 可以看成是一組函數,每個函數可以看成是一個接口。我們都知道,對於 restful 接口,還需要定義接口路徑(比如 /getUserInfo)和參數(query 參數、body 參數等),我們可以通過 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 用來指定參數的位置,query 表示是 query 參數,body 表示 body 參數;api.get="/api/get-random" 表示接口路徑是 /api/get-random,請求方法是 GET;

生成 Typescript

上面我們已經有了接口定義,那麼對應的 Typescript 應該就呼之欲出了,一起來看代碼:

interface GetRandomRequest {
  min: number;
  max: number;
  extra: string;
}
interface GetRandomResponse {
  random_num: number;
}
async function GetRandom(req: GetRandomRequest): Promise<GetRandomResponse> {
  return request<GetRandomResponse>({
    url: '/api/get-random',
    method: 'GET',
    query: {
      min: req.min,
      max: req.max,
    },
    body: {
      extra: req.extra,
    },
  });
}

生成 Typescript 後,我們無需關心生成的代碼長什麼樣,直接調用 GetRandom 即可。

架構設計

要實現基於 thrift 生成代碼,最核心的架構如下:

因爲 thrift 的內容我們不能直接拿來用,需要轉化成中間代碼(IR),這裏的中間代碼通常是 json、AST 或者自定義的 DSL。如果中間代碼是 json,可能的結構如下:

{
  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: [],
  },
};

爲了保持架構的開放性,我們在覈心鏈路上插入了 PrePlugin 和 PostPlugin,其中 PrePlugin 決定了 thrift 如何轉化成 IR,PostPlugin 決定 IR 如何生成目標代碼。

這裏之所以是「目標代碼」而不是「Typescript 代碼」,是因爲我希望不同的 PostPlugin 可以產生不同的目標代碼,比如可以通過 TSPostPlugin 生成 Typescript 代碼,通過 GoPostPlugin 生成 go 語言的代碼。

總結

代碼生成這塊的內容還有很多可以探索的地方,比如如何解析 thrift?是找第三方功能生成 AST 還是通過 pegjs 解析成自定義的 DSL?多文件聯編如何處理、字段名 case 如何轉換、運行時類型校驗、生成的代碼如何與 useRequest 或 ReactQuery 集成等。

thrift 其實可以看成接口定義的具體實現,如果 thrift 不滿足你的業務場景,也可以自己實現一套類似的接口定義語言;接口定義作爲前後端的約定,可以降低前後端的溝通成本;代碼生成,可以提升前端代碼的質量和研發效率。

關於本文

作者:探險家火焱

https://juejin.cn/post/7220054775298359351

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/qAHBvdBlypMKOuV_3h4Mag