openapi依据路径动态目录脚本生成

6 阅读7分钟

一键生成 OpenAPI 接口:让前端开发更高效

前言

在现代前端开发中,与后端 API 的对接是一项重复且容易出错的工作。手动编写接口调用代码不仅效率低下,还容易因为接口变更导致前后端不一致。本文将介绍一个基于 OpenAPI/Swagger 的自动化接口生成工具,帮助你告别手写接口代码的烦恼。

功能特性

这个工具具有以下核心功能:

  • 多源支持:可同时配置多个 API 源,一次性生成所有接口
  • 自动下载:从 Swagger 文档地址自动下载 OpenAPI 规范
  • 智能处理:自动移除路径前缀,按目录结构重组 tags
  • 目录化管理:将生成的接口按模块转换为目录结构,便于维护
  • 类型安全:基于 TypeScript,提供完整的类型提示

快速开始

1. 项目结构

首先,确保你的项目具有以下结构:

your-project/
├── scripts/
│   ├── generate-api.js       # 主脚本
│   └── openapi.config.js     # 配置文件
├── config/
│   └── config.ts             # UmiMax 配置
└── src/
    ├── api/                  # 临时存放下载的 OpenAPI 文档
    └── services/             # 生成的接口代码

2. 安装依赖

确保项目已安装必要的依赖:

npm install @umijs/max --save-dev
# 或
pnpm add @umijs/max -D

3. 配置 API 源

scripts/openapi.config.js 中配置你的 API 源:

module.exports = [
  {
    // API 源名称(用于生成目录名)
    name: 'zelos-api',
    
    // Swagger 文档地址
    url: 'https://openapi.apipost.net/swagger/v3/5ac414db5851000?locale=zh-cn',
    
    // 生成的服务目录(相对于 src/services/)
    outputDir: 'zelos-api',
    
    // 需要移除的路径前缀(支持正则表达式)
    removePrefix: '\{\{label-server\}\}',
  },
  
  // 可以添加更多 API 源
  {
    name: 'user-api',
    url: 'https://api.example.com/swagger/v3/docs',
    outputDir: 'user-api',
    removePrefix: '/api/v1',
  },
];

4. 配置 package.json

package.json 中添加快捷命令:

{
  "scripts": {
    "openapi": "node scripts/generate-api.js"
  }
}

5. 执行生成

运行命令即可生成接口:

npm run openapi
# 或
pnpm openapi

工作原理

整个工具分为三个核心步骤:

步骤 1:下载并处理 OpenAPI 文档

async function downloadAndProcess(config) {
  // 1. 从配置的 URL 下载 Swagger 文档
  // 2. 移除路径中的指定前缀(如 {{label-server}})
  // 3. 根据路径第一级目录自动设置 tags
  // 4. 保存处理后的文档到 src/api/
}

处理示例:

原始路径:

{{label-server}}/user/login
{{label-server}}/user/profile
{{label-server}}/order/list

处理后:

/user/login    → tag: user
/user/profile  → tag: user
/order/list    → tag: order

步骤 2:调用 OpenAPI 生成工具

function generateAPI(config) {
  // 1. 临时修改 config/config.ts 配置
  // 2. 调用 npx max openapi 生成接口代码
  // 3. 恢复原配置文件
}

这一步会生成类似这样的文件:

src/services/zelos-api/
├── user.ts
├── order.ts
├── product.ts
├── index.ts
└── typings.d.ts

步骤 3:转换为目录结构

function convertToDirectories(config) {
  // 将 user.ts → user/index.ts
  // 将 order.ts → order/index.ts
  // 更新主 index.ts 导出
}

最终结构:

src/services/zelos-api/
├── user/
│   └── index.ts
├── order/
│   └── index.ts
├── product/
│   └── index.ts
├── index.ts
└── typings.d.ts

使用生成的接口

生成完成后,你可以在项目中这样使用:

import zelosApi from '@/services/zelos-api';

// 调用用户登录接口
const login = async (username: string, password: string) => {
  try {
    const response = await zelosApi.user.postUserLogin({
      username,
      password,
    });
    return response;
  } catch (error) {
    console.error('登录失败', error);
  }
};

// 获取订单列表
const getOrders = async () => {
  const response = await zelosApi.order.getOrderList({
    page: 1,
    pageSize: 10,
  });
  return response.data;
};

