openapi2ts 统一后端返回值

一、安装 openapi2ts

openapi2ts介绍:一个强大的工具,可以根据 OpenAPI 3.0 文档生成 TypeScript 请求代码。

官方地址:github.com/chenshuai21...

命令行执行下面命令

ts 复制代码
npm i --save-dev @umijs/openapi
npm i --save-dev tslib

二、项目根目录配置

新建自定义的ts文件 在前端项目的根目录 下新建 openapi2ts.config.ts,根据需要定制生成的代码

ts 复制代码
export default {
  requestLibPath: "import { request } from '@/utils/alova'",
  schemaPath: 'http://localhost:9123/api/v3/api-docs',
  serversPath: './src',
  apiPrefix: '',
  namespace: 'API',
}

三、alova 配置文件

对于alova安装,请自行搜索

项目根目录创建/src/utils/alova.ts

ts 复制代码
import { createAlova } from 'alova'
import VueHook from 'alova/vue'
import adapterFetch from 'alova/fetch'
import { notification } from 'ant-design-vue'

// 创建 Alova 实例
const alovaInstance = createAlova({
  baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:9123/api/',
  statesHook: VueHook,
  requestAdapter: adapterFetch(),
  timeout: 10000,

  // 请求拦截器
  beforeRequest(method) {
    // 添加 token 等认证信息
    const token = localStorage.getItem('token')
    if (token) {
      method.config.headers = {
        ...method.config.headers,
        'Authorization': `Bearer ${token}`
      }
    }

    // 添加 Content-Type
    if (method.type === 'POST' || method.type === 'PUT' || method.type === 'PATCH') {
      method.config.headers = {
        ...method.config.headers,
        'Content-Type': 'application/json'
      }
    }
  },

  // 响应拦截器
  responded: {
    async onSuccess(response: Response) {
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }

      const result = await response.json()

      // 检查业务状态码
      if (result.code !== undefined) {
        switch (result.code) {
          case 20000: // SUCCESS
            return result
          case 40000: // PARAMS_ERROR
            notification.error({
              message: '参数错误',
              description: result.message || '请求参数错误'
            })
            throw new Error(result.message || '请求参数错误')
          case 40100: // NOT_LOGIN_ERROR
            notification.warning({
              message: '未登录',
              description: '请先登录后再操作'
            })
            localStorage.removeItem('token')
            localStorage.removeItem('user')
            window.location.href = '/login'
            throw new Error(result.message || '未登录')
          case 40101: // NO_AUTH_ERROR
            notification.error({
              message: '权限不足',
              description: result.message || '您没有权限执行此操作'
            })
            throw new Error(result.message || '权限不足')
          case 40400: // NOT_FOUND_ERROR
            notification.error({
              message: '资源不存在',
              description: result.message || '请求的资源不存在'
            })
            throw new Error(result.message || '资源不存在')
          case 40300: // FORBIDDEN_ERROR
            notification.error({
              message: '禁止访问',
              description: result.message || '禁止访问'
            })
            throw new Error(result.message || '禁止访问')
          case 50000: // SYSTEM_ERROR
            notification.error({
              message: '系统错误',
              description: result.message || '系统内部异常,请稍后重试'
            })
            throw new Error(result.message || '系统内部异常')
          case 50001: // OPERATION_ERROR
            notification.error({
              message: '操作失败',
              description: result.message || '操作失败,请重试'
            })
            throw new Error(result.message || '操作失败')
          default:
            notification.error({
              message: '请求失败',
              description: result.message || '未知错误'
            })
            throw new Error(result.message || '未知错误')
        }
      }

      return result
    },

    onError(err: any) {
      // 网络错误处理
      console.error('Request failed:', err)

      const status = err.response?.status || err.status

      // 根据HTTP状态码进行处理
      if (status === 401) {
        notification.warning({
          message: '未授权',
          description: '请重新登录'
        })
        localStorage.removeItem('token')
        localStorage.removeItem('user')
        window.location.href = '/login'
      } else if (status === 403) {
        notification.error({
          message: '权限不足',
          description: '您没有权限执行此操作'
        })
      } else if (status === 404) {
        notification.error({
          message: '资源不存在',
          description: '请求的资源不存在'
        })
      } else if (status >= 500) {
        notification.error({
          message: '服务器错误',
          description: '服务器内部错误,请稍后重试'
        })
      } else {
        notification.error({
          message: '网络错误',
          description: err.message || '网络连接异常,请检查网络'
        })
      }

      throw err
    }
  }
})

