KOA技术分享

专注 Koa.js 框架开发

Koa.js API 文档自动生成与 OpenAPI 实践

API 文档自动化概述

良好的 API 文档是前后端分离开发的基础。手动编写文档不仅繁琐,而且容易与实际代码脱节。本文介绍在 Koa.js 项目中如何实现 API 文档的自动生成,包括 Swagger/OpenAPI 集成、注解驱动文档、版本管理等功能。

技术选型对比

方案 特点 集成难度 适用场景
Swagger UI 可视化界面、在线调试 快速集成
注解驱动 代码即文档、自动生成 大型项目
RAML YAML 格式、RESTful API 标准规范
API Blueprint Markdown 格式、易读 团队协作

Swagger 集成实现

在 Koa.js 中集成 Swagger UI:

// Swagger 集成中间件
const KoaSwagger = require('koa2-swagger-ui');
const swaggerJsdoc = require('swagger-jsdoc');

class SwaggerMiddleware {
  constructor(options) {
    this.options = options;
    this.spec = null;
  }

  // 初始化 Swagger 规范
  async init() {
    const swaggerOptions = {
      definition: {
        openapi: '3.0.0',
        info: {
          title: this.options.title || 'API Documentation',
          version: this.options.version || '1.0.0',
          description: this.options.description || 'API Documentation',
          contact: {
            name: this.options.contactName || 'API Support',
            email: this.options.contactEmail || 'support@example.com'
          },
          license: {
            name: this.options.license || 'MIT',
            url: this.options.licenseUrl || 'https://opensource.org/licenses/MIT'
          },
          servers: this.options.servers || []
        },
        components: {
          securitySchemes: {
            bearerAuth: {
              type: 'http',
              scheme: 'bearer',
              bearerFormat: 'JWT'
            },
            apiKey: {
              type: 'apiKey',
              in: 'header',
              name: 'X-API-Key'
            }
          },
          schemas: this.options.schemas || {},
          responses: this.options.responses || {}
        },
        tags: this.options.tags || []
      },
      apis: this.options.apis || ['./routes/**/*.js']
    };

    this.spec = swaggerJsdoc(swaggerOptions);
    return this.spec;
  }

  // 中间件
  middleware() {
    return async (ctx, next) => {
      // 拦截 /api-docs 请求
      if (ctx.path === '/api-docs' || ctx.path.startsWith('/api-docs/')) {
        // 返回 Swagger UI 页面
        ctx.body = KoaSwagger({
          swaggerOptions: {
            spec: this.spec,
            url: '/api-docs/json'
          }
        });
        return;
      }

      // 返回 OpenAPI 规范 JSON
      if (ctx.path === '/api-docs/json') {
        ctx.set('Content-Type', 'application/json');
        ctx.body = this.spec;
        return;
      }

      await next();
    };
  }
}

// 使用示例
const swagger = new SwaggerMiddleware({
  title: 'EIMS 企业管理系统 API',
  version: '1.0.0',
  description: '企业信息化管理系统 RESTful API 文档',
  servers: [
    { url: 'http://localhost:3000', description: '开发环境' },
    { url: 'https://api.eims.com', description: '生产环境' }
  ],
  apis: ['./routes/*.js', './controllers/*.js']
});

await swagger.init();
app.use(swagger.middleware());

路由注解实现

通过注解自动生成 API 文档:

// 路由注解解析器
const RouteAnnotation = require('./annotation');

class ApiDocGenerator {
  constructor() {
    this.paths = {};
    this.definitions = {};
    this.tags = [];
  }

  // 解析控制器类
  parseController(ControllerClass) {
    const controller = new ControllerClass();
    const classDoc = RouteAnnotation.getClassDoc(ControllerClass);
    const methods = RouteAnnotation.getMethodDocs(ControllerClass);

    for (const method of methods) {
      const path = this.buildPath(classDoc.basePath, method.path, method.method);
      const operationId = `${ControllerClass.name}_${method.name}`;

      this.paths[path] = this.paths[path] || {};
      this.paths[path][method.method] = {
        operationId,
        summary: method.summary || method.description,
        description: method.description,
        tags: method.tags || [classDoc.tag],
        deprecated: method.deprecated || false,
        parameters: this.parseParameters(method.parameters),
        requestBody: this.parseRequestBody(method.requestBody),
        responses: this.parseResponses(method.responses),
        security: method.security ? [{ bearerAuth: [] }] : []
      };

      // 添加到标签列表
      if (method.tags && !this.tags.find(t => t.name === method.tags[0])) {
        this.tags.push({ name: method.tags[0], description: classDoc.description });
      }
    }

    return this;
  }

