自动生成 API 类型与请求函数:解放前端开发者的重复劳动
在前端开发中,与后端 API 交互是日常工作的重要组成部分,但手动处理接口参数往往带来诸多困扰:盯着 Swagger 文档逐个敲写 TS 类型、后端字段变更时需要同步修改多个地方、参数繁多时手写请求函数容易出错…… 这些重复劳动不仅低效,还可能引入人为错误。
本文将介绍一种基于 Swagger 文档的自动化方案,通过脚本一键生成 TS 类型定义和请求函数,彻底解决上述问题,让前端开发者专注于业务逻辑而非重复编码。
方案概述
我们将实现一个自动化脚本,通过以下步骤解决 API 交互的痛点:
- 从 Swagger 文档获取 API 元数据(接口地址、参数、返回值等)
- 自动转换为 TypeScript 类型定义
- 生成标准化的请求函数
- 支持多服务架构,一键更新所有 API
- 通过版本控制清晰追踪接口变更
整个流程在后端接口更新后只需执行一条命令,即可完成所有前端接口层的同步工作。
实现步骤
1. 配置执行命令
在package.json中添加脚本命令,支持单独或批量拉取多个微服务的 API 文档:
{
"scripts": {
"pull-api": "npm run pull-api-data-gov && npm run pull-api-data-development && npm run pull-api-system-center && npm run pull-api-data-service && npm run pull-api-main-data",
"pull-api-data-gov": "node swagger.js data-gov",
"pull-api-data-development": "node swagger.js data-development",
"pull-api-system-center": "node swagger.js system-center",
"pull-api-data-service": "node swagger.js data-service",
"pull-api-main-data": "node swagger.js main-data"
}
}
- pull-api:批量拉取所有服务的 API
- pull-api-[service-name]:单独拉取指定服务的 API,便于针对性更新
2. 核心脚本实现
创建swagger.js文件,实现从 Swagger 文档抓取数据到生成 TS 文件的完整逻辑:
const fs = require('fs');
const path = require('path');
const https = require('https');
const http = require('http');
// 获取命令行参数
const args = process.argv.slice(2);
// 服务名称,同步生成改服务名称文件
const apiName = args[0];
if (!apiName) {
console.error('请提供 API 名称,例如: node swagger.js main-data');
process.exit(1);
}
// 输出目录
const outputDir = path.join(__dirname, 'src/api/swagger');
// 确保目录存在
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// 从 vite.config.ts 中获取 host 配置
const viteConfigPath = path.join(__dirname, 'vite.config.ts');
const viteConfigContent = fs.readFileSync(viteConfigPath, 'utf8');
// 提取 apiConfig.host 的值,处理注释情况
const regex = /^\s*host\s*:\s*'([^']+)'/m;
const hostMatch = viteConfigContent.match(regex);
const host = hostMatch ? hostMatch[1] : 'http://xxx.xxx.xx.xx'; // 默认值
// swagger json文件 完整地址, 可打开swagger网页控制台查看
const swaggerUrl = `${host}/${apiName}/v2/api-docs?group=API`;
// 网络请求获取 JSON
function fetchSwaggerJson(url) {
return new Promise((resolve, reject) => {
const protocol = url.startsWith('https:') ? https : http;
const options = {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json; charset=utf-8',
'User-Agent': 'Mozilla/5.0 (compatible; Swagger-Parser)',
},
};
protocol
.get(url, options, (res) => {
let data = '';
// 设置正确的编码
res.setEncoding('utf8');
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const swaggerData = JSON.parse(data);
resolve(swaggerData);
} catch (error) {
reject(new Error('解析 JSON 失败: ' + error.message));
}
});
})
.on('error', (error) => {
reject(new Error('请求失败: ' + error.message));
});
});
}
// 定义 Swagger 类型到 TypeScript 类型的映射
const typeMapping = {
integer: 'number',
int32: 'number',
int64: 'number',
long: 'number',
float: 'number',
double: 'number',
string: 'string',
boolean: 'boolean',
object: 'object',
array: 'any[]',
binary: 'File', // 文件类型
};
// 类型名合法化,去除特殊字符
function normalizeTypeName(typeName) {
if (!typeName) return '';
return typeName.replace(/[^a-zA-Z0-9\u4e00-\u9fa5_]/g, '_'); // 保留英文、数字、中文、下划线,其他替换为下划线
}
// 类型转换函数,递归处理 $ref、array、object
function swaggerTypeToTsType(prop, definitions) {
if (!prop) return 'any';
if (prop.$ref) {
return normalizeTypeName(prop.$ref.replace('#/definitions/', ''));
}
if (prop.type === 'array') {
if (prop.items.$ref) {
if (prop.items.$ref.includes('Error-ModelName')) {
return 'any';
}
return swaggerTypeToTsType(prop.items, definitions) + '[]';
}
return (typeMapping[prop.items.type] || 'any') + '[]';
}
if (prop.type === 'object' && prop.properties) {
// 匿名对象类型
return (
'{ ' +
Object.entries(prop.properties)
.map(([k, v]) => {
return `${k}: ${swaggerTypeToTsType(v, definitions)}`;
})
.join('; ') +
' }'
);
}
return typeMapping[prop.type] || 'any';
}
// 生成 interface,类型全部转为 TS 标准类型,类型名合法化
function parseDefinition(defName, definitions, parsed = new Set(), typeNameMap = {}) {
const normDefName = normalizeTypeName(defName);
if (parsed.has(normDefName)) return '';
parsed.add(normDefName);
const def = definitions[defName];
if (!def) return '';
if (def.type === 'object') {
let props = Object.entries(def.properties || {})
.map(([k, v]) => {
let type = swaggerTypeToTsType(v, definitions);
k = k.includes('.') ? `'${k}'` : k;
return `\t/** ${v.description || ''} */\n\t${k}${def.required && def.required.includes(k) ? '' : '?'}: ${type};`;
})
.join('\n');
return `export interface ${normDefName} {\n${props}\n}\n`;
}
// 处理枚举、数组等
return '';
}
// 获取请求的 Content-Type
function getContentType(consumes) {
if (!consumes || consumes.length === 0) {
return 'application/json';
}
return consumes[0];
}
// 判断是否为文件上传请求
function isFileUpload(consumes) {
const contentType = getContentType(consumes);
return contentType.includes('multipart/form-data');
}
// 生成请求方法的模板函数,类型名合法化
function generateRequestFunction(path, method, summary, operationId, parameters, responses, definitions, consumes) {
let paramComment = '';
let responseType = 'any';
let url = path;
const isFileUploadRequest = isFileUpload(consumes);
// 参数分类
const pathParams = [];
const queryParams = [];
const bodyParams = [];
const formDataParams = [];
(parameters || []).forEach((param) => {
if (param.in === 'path') pathParams.push(param);
else if (param.in === 'query') queryParams.push(param);
else if (param.in === 'body') {
if (isFileUploadRequest) {
// 对于文件上传,body参数可能是FormData
formDataParams.push(param);
} else {
bodyParams.push(param);
}
}
});
// 路径参数拼接
if (pathParams.length > 0) {
pathParams.forEach((param) => {
url = url.replace(`{${param.name}}`, `\${params.path?.${param.name}}`);
});
}
// 类型定义
let pathType = pathParams.length
? '{ ' + pathParams.map((p) => `${p.name}${p.required ? '' : '?'}: ${swaggerTypeToTsType(p, definitions)}`).join('; ') + ' }'
: 'undefined';
let queryType = queryParams.length
? '{ ' + queryParams.map((p) => `${p.name}${p.required ? '' : '?'}: ${swaggerTypeToTsType(p, definitions)}`).join('; ') + ' }'
: 'undefined';
let bodyType = 'undefined';
if (bodyParams.length) {
bodyType = swaggerTypeToTsType(bodyParams[0].schema, definitions);
} else if (formDataParams.length) {
// 对于文件上传,使用FormData类型
bodyType = 'FormData';
}
// params 类型
let paramsType = [
pathParams.length ? `path: ${pathType}` : '',
queryParams.length ? `query: ${queryType}` : '',
bodyParams.length || formDataParams.length ? `body: ${bodyType}` : '',
]
.filter(Boolean)
.join('; ');
paramsType = paramsType ? `{ ${paramsType} }` : 'undefined';
// 生成函数参数
const functionParams = paramsType === 'undefined' ? '' : `params: ${paramsType}`;
const functionParamsWithConfig = functionParams ? `${functionParams}, config?: AxiosRequestConfig` : `config?: AxiosRequestConfig`;
// 注释
pathParams.forEach((p) => (paramComment += ` * @param {${swaggerTypeToTsType(p, definitions)}} path.${p.name} ${p.description || ''}\n`));
queryParams.forEach((p) => (paramComment += ` * @param {${swaggerTypeToTsType(p, definitions)}} query.${p.name} ${p.description || ''}\n`));
if (bodyParams.length) {
bodyParams.forEach(
() => (paramComment += ` * @param {${swaggerTypeToTsType(bodyParams[0].schema, definitions)}} body ${bodyParams[0].description || ''}\n`)
);
} else if (formDataParams.length) {
formDataParams.forEach((p) => (paramComment += ` * @param {FormData} body ${p.description || '文件上传数据'}\n`));
}
// 响应类型
if (responses && responses['200'] && responses['200'].schema) {
responseType = swaggerTypeToTsType(responses['200'].schema, definitions);
}
// request 生成
let requestLines = [`url: \`${url}\``, `method: '${method}'`];
// 根据请求类型设置不同的参数
if (queryParams.length) {
requestLines.push('params: params.query');
}
if (isFileUploadRequest || bodyParams.length) {
// 文件上传或普通JSON请求
requestLines.push('data: params.body');
}
const functionTemplate = `
/**
* ${summary}
${paramComment} * @param {AxiosRequestConfig} config 可选的请求配置,用于覆盖默认配置
*/
export const ${operationId} = (${functionParamsWithConfig}): Promise<${responseType}> => {
return request({
${requestLines.join(',\n ')},
...config
});
};
`;
return functionTemplate;
}
// 主函数
async function main() {
try {
console.log(`正在获取 Swagger JSON: ${swaggerUrl}`);
const swaggerData = await fetchSwaggerJson(swaggerUrl);
// 保存原始 JSON 到本地文件
// const jsonFilePath = path.join(outputDir, apiName + '.json');
// fs.writeFileSync(jsonFilePath, JSON.stringify(swaggerData, null, 2), 'utf8');
// console.log(`原始 Swagger JSON 已保存到: ${jsonFilePath}`);
// 生成所有类型定义
let typeDefs = '';
const parsed = new Set();
for (const defName in swaggerData.definitions) {
typeDefs += parseDefinition(defName, swaggerData.definitions, parsed);
}
// 生成所有请求方法
let allFunctions = '';
for (const [path, pathData] of Object.entries(swaggerData.paths)) {
for (const [method, methodData] of Object.entries(pathData)) {
const { summary, operationId, parameters, responses, consumes } = methodData;
const requestFunction = generateRequestFunction(path, method, summary, operationId, parameters, responses, swaggerData.definitions, consumes);
allFunctions += requestFunction;
}
}
const importStr = `import request from '/@/utils/request';\n\n`;
const outputFilePath = path.join(outputDir, apiName + '.ts');
// 生成文件头部信息
const currentTime = new Date().toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
});
const fileHeader = `/**
* 自动生成的 API 方法文件
* 生成时间: ${currentTime}
* 数据源: ${swaggerUrl}
* 请勿手动修改此文件,重新生成时会覆盖
*/
`;
fs.writeFileSync(outputFilePath, fileHeader + importStr + typeDefs + allFunctions, 'utf8');
console.log(`生成的 API 方法已保存到 ${outputFilePath}`);
console.log('脚本执行完成!');
} catch (error) {
console.error('脚本执行失败:', error.message);
process.exit(1);
}
}
// 执行主函数
main();
3. 脚本核心功能解析
脚本主要包含以下关键模块:
- Swagger 数据获取:通过 HTTP 请求从 Swagger 地址抓取 API 元数据,支持 HTTP/HTTPS 协议
- 类型转换系统:
-
- 基础类型映射(如integer→number)
-
- 复杂类型递归处理(支持对象嵌套、数组、引用类型)
-
- 类型名标准化(自动处理特殊字符,避免 TS 语法错误)
- 接口生成器:
-
- 根据 Swagger 定义自动生成 TS 接口
-
- 保留字段描述作为 JSDoc 注释
-
- 正确标记可选字段(根据required属性)
- 请求函数生成器:
-
- 自动区分路径参数、查询参数和请求体
-
- 支持文件上传(识别multipart/form-data类型)
-
- 生成完整的 JSDoc 注释(包含参数说明和类型)
-
- 兼容 Axios 配置扩展
使用效果
生成产物示例
自动生成的 TS 类型定义
自动生成的请求函数
接口变更追踪
当后端更新接口并部署后,只需执行npm run pull-api,生成的文件会自动更新,通过 Git 可以清晰查看变更内容:
这种可视化的变更追踪让前后端协作更高效,无需人工比对文档。
注意事项
- 环境依赖:确保项目安装了 Node.js 环境(v14 + 推荐)
- Swagger 兼容性:脚本适用于 Swagger 2.0 版本,OpenAPI 3.0 需要适当调整路径解析逻辑
- 自定义扩展:
-
- 可根据项目需求修改typeMapping调整类型映射规则
-
- 如需支持更多请求库(如 fetch),可修改请求函数模板
-
- 复杂的响应结构可能需要优化swaggerTypeToTsType函数
- 安全机制:生成的文件应加入版本控制,但避免在生产环境暴露 Swagger 地址
总结
通过这套自动化方案,前端开发者可以:
- 彻底告别手动编写 API 类型和请求函数的重复劳动
- 后端接口变更时一键同步,减少 90% 以上的适配时间
- 通过类型系统提前发现接口不匹配问题
- 借助 Git 清晰追踪接口变更历史
该方案特别适合微服务架构或接口频繁变动的项目,将前端开发者从繁琐的 API 适配工作中解放出来,专注于业务逻辑实现,显著提升开发效率和代码质量。