配置说明

配置项详解

配置项类型必填说明
namestringAPI 源名称,用于生成目录和日志标识
urlstringSwagger 文档的完整 URL
outputDirstring生成代码的输出目录(相对于 src/services/
removePrefixstring需要从路径中移除的前缀(支持正则表达式)

removePrefix 使用技巧

removePrefix 支持正则表达式,可以灵活处理各种前缀:

// 移除固定前缀
removePrefix: '/api/v1'

// 移除变量前缀(需要转义特殊字符)
removePrefix: '\{\{server\}\}'

// 移除多个可能的前缀
removePrefix: '(/api/v1|/api/v2)'

高级用法

1. 多环境配置

你可以为不同环境创建不同的配置文件:

// openapi.config.dev.js
module.exports = [
  {
    name: 'api',
    url: 'https://dev-api.example.com/swagger',
    outputDir: 'api',
    removePrefix: '/dev',
  },
];

// openapi.config.prod.js
module.exports = [
  {
    name: 'api',
    url: 'https://api.example.com/swagger',
    outputDir: 'api',
    removePrefix: '/v1',
  },
];

然后在 package.json 中添加:

{
  "scripts": {
    "openapi:dev": "node scripts/generate-api.js --config=openapi.config.dev.js",
    "openapi:prod": "node scripts/generate-api.js --config=openapi.config.prod.js"
  }
}

2. 自定义生成后处理

你可以在脚本中添加自定义的后处理逻辑:

// 在 convertToDirectories 函数后添加
function postProcess(config) {
  const apiDir = path.join(PROJECT_ROOT, 'src/services', config.outputDir);
  
  // 例如:添加自定义的请求拦截器
  const interceptorContent = `
import { request } from '@umijs/max';

// 自定义请求拦截器
request.interceptors.request.use((config) => {
  // 添加 token
  config.headers.Authorization = localStorage.getItem('token');
  return config;
});
`;
  
  fs.writeFileSync(
    path.join(apiDir, 'interceptor.ts'),
    interceptorContent
  );
}

3. 集成到 CI/CD

在持续集成流程中自动生成接口:

# .github/workflows/generate-api.yml
name: Generate API

on:
  schedule:
    - cron: '0 2 * * *'  # 每天凌晨 2 点执行
  workflow_dispatch:      # 支持手动触发

jobs:
  generate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          
      - name: Install dependencies
        run: pnpm install
        
      - name: Generate API
        run: pnpm openapi
        
      - name: Create Pull Request
        uses: peter-evans/create-pull-request@v5
        with:
          commit-message: 'chore: update API definitions'
          title: '🤖 自动更新 API 定义'
          branch: auto-update-api

常见问题

Q1: 生成失败,提示 "配置文件错误"

A: 检查 openapi.config.js 是否正确导出数组,且每个配置项都包含必填字段。

Q2: 生成的接口类型不准确

A: 这通常是 Swagger 文档本身的问题。建议:

  1. 检查后端 Swagger 文档的类型定义
  2. 联系后端开发完善文档
  3. 必要时可以手动修改生成的 typings.d.ts

Q3: 如何处理接口版本变更?

A:

  1. 为不同版本创建不同的 outputDir
  2. 或者使用 Git 分支管理不同版本的接口代码

Q4: 生成的代码能否自定义?

A: 可以通过修改 UmiMax 的 OpenAPI 配置来自定义生成模板,参考 UmiMax OpenAPI 文档

最佳实践

  1. 定期更新:建议每天或每次后端接口变更后重新生成
  2. 版本控制:将生成的代码纳入 Git 管理,便于追踪变更
  3. 代码审查:生成后检查 diff,确保变更符合预期
  4. 文档同步:要求后端团队及时更新 Swagger 文档
  5. 类型检查:开启 TypeScript 严格模式,充分利用类型安全

总结

这个 OpenAPI 自动化工具能够显著提升前端开发效率,减少手动编写接口代码的工作量。通过配置化管理多个 API 源,你可以轻松维护复杂的微服务架构项目。

核心优势:

  • 🚀 效率提升:从手写到自动化,节省 80% 的接口对接时间
  • 🛡️ 类型安全:TypeScript 类型提示,减少运行时错误
  • 📦 易于维护:目录化结构,清晰的模块划分
  • 🔄 持续同步:接口变更自动同步,保持前后端一致

希望这个工具能帮助你的团队提升开发效率!如果有任何问题或建议,欢迎在评论区讨论。


相关资源:

源码

generate-api.js

#!/usr/bin/env node  
/**
 * 一键生成 OpenAPI 接口(支持多源配置)
 * 1. 下载并处理 Swagger 文档
 * 2. 调用 OpenAPI 生成工具
 * 3. 转换为目录结构
 */
const https = require('https');
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

// 加载配置
const apiConfigs = require('./openapi.config.js');
const PROJECT_ROOT = path.join(__dirname, '..');

// 步骤1: 下载并处理 OpenAPI 文档
async function downloadAndProcess(config) {
  console.log(`📥 [${config.name}] 正在下载 OpenAPI 文档...`);

  const openAPIData = await new Promise((resolve, reject) => {
    https
      .get(config.url, (res) => {
        let data = '';
        res.on('data', (chunk) => {
          data += chunk;
        });
        res.on('end', () => {
          try {
            resolve(JSON.parse(data));
          } catch (e) {
            reject(e);
          }
        });
      })
      .on('error', reject);
  });

  console.log(`🔧 [${config.name}] 正在处理路径和标签...`);

  const paths = openAPIData.paths;
  const newPaths = {};
  const prefixRegex = new RegExp(config.removePrefix, 'g');

  Object.keys(paths).forEach((path) => {
    // 移除路径中的配置的前缀
    const newPath = path.replace(prefixRegex, '');
    const pathMethods = paths[path];

    // 动态识别所有 HTTP 方法并重新设置 tags
    Object.keys(pathMethods).forEach((method) => {
      // 跳过非方法属性
      if (
        method.startsWith('x-') ||
        ['parameters', 'servers', 'summary', 'description'].includes(method)
      ) {
        return;
      }

      // 检查是否为有效的 HTTP 方法
      if (
        pathMethods[method] &&
        typeof pathMethods[method] === 'object' &&
        'responses' in pathMethods[method]
      ) {
        // 根据路径第一级目录设置 tag
        const pathParts = newPath
          .split('/')
          .filter((part) => part && !part.startsWith('{'));
        const firstDir = pathParts[0] || 'root';
        pathMethods[method].tags = [firstDir];
      }
    });

    newPaths[newPath] = pathMethods;
  });

  openAPIData.paths = newPaths;

  // 保存处理后的文档
  const outputPath = path.join(PROJECT_ROOT, 'src/api', `${config.name}.json`);
  const dir = path.dirname(outputPath);
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
  }
  fs.writeFileSync(outputPath, JSON.stringify(openAPIData, null, 2));
  console.log(`✅ [${config.name}] 文档处理完成`);

  return outputPath;
}