  // 构建路径
  buildPath(basePath, path, method) {
    const fullPath = (basePath || '') + path;
    return fullPath.replace(/{(\w+)}/g, ':$1');  # 转换 :id 为 {id}
  }

  // 解析参数
  parseParameters(params) {
    if (!params) return [];

    return params.map(param => ({
      name: param.name,
      in: param.in,  // query, path, header, cookie
      description: param.description,
      required: param.required || false,
      schema: {
        type: param.type,
        format: param.format,
        enum: param.enum,
        default: param.default
      }
    }));
  }

  // 解析请求体
  parseRequestBody(requestBody) {
    if (!requestBody) return null;

    const { schema, description, required } = requestBody;

    return {
      description,
      required: required !== false,
      content: {
        'application/json': {
          schema: this.getSchemaRef(schema)
        }
      }
    };
  }

  // 解析响应
  parseResponses(responses) {
    const result = {};
    for (const [code, response] of Object.entries(responses)) {
      result[code] = {
        description: response.description,
        content: response.content ? {
          'application/json': {
            schema: this.getSchemaRef(response.schema)
          }
        } : undefined
      };
    }
    return result;
  }

  // 获取 Schema 引用
  getSchemaRef(schema) {
    if (!schema) return {};

    if (schema.$ref) {
      return { $ref: `#/components/schemas/${schema.$ref}` };
    }

    if (schema.type === 'array') {
      return {
        type: 'array',
        items: this.getSchemaRef(schema.items)
      };
    }

    if (schema.type === 'object') {
      return {
        type: 'object',
        properties: Object.entries(schema.properties).reduce((acc, [key, prop]) => {
          acc[key] = this.getSchemaRef(prop);
          return acc;
        }, {})
      };
    }

    return schema;
  }

  // 生成 OpenAPI 规范
  generate() {
    return {
      openapi: '3.0.0',
      info: {
        title: 'API Documentation',
        version: '1.0.0'
      },
      paths: this.paths,
      components: {
        schemas: this.definitions,
        securitySchemes: {
          bearerAuth: {
            type: 'http',
            scheme: 'bearer',
            bearerFormat: 'JWT'
          }
        }
      },
      tags: this.tags
    };
  }
}

// 注解定义
const api = {
  // 类级别注解
  Controller: (config) => {
    return (target) => {
      RouteAnnotation.setClassDoc(target, config);
      return target;
    };
  },

  // 方法级别注解
  Get: (path, config = {}) => RouteAnnotation.createMethodDoc('get', path, config),
  Post: (path, config = {}) => RouteAnnotation.createMethodDoc('post', path, config),
  Put: (path, config = {}) => RouteAnnotation.createMethodDoc('put', path, config),
  Delete: (path, config = {}) => RouteAnnotation.createMethodDoc('delete', path, config),

  // 参数注解
  Query: (name, config = {}) => RouteAnnotation.createParamDoc('query', name, config),
  Param: (name, config = {}) => RouteAnnotation.createParamDoc('path', name, config),
  Body: (config = {}) => RouteAnnotation.createParamDoc('body', 'body', config),
  Header: (name, config = {}) => RouteAnnotation.createParamDoc('header', name, config),

  // 响应注解
  Success: (status, config = {}) => RouteAnnotation.createResponseDoc(status, config),
  Error: (status, config = {}) => RouteAnnotation.createResponseDoc(status, config)
};

// 使用示例
@Controller({
  basePath: '/api/users',
  tag: '用户管理',
  description: '用户相关接口'
})
class UserController {
  @Get('/users', {
    summary: '获取用户列表',
    description: '分页获取用户列表,支持过滤和排序',
    tags: ['用户管理']
  })
  @Query('page', { type: 'integer', description: '页码', default: 1 })
  @Query('pageSize', { type: 'integer', description: '每页数量', default: 20 })
  @Query('keyword', { type: 'string', description: '搜索关键词' })
  @Success(200, {
    description: '成功',
    schema: { type: 'array', items: { $ref: 'User' } }
  })
  @Error(500, { description: '服务器错误' })
  async listUsers(ctx) {
    const { page, pageSize, keyword } = ctx.query;
    const users = await this.userService.findAll({ page, pageSize, keyword });
    ctx.body = users;
  }

