swagger-decorator:注解方式为 Koa2 应用动态生成 Swagger 文档

953 阅读5分钟
原文链接: zhuanlan.zhihu.com

swagger-decorator:注解方式为 Koa2 应用自动生成 Swagger 文档 从属于笔者的服务端应用程序开发与系统架构,记述了如何在以 Koa2 与 koa-router 开发服务端应用时,通过自定义 swagger-decorator 库来实现类 Spring-Boot 中注解方式动态生成 Swagger 标准的接口文档。

swagger-decorator:注解方式为 Koa2 应用动态生成 Swagger 文档

目前我司服务端应用程序框架主要采用了 Java Spring 与 Node.js,而因为今年有很多的调研阶段的产品线 Demo 发布,持续部署、接口文档以及线上质量监控这三个问题愈发突出。本文则主要针对接口文档的实时发布进行一些探讨;在前后端分离的今天,即使是由单人纵向负责某个业务流,也需要将前后端交互的接口规范清晰地定义并且发布,以保证项目的透明性与可维护性。理想的开发流程中,应当在产品设计阶段确定好关键字段命名、数据库表设计以及接口文档;不过实际操作中往往因为业务的多变性以及人手的缺失,使得接口的定义并不能总是实时地在项目成员之间达成一致。如果要让开发人员在更改接口的同时花费额外精力维护一份开发文档,可能对于我司这样的小公司而言存在着很大的代价与风险。软件开发中存在着所谓 Single Source of Truth 的原则,我们也需要尽量避免文档与实际实现的不一致造成的团队内矛盾以及无用的付出。综上所述,我们希望能够在编写后台代码、添加注释的同时,能够自动地生成接口文档;笔者比较熟悉 Spring 中以注解方式添加 Swagger 文档的模式,不过 Java 库的抽象程度一般较高,用起来也不怎么顺手。笔者在编写我司 node-server-boilerplate 根据自己的想法设计了 swagger-decorator。此外,项目中使用 Flow 进行静态类型检测,并且遵循我司内部的 JavaScript 编程样式指南

我们可以使用 npm 或者 yarn 安装 swagger-decorator,需要注意的是,因为使用了注解,因此建议是配置 Webpack 与 Babel,不熟悉的同学可以直接参考 node-server-boilerplate

$ yarn add swagger-decorator

# 依赖于 Babel 的 transform-decorators-legacy 转换插件来使用 Decorator
$ yarn add transform-decorators-legacy -D

安装完毕之后,我们需要对项目中使用的路由进行封装。目前笔者只是针对 koa-router 中的路由对象进行封装,未来若有必要可以针对其他框架的路由解决方案进行封装。我们首先需要做的就是在路由定义之前使用 wrappingKoaRouter 函数修饰 router 对象:

import { wrappingKoaRouter } from "swagger-decorator";

...

const Router = require("koa-router");

const router = new Router();

wrappingKoaRouter(router, "localhost:8080", "/api", {
  title: "Node Server Boilerplate",
  version: "0.0.1",
  description: "Koa2, koa-router,Webpack"
});

//定义默认的根路由
router.get("/", async function(ctx, next) {
  ctx.body = { msg: "Node Server Boilerplate" };
});

//定义用户处理路由
router.scan(UserController);

该函数的参数说明如下,对于 info 的结构参考这里

/**
 * Description 将 router 对象的方法进行封装
 * @param router 路由对象
 * @param host API 域名
 * @param basePath API 基本路径
 * @param info 其他的 Swagger 基本信息
 */
export function wrappingKoaRouter(
  router: Object,
  host: string = "localhost",
  basePath: string = "",
  info: Object = {}
) {}

值得一提的是,在封装 router 时,笔者自定义了 scan 方法,其能够根据自动遍历目标类中的自定义方法,有点类似于 Java 中的 ComponentScan:

/**
* Description 扫描某个类中的所有静态方法,按照其注解将其添加到
* @param staticClass
*/
router.scan = function(staticClass: Function) {
    let methods = Object.getOwnPropertyNames(staticClass);
    
    // 移除前三个属性 constructor、name
    methods.shift();
    methods.shift();
    methods.shift();
    
    for (let method of methods) {
      router.all(staticClass[method]);
    }
};

准备工作完成之后,我们即可以开始定义具体的接口控制器;笔者不喜欢过多的封装,因此这里选用了类的静态方法来定义具体的接口函数,整个 Controller 也只是朴素函数。下面笔者列举了常见的获取全部用户列表、根据用户编号获取用户详情、创建新用户这几个接口的文档注释方式:

import {
  apiDescription,
  apiRequestMapping,
  apiResponse,
  bodyParameter,
  pathParameter,
  queryParameter
} from "swagger-decorator";
import User from "../entity/User";

/**
 * Description 用户相关控制器
 */
export default class UserController {
  @apiRequestMapping("get", "/users")
  @apiDescription("get all users list")
  @apiResponse(200, "get users successfully", [User])
  static async getUsers(ctx, next): [User] {
    ...
  }

  @apiRequestMapping("get", "/user/:id")
  @apiDescription("get user object by id, only access self or friends")
  @pathParameter({
    name: "id",
    description: "user id",
    type: "integer"
  })
  @queryParameter({
    name: "tags",
    description: "user tags, for filtering users",
    required: false,
    type: "array",
    items: ["string"]
  })
  @apiResponse(200, "get user successfully", User)
  static async getUserByID(ctx, next): User {
    ...
  }

  @apiRequestMapping("post", "/user")
  @apiDescription("create new user")
  @bodyParameter({
    name: "user",
    description: "the new user object, must include user name",
    required: true,
    schema: User
  })
  @apiResponse(200, "create new user successfully", {
    status_code: "200"
  })
  static async postUser(): number {
    ...
  }
}

在对接口注解的时候,我们需要用实体类指明返回值或者请求体中包含的参数信息,因此我们也需要使用 swagger-decorator 提供的 entityProperty 注解来为实体类添加描述。值得一提的是,这里我们支持直接将 Object 作为描述对象的返回值,算是避免了 Java 中的一大痛点。

// @flow

import { entityProperty } from "swagger-decorator";
/**
 * Description 用户实体类
 */
export default class User {
  // 编号
  @entityProperty({
    type: "integer",
    description: "user id, auto-generated",
    required: false
  })
  id: string = 0;

  // 姓名
  @entityProperty({
    type: "string",
    description: "user name, 3~12 characters",
    required: true
  })
  name: string = "name";

  // 朋友列表
  friends: [number] = [1];

  // 属性
  properties: {
    address: string
  } = {
    address: "address"
  };
}

对于没有添加注解的属性,swagger-decorator 会自动根据其默认值来推测类型。然后我们就可以正常地启动应用,swagger-decorator 已经自动地为 router 对象添加了两个路由,其中 /swagger 指向了 Swagger UI:

<img src="https://pic2.zhimg.com/v2-8ed55bc093d435bdfea814d63511d655_b.png" data-rawwidth="2436" data-rawheight="1546" class="origin_image zh-lightbox-thumb" width="2436" data-original="https://pic2.zhimg.com/v2-8ed55bc093d435bdfea814d63511d655_r.png">

而 /swagger/api.json 指向了 Swagger 生成的 JSON 文档:

<img src="https://pic2.zhimg.com/v2-2931f1bf8c7a1bb83df124a2ef756b09_b.png" data-rawwidth="968" data-rawheight="1516" class="origin_image zh-lightbox-thumb" width="968" data-original="https://pic2.zhimg.com/v2-2931f1bf8c7a1bb83df124a2ef756b09_r.png">

欢迎有兴趣的朋友提出 ISSUE、指导意见或者希望纳入的特性。