// 导出请求方法,根据method类型选择合适的alova方法
export const request = <TResponse = any>(url: string, options?: any) => {
  const { method = 'GET', data, ...restOptions } = options || {}

  switch (method.toUpperCase()) {
    case 'POST':
      return alovaInstance.Post(url, data, restOptions) as Promise<TResponse>
    case 'PUT':
      return alovaInstance.Put(url, data, restOptions) as Promise<TResponse>
    case 'DELETE':
      return alovaInstance.Delete(url, restOptions) as Promise<TResponse>
    case 'PATCH':
      return alovaInstance.Put(url, data, restOptions) as Promise<TResponse>
    default:
      return alovaInstance.Get(url, { ...restOptions, params: options?.params }) as Promise<TResponse>
  }
}

export default alovaInstance

三、统一后端返回值工具类

在项目根目录创建一个script/update-api-response.js

ts 复制代码
#!/usr/bin/env node

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

// 获取当前脚本所在目录
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// API 目录路径(相对于项目根目录)
const projectRoot = path.join(__dirname, '..');
const apiDir = path.join(projectRoot, 'src', 'api');

// 排除的文件列表
const excludeFiles = [
  'index.ts',
  'typings.d.ts',
  'fileTransferUtil.ts'  // 工具类文件,可能不需要修改
];

// 正则表达式匹配 request<类型> 模式
const requestTypeRegex = /request<([^>]+)>/g;

/**
 * 更新单个 API 文件
 * @param {string} filePath - 文件路径
 * @returns {object} - 更新结果 { updated: boolean, changeCount: number }
 */
function updateApiFile(filePath) {
  try {
    const content = fs.readFileSync(filePath, 'utf8');
    
    // 查找所有匹配的内容
    const matches = content.match(requestTypeRegex) || [];
    
    if (matches.length === 0) {
      return { updated: false, changeCount: 0 };
    }
    
    // 替换所有的 request<任何类型> 为 request<API.BaseResponseVoid>
    const updatedContent = content.replace(requestTypeRegex, 'request<API.BaseResponseVoid>');
    
    // 检查是否有变化
    if (content !== updatedContent) {
      fs.writeFileSync(filePath, updatedContent, 'utf8');
      return { updated: true, changeCount: matches.length };
    } else {
      return { updated: false, changeCount: 0 };
    }
  } catch (error) {
    console.error(`❌ 处理文件失败: ${path.basename(filePath)}`, error.message);
    return { updated: false, changeCount: 0 };
  }
}

/**
 * 获取所有需要处理的 TypeScript 文件
 * @returns {string[]} - 文件路径列表
 */
function getApiFiles() {
  try {
    const files = fs.readdirSync(apiDir);
    
    return files
      .filter(file => {
        // 只处理 .ts 文件,排除 .d.ts 文件和指定的排除文件
        return file.endsWith('.ts') && 
               !file.endsWith('.d.ts') && 
               !excludeFiles.includes(file);
      })
      .map(file => path.join(apiDir, file));
  } catch (error) {
    console.error(`❌ 读取 API 目录失败: ${error.message}`);
    return [];
  }
}

/**
 * 主函数
 */