  @Post('/users', {
    summary: '创建用户',
    description: '创建新用户',
    tags: ['用户管理'],
    security: true
  })
  @Body({
    description: '用户信息',
    schema: { $ref: 'CreateUserDTO' }
  })
  @Success(201, {
    description: '创建成功',
    schema: { $ref: 'User' }
  })
  @Error(400, { description: '参数错误' })
  @Error(409, { description: '用户已存在' })
  async createUser(ctx) {
    const userData = ctx.request.body;
    const user = await this.userService.create(userData);
    ctx.status = 201;
    ctx.body = user;
  }
}

API 版本管理

支持多版本 API 文档管理:

// API 版本管理服务
class ApiVersionManager {
  constructor() {
    this.versions = new Map();
  }

  // 注册API版本
  registerVersion(version, config) {
    const generator = new ApiDocGenerator();
    const spec = generator.parseController(config.controller).generate();

    // 添加版本信息
    spec.info.version = version;
    spec.info.title = `${config.title} - v${version}`;
    spec.servers = config.servers || [];
    spec.deprecated = config.deprecated || false;

    this.versions.set(version, {
      spec,
      controller: config.controller,
      deprecated: config.deprecated || false,
      sunsetDate: config.sunsetDate,
      config
    });

    return this;
  }

  // 获取版本列表
  getVersionList() {
    return Array.from(this.versions.entries()).map(([version, data]) => ({
      version,
      title: data.config.title,
      deprecated: data.deprecated,
      sunsetDate: data.sunsetDate,
      docsUrl: `/api-docs/v${version}`
    }));
  }

  // 获取最新稳定版本
  getLatestStable() {
    const stable = Array.from(this.versions.entries())
      .filter(([_, data]) => !data.deprecated)
      .sort((a, b) => this.compareVersion(b[0], a[0]));

    return stable[0] ? { version: stable[0][0], spec: stable[0][1].spec } : null;
  }

  // 版本兼容检查
  checkCompatibility(currentVersion, targetVersion) {
    const current = this.versions.get(currentVersion);
    const target = this.versions.get(targetVersion);

    if (!current || !target) {
      return { compatible: false, reason: '版本不存在' };
    }

    // 主版本不同不兼容
    if (this.getMajorVersion(currentVersion) !== this.getMajorVersion(targetVersion)) {
      return { compatible: false, breaking: true, reason: '主版本不同,不兼容' };
    }

    // 次版本升级兼容
    if (this.getMinorVersion(targetVersion) > this.getMinorVersion(currentVersion)) {
      return { compatible: true, breaking: false, action: 'soft_warning' };
    }

    return { compatible: true, breaking: false };
  }

  // 版本废弃通知
  async getDeprecationNotices() {
    const notices = [];
    const now = new Date();

    for (const [version, data] of this.versions.entries()) {
      if (data.deprecated && data.sunsetDate) {
        const daysUntilSunset = Math.ceil(
          (new Date(data.sunsetDate) - now) / (1000 * 60 * 60 * 24)
        );

        notices.push({
          version,
          sunsetDate: data.sunsetDate,
          daysRemaining: daysUntilSunset,
          latestStableVersion: this.getLatestStable()?.version
        });
      }
    }

    return notices;
  }

  // 版本比较
  compareVersion(v1, v2) {
    const parts1 = v1.split('.').map(Number);
    const parts2 = v2.split('.').map(Number);

    for (let i = 0; i < 3; i++) {
      if (parts1[i] > parts2[i]) return 1;
      if (parts1[i] < parts2[i]) return -1;
    }
    return 0;
  }

  getMajorVersion(version) {
    return parseInt(version.split('.')[0]);
  }

  getMinorVersion(version) {
    return parseInt(version.split('.')[1]);
  }
}

总结

API 文档自动化是提升开发效率的重要手段,核心价值包括:

建议在项目初期就建立完善的 API 文档体系,并集成到 CI/CD 流程中。

← 下一篇:Koa.js 分布式事务解决方案与实践