作为前端,你是否每天都在做这些事:
- 盯着 Swagger 文档,把接口参数一个个敲成 TS 类型
- 后端改了字段,前端同步改 N 个地方还怕漏
- 接口参数多到眼花,手写请求函数总出错
配置package.json 命令
这里我们是微服务架构所以有多个文档地址,为了方便设置单独服务拉取命令,每次后端部署完成后执行即可
json
"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"
},
拉取脚本代码
新建swagger.js 于项目根目录
swagger.js
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();
脚本生成产物效果预览
上半部份自动生成接口类型声明
下半部份为请求方法 后端更新接口部署完成后执行效果,通过git可查看本次接口变更