// 步骤2: 调用 OpenAPI 生成工具
function generateAPI(config) {
  console.log(`\n🔨 [${config.name}] 正在生成接口代码...`);
  try {
    // 临时更新配置文件以使用当前 API 源
    const configPath = path.join(PROJECT_ROOT, 'config/config.ts');
    const configContent = fs.readFileSync(configPath, 'utf-8');

    // 备份原配置
    const backupPath = configPath + '.backup';
    fs.writeFileSync(backupPath, configContent);

    try {
      // 更新 schemaPath 和 projectName
      const updatedConfig = configContent
        .replace(
          /schemaPath:\s*join\([^)]+\)/,
          `schemaPath: join(__dirname, '../src/api/${config.name}.json')`,
        )
        .replace(
          /projectName:\s*['"][^'"]*['"]/,
          `projectName: '${config.outputDir}'`,
        );

      fs.writeFileSync(configPath, updatedConfig);

      // 执行生成命令
      execSync('npx max openapi', { stdio: 'inherit', cwd: PROJECT_ROOT });

      console.log(`✅ [${config.name}] 接口代码生成完成`);
    } finally {
      // 恢复原配置
      fs.writeFileSync(configPath, configContent);
      fs.unlinkSync(backupPath);
    }
  } catch (error) {
    console.error(`❌ [${config.name}] 生成失败:`, error.message);
    throw error;
  }
}