function main() {
  console.log('🚀 开始批量更新 API 接口返回值类型...');
  console.log(`📂 API 目录: ${apiDir}\n`);
  
  // 检查 API 目录是否存在
  if (!fs.existsSync(apiDir)) {
    console.error(`❌ API 目录不存在: ${apiDir}`);
    process.exit(1);
  }
  
  // 获取所有需要处理的文件
  const apiFiles = getApiFiles();
  
  if (apiFiles.length === 0) {
    console.log('⚠️  未找到需要处理的 TypeScript 文件');
    return;
  }
  
  console.log(`📝 找到 ${apiFiles.length} 个文件需要处理:\n`);
  
  let updatedCount = 0;
  let totalInterfaces = 0;
  
  // 处理每个文件
  apiFiles.forEach(filePath => {
    const fileName = path.basename(filePath);
    console.log(`📄 正在处理: ${fileName}`);
    
    const result = updateApiFile(filePath);
    
    if (result.updated) {
      updatedCount++;
      totalInterfaces += result.changeCount;
      console.log(`✅ 已更新: ${fileName} (${result.changeCount} 个接口)`);
    } else if (result.changeCount === 0) {
      console.log(`⏭️  无接口需要更新: ${fileName}`);
    } else {
      console.log(`⏭️  无需更新: ${fileName}`);
    }
    
    console.log('');
  });
  
  // 输出汇总信息
  console.log('═'.repeat(50));
  console.log('✨ 批量更新完成!');
  console.log(`📊 统计信息:`);
  console.log(`   - 处理文件数: ${apiFiles.length}`);
  console.log(`   - 更新文件数: ${updatedCount}`);
  console.log(`   - 更新接口数: ${totalInterfaces}`);
  
  if (updatedCount > 0) {
    console.log('\n🎉 所有接口返回值已统一设置为 API.BaseResponseVoid');
  } else {
    console.log('\n✅ 所有文件均已是最新状态,无需更新');
  }
}

// 运行主函数
main();

// 导出函数供其他模块使用
export { updateApiFile, getApiFiles };

3.1 功能说明

update-api-response.js 是一个自动化脚本,用于批量将 src/api/ 目录下所有 TypeScript 文件中的接口返回值类型统一设置为 API.BaseResponseVoid。 功能说明:

  • 🔍 自动扫描 :自动扫描 src/api/ 目录下的所有 .ts 文件
  • 🎯 智能过滤 :自动排除不需要处理的文件(如 index.tstypings.d.ts 等)
  • 🔄 批量替换 :将所有 request<任何类型> 替换为 request<API.BaseResponseVoid>
  • 📊 详细统计:提供处理结果的详细统计信息
  • 安全处理:只修改需要修改的文件,避免不必要的变更

3.2 使用方法

方式一:使用 npm 脚本(推荐)

命令行执行

bash 复制代码
npm run update-api

方式二:直接运行脚本

命令行执行

bash 复制代码
node scripts/update-api-response.js

方式三:和openapi2ts脚本一起执行

修改 package.json

json 复制代码
"openapi2ts": "openapi2ts && node scripts/update-api-response.js",
相关推荐
夕水7 小时前
10个互动关卡带你轻松掌握js的位运算
前端·javascript
复苏季风7 小时前
为什么Vite动态加载图片报错? 动态资源引入的那些事儿
前端·vue.js·架构
sp427 小时前
静态网站生成利器 Eleventy
前端
带娃的IT创业者7 小时前
Python备份实战专栏第4/6篇:Vue.js + Flask 打造企业级备份监控面板
vue.js·python·flask
阿聪_8 小时前
React.ComponentType 类型使用
前端
aiwery8 小时前
理解 JavaScript 中的 Iterator、Generator、Promise 与 async/await
前端·面试
K仔8 小时前
什么是DOM事件模型
前端
熊猫片沃子8 小时前
新手必避的 Vue 基础坑:从数据绑定到事件处理的常见错误与解决方案
前端·vue.js
lichenyang4538 小时前
UniApp 实现搜索页逻辑详解
前端