经常开发表格,是不是已经被手写 Ant-Design Table 的 Columns 整烦了?
尤其是 ToB 项目,表格经常动不动就几十列。每次照着后端给的接口文档一个个配置,太头疼了,主要是有时还会粘错就尴尬了。
那有没有办法能自动生成 columns 配置呢?
当然可以。
目前后端的接口文档一般是使用 Swagger 来生成的,Swagger 是基于 OpenAPI 规范的一种实现。(OpenAPI
规范是一种描述 RESTful API 的语言无关的格式,它允许开发者定义 API 的操作、输入和输出参数、错误响应等信息,并提供了一种规范的方式来描述和交互 API。)
那么我们只需要解析 Swagger 的配置就可以反向生成前端代码。
接下来我们就写个 CLI 工具来生成 Table Columns。
平常我们实现一个 CLI 工具一般都是用 Node,今天我们搞点不一样的,用 Rust。
开始咯
swagger.json
打开后端用 swagger 生成的接口文档中的一个接口,一般是下面这样的,可以看到其 json 配置文件,如下图:
swagger: 2.0
表明了这个文档使用的 swagger 版本,不同版本 json 配置结构会不同。
paths
这里 key 是接口地址。
可以看到当前接口是“/api/operations/cate/rhythmTableList”。
顺着往下看,“post.responses.200.schema.originalRef”,这就是我们要找的,这个接口对应的返回值定义。
definitions
拿到上面的返回值定义,就可以在“definitions”里找到对应的值。
这里是“definitions.ResponseResult«List«CateInsightRhythmListVO»».properties.data.items.originalRef”
通过他就可找到返回的实体类定义CateInsightRhythmListVO
CateInsightRhythmListVO
这里就是我们生成 Table Columns 需要的字段定义了。
CLI
接下来制作命令行工具
起初我使用的是commander-rust,感觉用起来更符合直觉,全程采用 macros 定义即可。
但到发布的时候才发现,Rust 依赖必须有一个确定的版本,commander-rust 目前使用的是分支解析。。。
最后还是换了clap
clap 的定义就要繁琐些,如下:
#[derive(Parser)]
#[command(author, version)]
#[command(about = "swagger_to - Generate code based on swagger.json")]
struct Cli {
#[command(subcommand)]
command: Option,
}
#[derive(Subcommand)]
enum Commands {
/// Generate table columns for ant-design
Columns(JSON),
}
#[derive(Args)]
struct JSON {
/// path/to/swagger.json
path: Option,
}
复制代码
这里使用#[command(subcommand)]
和#[derive(Subcommand)]
来定义 columns 子命令
使用#[derive(Args)]
定义了 path 参数,用来让用户输入 swagger.json 的路径
实现 columns 子命令
columns 命令实现的工作主要是下面几步:
读取用户输入的 swagger.json
解析 swager.json
生成 ant-design table columns
生成对应 Typescript 类型定义
读取用户输入的 swagger.json
这里用到了一个 crate,serde_json
, 他可以将 swagger.json 转换为对象。
let file = File::open(json).expect("File should open");
let swagger_json: Value = serde_json::from_reader(file).expect("File should be proper JSON");
复制代码
解析 swager.json
有了 swagger_json 对象,我们就可以按照 OpenAPI 的结构来解析它。
/// openapi.rs
pub fn parse_openapi(swagger_json: Value) -> Vec {
let paths = swagger_json["paths"].as_object().unwrap();
let apis = paths
.iter()
.map(|(path, path_value)| {
let post = path_value["post"].as_object().unwrap();
let responses = post["responses"].as_object().unwrap();
let response = responses["200"].as_object().unwrap();
let schema = response["schema"].as_object().unwrap();
let original_ref = schema["originalRef"].as_str().unwrap();
let data = swagger_json["definitions"][original_ref]["properties"]["data"]
.as_object()
.unwrap();
let items = data["items"].as_object().unwrap();
let original_ref = items["originalRef"].as_str().unwrap();
let properties = swagger_json["definitions"][original_ref]["properties"]
.as_object()
.unwrap();
let response = properties
.iter()
.map(|(key, value)| {
let data_type = value["type"].as_str().unwrap();
let description = value["description"].as_str().unwrap();
ResponseDataItem {
key: key.to_string(),
data_type: data_type.to_string(),
description: description.to_string(),
}
})
.collect();
Api {
path: path.to_string(),
model_name: original_ref.to_string(),
response: response,
}
})
.collect();
return apis;
}
复制代码
这里我写了一个parse_openapi()
方法,用来将 swagger.json 解析成下面这种形式:
[
{
path: 'xxx',
model_name: 'xxx',
response: [
{
key: '字段key',
data_type: 'number',
description: '字段名'
}
]
}
]
复制代码
对应的 Rust 结构定义是这样的:
pub struct ResponseDataItem {
pub key: String,
pub data_type: String,
pub description: String,
}
pub struct Api {
pub path: String,
pub model_name: String,
pub response: Vec<ResponseDataItem>,
}
复制代码
生成 ant-design table columns
有了 OpenAPI 对象就可以生成 Table Column 了,这里写了个generate_columns()
方法:
/// generator.rs
pub fn generate_columns(apis: &mut Vec) -> String {
let mut output_text = String::new();
output_text.push_str("import type { ColumnsType } from 'antd'\n");
output_text.push_str("import type * as Types from './types'\n");
output_text.push_str("import * as utils from './utils'\n\n");
for api in apis {
let api_name = api.path.split('/').last().unwrap();
output_text.push_str(
&format!(
"export const {}Columns: ColumnsType = [\n",
api_name,
api.model_name
)
);
for data_item in api.response.clone() {
output_text.push_str(
&format!(
" {{\n title: '{}',\n key: '{}',\n dataIndex: '{}',\n {}\n }},\n",
data_item.description,
data_item.key,
data_item.key,
get_column_render(data_item.clone())
)
);
}
output_text.push_str("]\n");
}
return output_text;
}
复制代码
这里主要就是采用字符串模版的形式,将 OpenAPI 对象遍历生成 ts 代码。
生成对应 Typescript 类型定义
Table Columns 的类型使用generate_types()
来生成,原理和生成 columns 一样,采用字符串模版:
/// generator.rs
pub fn generate_types(apis: &mut Vec) -> String {
let mut output_text = String::new();
for api in apis {
let api_name = api.path.split('/').last().unwrap();
output_text.push_str(
&format!(
"export type {} = {{\n",
Some(api.model_name.clone()).unwrap_or(api_name.to_string())
)
);
for data_item in api.response.clone() {
output_text.push_str(&format!(" {}: {},\n", data_item.key, data_item.data_type));
}
output_text.push_str("}\n\n");
}
return output_text;
}
复制代码
然后我们在 main.rs 中分别调用上面这两个方法即可
/// main.rs
let mut apis = parse_openapi(swagger_json);
let columns = generator::generate_columns(&mut apis);
let mut columns_ts = File::create("columns.ts").unwrap();
write!(columns_ts, "{}", columns).expect("Failed to write to output file");
let types = generator::generate_types(&mut apis);
let mut types_ts = File::create("types.ts").unwrap();
write!(types_ts, "{}", types).expect("Failed to write to output file");
复制代码
对于 columns 和 types 分别生成两个文件,columns.ts 和 types.ts。
!这里有一点需要注意
当时开发的时候对 Rust 理解不是很深,起初拿到 parse_openapi 返回的 apis 我是直接分别传给 generate_columns(apis)和 generate_types(apis)的。但编译的时候报错了:
这对于 js 很常见的操作竟然在 Rust 中报错了。原来 Rust 所谓不依赖运行时垃圾回收而管理变量分配引用的特点就体现在这里。
我就又回去读了遍 Rust 教程里的“引用和借用”那篇,算是搞懂了。这里实际上是 Rust 变量所有权、引用和借用的问题。读完了自然你也懂了。
看看效果
安装
使用
swagger_to columns path/to/swagger.json
复制代码
会在 swagger.json 所在同级目录生成三个文件:
columns.ts
ant-design table columns 的定义
types.ts
columns 对应的类型定义
utils.ts
column 中 render 对 number 类型的字段添加了格式化工具
Enjoy
作者:京东零售 于弘达
来源:京东云开发者社区
评论