// 步骤3: 转换为目录结构
function convertToDirectories(config) {
  console.log(`\n📁 [${config.name}] 正在转换为目录结构...`);

  const apiDir = path.join(PROJECT_ROOT, 'src/services', config.outputDir);

  if (!fs.existsSync(apiDir)) {
    console.log(`⚠️  [${config.name}] 目录不存在: ${apiDir}`);
    return;
  }

  const files = fs.readdirSync(apiDir);
  const filesToConvert = files.filter(
    (file) =>
      file.endsWith('.ts') && file !== 'index.ts' && file !== 'typings.d.ts',
  );

  if (filesToConvert.length === 0) {
    console.log(`⚠️  [${config.name}] 没有需要转换的文件`);
    return;
  }

  const modules = [];

  filesToConvert.forEach((file) => {
    const moduleName = file.replace('.ts', '');
    const filePath = path.join(apiDir, file);
    const dirPath = path.join(apiDir, moduleName);
    const newFilePath = path.join(dirPath, 'index.ts');

    // 读取并移动文件
    const content = fs.readFileSync(filePath, 'utf-8');

    if (!fs.existsSync(dirPath)) {
      fs.mkdirSync(dirPath, { recursive: true });
    }

    fs.writeFileSync(newFilePath, content);
    fs.unlinkSync(filePath);

    modules.push(moduleName);
    console.log(`  ✓ ${file}${moduleName}/index.ts`);
  });

  // 更新主 index.ts
  if (modules.length > 0) {
    const imports = modules
      .sort()
      .map((m) => `import * as ${m} from './${m}';`)
      .join('\n');
    const exports = modules
      .sort()
      .map((m) => `  ${m},`)
      .join('\n');

    const indexContent = `// @ts-ignore
/* eslint-disable */
// API 更新时间:
// API 唯一标识:
${imports}
export default {
${exports}
};
`;

    fs.writeFileSync(path.join(apiDir, 'index.ts'), indexContent);
    console.log(`  ✓ 更新 index.ts`);
  }

  console.log(`✅ [${config.name}] 目录结构转换完成`);
}

// 主流程
async function main() {
  console.log('🚀 开始生成 OpenAPI 接口\n');

  if (!Array.isArray(apiConfigs) || apiConfigs.length === 0) {
    console.error(
      '❌ 配置文件错误:请在 scripts/openapi.config.js 中配置至少一个 API 源',
    );
    process.exit(1);
  }

  console.log(`📋 共找到 ${apiConfigs.length} 个 API 源\n`);

  try {
    // 处理每个 API 源
    for (const config of apiConfigs) {
      console.log(`${'='.repeat(60)}`);
      console.log(`开始处理: ${config.name}`);
      console.log(`${'='.repeat(60)}\n`);

      // 验证配置
      if (
        !config.name ||
        !config.url ||
        !config.outputDir ||
        !config.removePrefix
      ) {
        console.error(`❌ [${config.name || '未命名'}] 配置不完整,跳过`);
        continue;
      }

      await downloadAndProcess(config);
      generateAPI(config);
      convertToDirectories(config);

      console.log(`\n✅ ${config.name} 处理完成\n`);
    }

    console.log('='.repeat(60));
    console.log('✨ 全部完成!');
    console.log('='.repeat(60));
  } catch (error) {
    console.error('\n❌ 生成失败:', error.message);
    process.exit(1);
  }
}

main();
openapi.config.js
/**
 * OpenAPI 配置文件
 * 支持配置多个 API 源
 */
module.exports = [
  {
    // API 源名称(用于生成目录名)
    name: 'zelos-api',
    // Swagger 文档地址
    url: 'https://openapi.apipost.net/swagger/v3/5ac414db5851000?locale=zh-cn',
    // 生成的服务目录(相对于 src/services/)
    outputDir: 'zelos-api',
    // 需要移除的路径前缀(支持正则表达式字符串)
    removePrefix: '\\{\\{label-server\\}\\}',
  },

  // 如果有其他 API 源,在这里添加
  // {
  //   name: 'another-api',
  //   url: 'https://openapi.apipost.net/swagger/v3/5ad464cd8052000?locale=zh-cn',
  //   outputDir: 'another-api',
  //   removePrefix: '\\{\\{dap-server\\}\\}',
  // },
];