我的原始需求是这样的,写了一个nodejs命令行工具,工具的功能是,拉取后端接口导出的 postman.json 接口内容,通过接口中的入参出参数据,生成入参出参的TS类型声明文件,达到在ts业务代码中可以校验接口入参和出参类型的目的,
postman.json的大致格式如下
代码语言:javascript复制{
"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": 0n}"
},
"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": 0n}"
},
"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": "", //税票UUIDn "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": falsen }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类型声明代码为
代码语言:javascript复制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一下最终的代码
代码语言:javascript复制// 要先安装下面的依赖
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 类型的代码,最后将处理后的代码生成好赋值给变量返回,
处理后的声明文件内容就成了下面这个样子了
代码语言:javascript复制export type TWechatRecV1NatureApplyTaxRebateInfoRess = {
id: number;
taxRebateNumber: string;
natureRecordId: string;
// 。。。其他属性
};
至于为什么要用type而不是interface,原因是,Data 数据有可能不是一个对象,而是基础数据类型或数组,比如
代码语言:javascript复制export type TWechatRecV1JodCancelCmbcBillRess = boolean;
经过这次经验,我想以后再遇到这种代码处理的需求,我不会第一时间想到通过正则来处理,而是通过AST来处理了,即安全又逻辑清晰,