我的原始需求是这样的,写了一个nodejs命令行工具,工具的功能是,拉取后端接口导出的 postman.json 接口内容,通过接口中的入参出参数据,生成入参出参的TS类型声明文件,达到在ts业务代码中可以校验接口入参和出参类型的目的,
postman.json的大致格式如下
{
"item": [
{
"item": [
{
"item": [],
"name": "WechatMiniAppNatureController",
"description": "WechatMiniAppNatureController"
}
],
"name": "wpe-miniwe-recycle-srv-api",
"description": "exported at 2024-09-09 15:17:40"
},
{
"item": [
{
"item": [
{
"request": {
"method": "POST",
"description": "",
"header": [
{
"key": "Content-Type",
"value": "application/json",
"type": "text",
"description": ""
}
],
"body": {
"mode": "raw",
"options": {
"raw": {
"language": "json"
}
},
"raw": "{\n \"Id\": 0\n}"
},
"url": {
"path": [
"wechat",
"rec",
"v1",
"nature",
"apply",
"taxRebateInfo"
],
"query": [],
"host": "{{wpe-miniwe-recycle-srv-web}}",
"raw": "{{wpe-miniwe-recycle-srv-web}}/wechat/rec/v1/nature/apply/taxRebateInfo"
}
},
"response": [
{
"name": "退税申请详情接口-Example",
"originalRequest": {
"method": "POST",
"description": "",
"header": [
{
"key": "Content-Type",
"value": "application/json",
"type": "text",
"description": ""
}
],
"body": {
"mode": "raw",
"options": {
"raw": {
"language": "json"
}
},
"raw": "{\n \"Id\": 0\n}"
},
"url": {
"path": [
"wechat",
"rec",
"v1",
"nature",
"apply",
"taxRebateInfo"
],
"query": [],
"host": "{{wpe-miniwe-recycle-srv-web}}",
"raw": "{{wpe-miniwe-recycle-srv-web}}/wechat/rec/v1/nature/apply/taxRebateInfo"
}
},
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"name": "date",
"key": "date",
"value": "周一, 09 9月 202415:17:40 GMT",
"description": "The date and time that the message was sent"
}
],
"body": "{\n \"Response\": {\n \"RequestId\": \"\",\n \"Error\": {\n \"Code\": \"\",\n \"Message\": \"\"\n },\n \"Data\": {\n \"id\": 0,\n \"taxRebateNumber\": \"\", //退税申请序号\n \"natureRecordId\": \"\", //自然人档案号\n \"registryNumber\": \"\", //登记序号\n \"collectionItemCode\": \"\", //征收项目代码\n \"collectionItemName\": \"\", //征收项目名称\n \"collectionCode\": \"\", //征收品目代码\n \"taxBureauCode\": \"\", //主管税务所科分局代码\n \"taxAuthorityCode\": \"\", //主管税务机关代码\n \"taxAmountAuthorityCode\": \"\", //税款所属税务机关代码\n \"taxAmountAuthorityName\": \"\", //税款所属税务机关名称\n \"streetTownCode\": \"\", //街道乡镇代码\n \"taxAmount\": 0.0, //应退税额\n \"taxRate\": 0.0, //税率\n \"taxUuid\": \"\", //税票UUID\n \"eleTaxNumber\": \"\", //电子税票号码\n \"taxStartTime\": \"\", //税款所属期起\n \"taxEndTime\": \"\", //税款所属期止\n \"applyStatus\": 0,\n \"rebateStatus\": \"\",\n \"rebateStatusCn\": \"\",\n \"taxStatus\": 0,\n \"rejectReason\": \"\",\n \"bankCardNo\": \"\",\n \"createTime\": \"\"\n }\n }\n}"
}
],
"name": "退税申请详情接口"
}
],
"name": "WechatMiniAppTaxController",
"description": "WechatMiniAppTaxController"
},
{
"item": [
{
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json",
"type": "text",
"description": ""
}
],
"body": {
"mode": "raw",
"options": {
"raw": {
"language": "json"
}
},
"raw": "{\n \"QrId\": \"\"\n}"
},
"url": {
"path": [
"wechat",
"rec",
"v1",
"operator",
"inviteConfirm"
],
"query": [],
"host": "{{wpe-miniwe-recycle-srv-web}}",
"raw": "{{wpe-miniwe-recycle-srv-web}}/wechat/rec/v1/operator/inviteConfirm"
}
},
"response": [
{
"name": "operatorInviteConfirm-Example",
"originalRequest": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json",
"type": "text",
"description": ""
}
],
"body": {
"mode": "raw",
"options": {
"raw": {
"language": "json"
}
},
"raw": "{\n \"QrId\": \"\"\n}"
},
"url": {
"path": [
"wechat",
"rec",
"v1",
"operator",
"inviteConfirm"
],
"query": [],
"host": "{{wpe-miniwe-recycle-srv-web}}",
"raw": "{{wpe-miniwe-recycle-srv-web}}/wechat/rec/v1/operator/inviteConfirm"
}
},
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"name": "date",
"key": "date",
"value": "周一, 09 9月 202415:17:40 GMT",
"description": "The date and time that the message was sent"
}
],
"body": "{\n \"Response\": {\n \"RequestId\": \"\",\n \"Error\": {\n \"Code\": \"\",\n \"Message\": \"\"\n },\n \"Data\": false\n }\n}"
}
],
"name": "operatorInviteConfirm"
}
],
"name": "WechatMiniAppOperatorController",
"description": "WechatMiniAppOperatorController"
}
],
"name": "wpe-miniwe-recycle-srv-web",
"description": "exported at 2024-09-09 15:17:40"
}
],
"info": {
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"name": "wpe-miniwe-recycle-srv-20240909151740",
"description": "exported at 2024-09-09 15:17:40"
}
}
上面的接口文档中 接口地址为 /wechat/rec/v1/nature/apply/taxRebateInfo 的接口返回体,经过数据反解析后输出的Ts类型声明代码为
export type TWechatRecV1NatureApplyTaxRebateInfoRess = {
Code: number;
Error: {
Code: number;
Message: string;
},
RequestId: string;
Data: {
id: number;
taxRebateNumber: string;
natureRecordId: string;
// ... 其他属性
}
};
上面的代码中,除了Data内的数据,其他的都是所有接口相同的属性内容,所以我需要处理生成的ts文件,只保留 Data 的类型描述。
也尝试过好几种方案,主要有
- 从源代码处理,在postman文件的response -> body 代码中处理完内容再去做反解析
- 在生成的文件中通过字符串匹配去查找
上面的方法中,主要都存在一个问题,就是postman源代码中,body包含了很多杂七杂八的内容,比如换行符,注释,还有转译字符,加上body内容的层级是不固定,这为我们做正则匹配带来了很多麻烦,所以兜兜转转想到了最终的解决方案,那就是使用AST的方法来处理生成的TS代码,这样在操作AST的过程中,babel 会帮我们处理好注释和其他不相关的内容。先show一下最终的代码
// 要先安装下面的依赖
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
/**
* 取出声明代码中指定key的interface代码
* 使用babel的抽象语法树转换,处理,生成代码
* @param typeCode ts代码
* @param rootName 根类型名
* @param keyName 属性名
* @returns 取出的代码
*/
export const subInterfaceByKey = (typeCode: string, rootName: string, keyName: string): string => {
// 从ts类型声明代码中取出指定key的interface代码,现将代码转换成ast
const ast = parser.parse(typeCode, { sourceType: 'module', plugins: ['typescript'] });
let titleType = 'any';
// 从ast中找到指定key的接口,生成代码返回。
traverse(ast, {
TSInterfaceDeclaration(path) {
if (path.node.id.name === rootName) {
const properties = path.node.body.body;
const titleProperty = properties.find(prop => prop.key.name === keyName);
// console.log(titleProperty.typeAnnotation);
if (titleProperty) {
titleType = generate(titleProperty.typeAnnotation).code;
}
}
},
});
if (titleType !== 'any') {
// 去掉类型前面的冒号和空格
return titleType.replace(/:\s/, '');
}
return titleType;
};
上面的代码中,通过babel parser将要处理的代码转换成ast,然后通过 traverse 的 TSInterfaceDeclaration 勾子来处理ts interface 类型的代码,最后将处理后的代码生成好赋值给变量返回,
处理后的声明文件内容就成了下面这个样子了
export type TWechatRecV1NatureApplyTaxRebateInfoRess = {
id: number;
taxRebateNumber: string;
natureRecordId: string;
// 。。。其他属性
};
至于为什么要用type而不是interface,原因是,Data 数据有可能不是一个对象,而是基础数据类型或数组,比如
export type TWechatRecV1JodCancelCmbcBillRess = boolean;
经过这次经验,我想以后再遇到这种代码处理的需求,我不会第一时间想到通过正则来处理,而是通过AST来处理了,即安全又逻辑清晰,
评论 (0)