字节都在用的代码自动生成
背景
如果有一份接口定义,前端和后端都能基于此生成相应端的代码,不仅能降低前后端沟通成本,而且还能提升研发效率。字节内部的 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({
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