如果有一份接口定义,前端和后端都能基于此生成相应端的代码,不仅能降低前后端沟通成本,而且还能提升研发效率。
字节内部的 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 应该就呼之欲出了,一起来看代码:
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 不满足你的业务场景,也可以自己实现一套类似的接口定义语言;接口定义作为前后端的约定,可以降低前后端的沟通成本;代码生成,可以提升前端代码的质量和研发效率。
如果本文对你有启发,欢迎点赞、关注、留言交流。
关于本文
最后不要忘了点赞呦!