我写了个脚本,让前端彻底告别 Swagger 手动搬砖

作为前端,你是否每天都在做这些事:

  • 盯着 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可查看本次接口变更

相关推荐
一斤代码2 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子2 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年2 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子2 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina2 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路3 小时前
React--Fiber 架构
前端·react.js·架构
伍哥的传说4 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js
qq_424409194 小时前
uniapp的app项目,某个页面长时间无操作,返回首页
前端·vue.js·uni-app
我在北京coding4 小时前
element el-table渲染二维对象数组
前端·javascript·vue.js
布兰妮甜4 小时前
Vue+ElementUI聊天室开发指南
前端·javascript·vue.js·elementui