随着移动客户端的流行,后端系统需开放越来越多的API来供客户端使用。API文档的编写和管理是一个挑战,随着API不断变化,文档必须及时更新,但编写文档也是个不小的负担。一个比较好的做法(Best Practice),就是将文档放在代码里,开发人员编写代码时同时修改文档。然后通过工具从代码中抽出文档,并生成方便用户阅读的格式。此类工具早已存在,比如Java中的javadoc。这里我们要介绍一个非常轻量级的,适用于几乎所有流行语言的,针对Restful API的文档自动生成工具-apiDoc。

工具安装

和所有的技术介绍一样,我们先从安装开始。apiDoc基于NodeJS实现,所以你需先有NodeJS及NPM环境。然后通过:

$ npm install apidoc -g

全局安装apiDoc。之后,你就可以执行下面的命令来生成文档:

$ apidoc -i src -o dest

该命令从当前工作目录的”src”子目录下读取所有代码文件,并从中抽取apiDoc的文档注释,然后生成HTML格式的文档,并保存在”dest”子目录下。执行完后,用户就可以打开”dest”下的”index.html”来阅读文档。

“apidoc”命令支持许多参数,常用的有:

  • -t template
    根据”template”子目录下的模板生成文档,缺省时采用apiDoc的默认模板。用户也可以编写自己的模板。关于模板的细节,本文不多介绍了。

  • -f ".*\\.py$"
    只解析Python代码。-f后的参数按正则匹配,可以设置多个-f参数。

  • -h
    显示该命令帮助信息。

配置文件

运行apidoc前,你需先有一个配置文件,放在代码根目录下(如上例中的src目录),取名为”apidoc.json”。下面是一个标准配置文件的例子:

{
  "name": "User API Document",
  "version": "0.1.0",
  "description": "A sample of User API document generated from apiDoc",
  "title": "User API",
  "url" : "http://localhost:5000",
  "sampleUrl": "http://localhost:5000",
  "header": {
    "title": "Overview",
    "filename": "header.md"
  },
  "footer": {
    "title": "Copyright",
    "filename": "footer.md"
  },
  "template": {
    "withCompare": true,
    "withGenerator": true
  }
}

简单介绍下:

  • name, version, description
    这三个配置项的内容会显示在生成文档的最上方,其中name即整个文档的大标题。

  • title
    网页的标题,显示在浏览器标签上。

  • url
    文档中每个API地址的前缀。

  • sampleUrl
    当有此项时,每个API文档的最后会有Sample Request测试部分。该配置项是测试API地址的前缀。

  • header, footer
    页面的头和尾。当多个页面都有相同的头和尾时,这个配置项就非常有用。子配置项中

    • title
      页面左边菜单栏中显示的标题。
    • filename
      指向页头或页尾的模板文件,apiDoc使用的是Markdown文件。
  • template

    • withCompare
      生成的文档有版本比较功能。后面会介绍。默认为”true”。
    • withGenerator
      生成的文档页尾带有一段文字,表示这个文档是由apiDoc生成的,加上这个代表对作者的尊重吧。默认为”true”。

让我们简单写下页头和页尾的Markdown文件,并将它们同”apidoc.json”放在一起,即”src”目录下。

header.md

## Overview
Below is the API doc of User APIs.

footer.md

Copyright © 2014 - 2015 [www.bjhee.com](http://www.bjhee.com/) All Rights Reserved

API文档注释

接下来,我们就要介绍最主要的部分了,就是API代码上的文档注释。这个注释一般会加在对外开放的API函数上方。这里,我们要用到本博客文章Flask扩展系列(一)–Restful中的例子。这个例子是Python代码写的,其提供了user对象的增删改查API。Python的注释是由三个双引号"""来包括,如果是Java, JS或PHP,则用/** */来包括。我们就拿单个user的GET方法来举例子:

class User(Resource):
    """
    @api {get} /users/:user_id Request User Information
    @apiVersion 0.1.0
    @apiName GetUser
    @apiGroup User
    @apiPermission admin

    @apiDescription API to get the user information.

    @apiExample Example usage:
    curl -i http://localhost:5000/users/2

    @apiParam {Number} user_id The user's unique ID.

    @apiSuccess {String} name Name of the User.
    @apiSuccessExample {json} Success-Response:
        HTTP/1.1 200 OK
        {
            "name": "Tom"
        }

    @apiError UserNotFound The <code>user_id</code> of the User was not found.

    @apiErrorExample {json} Error-Response:
        HTTP/1.1 404 Not Found
        {
            "error": "UserNotFound",
            "message": "User {user_id} doesn't exist"
        }

    @apiSampleRequest http://localhost:5000/users/:user_id
    """
    def get(self, user_id):
        abort_if_not_exist(user_id)
        return USER_LIST[user_id]

可以看到,文档注释主要是由一系列以@符号开头的参数组成,让我们来逐一介绍:

  • @api {get} /users/:user_id Request User Information
    最主要的参数,{get}定义了HTTP请求是GET,API地址是/users/:user_id,文档中API的名称是Request User Information

  • @apiVersion 0.1.0
    API的版本号,默认显示在API名称的右方。该参数可用来在不同的版本之间做比较,后面会介绍。

  • @apiName GetUser
    API名称,不影响文档。

  • @apiGroup User
    API分组名,文档内容中和菜单栏中同一组的API会在一同显示,方便阅读。

  • @apiPermission admin
    API的访问权限,文档中默认会API地址下面显示。没有权限要求的话,此项可以省略。

  • @apiDescription API to get the user information.
    API的详细描述,默认显示在API名称的下方。

  • @apiExample Example usage:
    API调用示例,该参数的下一行就是示例的内容,直到有空行结束。可以定义多个@apiExample,默认在文档中会以标签形式列出,标签名就是”Example usage:“。

  • @apiParam {Number} user_id The user’s unique ID.
    API参数字段介绍,”{Number}“定义了字段类型,”user_id”是字段名称,后面则是字段描述。可以定义多个@apiParam字段。

  • @apiSuccess {String} name Name of the User.
    API成功后返回的字段,如同@apiParam,”{String}“定义了字段类型,”name”是返回字段名称,后面则是字段描述。可以定义多个@apiSuccess字段。

  • @apiSuccessExample {json} Success-Response:
    显示一个API成功返回后Response响应的示例,”{json}“代表响应体是JSON类型。该参数的下行就是响应体内容,直到有空行结束。可以定义多个@apiSuccessExample,默认在文档中会以标签形式列出,标签名就是”Success-Response:“。

  • @apiError UserNotFound User was not found.
    API发生错误后的返回,”UserNotFound”是错误名称,后面则是错误描述。可以定义多个错误返回。

  • @apiErrorExample {json} Error-Response:
    显示一个API错误返回后Response响应的示例,”{json}“代表响应体是JSON类型。该参数的下行就是响应体内容,直到有空行结束。可以定义多个@apiErrorExample,默认在文档中会以标签形式列出,标签名就是”Error-Response:“。

  • @apiSampleRequest http://localhost:5000/users/:user_id
    文档提供的API Sample测试的地址。其实在”apidoc.json”中配过”sampleUrl”项后,此参数即可省去,除非这个API的测试URL比较特殊,需特别指定。

好,现在让我们把这个代码放在”src”目录下,然后执行命令

$ apidoc -i src -o dest

打开”dest”目录下的”index.html”文件,看看我们的API文档,是不是很不错?要不动动手,把这个代码的其他API文档注释一起加上吧?

参数信息重用

很多时候,不同的API有着相同的文档内容,比如相同的成功响应示例,或者相同的错误返回。当API非常多时,一个变化需要改动多处的API文档,很麻烦。能不能像写C代码一样,定义一个宏,然后在API文档注释中引用这个宏呢?答案是有,在apiDoc中,这叫继承(inherit)。首先,你要在源代码注释中用@apiDefine定义这个宏。比如,下面的注释,就是把上例中UserNotFound错误及其响应示例定义在一个名叫UserNotFoundError的宏中。此后,所有需要使用这段文档的地方,就通过@apiUse UserNotFoundError引入这个宏即可。

class User(Resource):
    """
    @apiDefine UserNotFoundError

    @apiError UserNotFound The <code>user_id</code> of the User was not found.

    @apiErrorExample {json} Error-Response:
    HTTP/1.1 404 Not Found
    {
        "error": "UserNotFound",
        "message": "User {user_id} doesn't exist"
    }
    """

    """
    @api {get} /users/:user_id Request User Information
    ...
    @apiUse UserNotFoundError
    """
    def get(self, user_id):
        # Code here

    """
    @api {delete} /users/:user_id Delete a User
    ...
    @apiUse UserNotFoundError
    """
    def delete(self, user_id):
        # Code here

当宏定义比较多时,怎么管理呢?我们可以把宏定义挪到代码以外去。让我们在”src”根目录下建立一个文件”_apidoc.py”。然后把上面UserNotFoundError的定义移到这个文件中。

"""
    @apiDefine UserNotFoundError
    @apiVersion 0.1.0
    @apiError UserNotFound The <code>user_id</code> of the User was not found.
    @apiErrorExample {json} Error-Response:
        HTTP/1.1 404 Not Found
        {
            "error": "UserNotFound",
            "message": "User {user_id} does not exist"
        }
 """

执行下apidoc命令,看下效果吧。是不是和上例中一样?实际项目中,建议尽可能把所有的宏定义都写在”_apidoc.py”中。还有人会问,那这个文件太大了怎么办?你可以分出多个文件,记得给一个有意义的命名即可。这个文件也不一定是python文件,你的代码是什么语言,就用什么文件,方便其与代码中的文档注释相互拷贝。细心的朋友们还会注意到,上例的宏定义中加了@apiVersion版本信息,这说明这段宏只对特定版本有效。

定义访问权限

上面我们讲到@apiPermission参数可以定义当前API的访问权限。如果文档的读者想了解更多关于这个权限的信息呢?我们可以通过@apiDefine来定义每个权限的详细信息。

"""
    @apiDefine admin Admin access rights needed.
    The admin represents the administration privilege of the application.

    @apiVersion 0.1.0
"""

加上这段定义后,在文档出现”admin”权限名时,后面会有个“i”符号。鼠标点击后就会弹出”admin”的详细信息。这段定义一般也放在”_apidoc.py”中。

历史版本比较

这是我认为apiDoc最酷的功能,当API更新时,不用再耗费体力查找到底哪里改动了,直接在文档上比较下就知道了。当你在文档注释中定义多个版本后,生成后的文档页面中版本号是一个下拉列表。你点击其中一个历史版本,页面就会出现比较信息,通过高亮,删除线等来告诉你哪里被改动过了(用过代码比较工具的朋友们肯定很熟悉)。

API Diff

怎么来添加这历史版本信息呢。其实你要做的事情很简单,就是每次API文档改动时,把原来的API文档注释复制备份到”_apidoc.py”中,然后再修改。修改完后别忘了更新版本号就行。这里我们运行个例子,在”_apidoc.py”中加上一个老版本的GET方法注释:

"""
 @api {get} /user/:user_id Request User Information
 @apiVersion 0.0.1
 @apiName GetUser
 @apiGroup User
 @apiPermission admin

 @apiDescription Here you can describe the function.
 Multilines are possible.

 @apiParam {String} id The Users-ID.

 @apiSuccess {String} id         The Users-ID.
 @apiSuccess {Date}   name       Fullname of the User.

 @apiError UserNotFound   The <code>id</code> of the User was not found.
"""

然后运行下,你就可以做版本比较了。@apiDefine宏定义也可以有历史版本,要注意,这个版本号是同API注释的版本号相匹配的。比如,假设我们在上例GET方法”0.0.1”版本注释中加入@apiUse UserNotFoundError,这个时候执行apidoc命令会报错。因为它找不到”0.0.1”版本的@apiDefine UserNotFoundError定义(前面的例子中我们指定的版本号是@apiVersion 0.1.0)。所以,必须要补上”0.0.1”版的@apiDefine UserNotFoundError命令才能运行成功。

apiDoc的主要功能讲完了,这些足够让你的项目拥有一个专业的文档了吧。如果你对apiDoc感兴趣想了解更多细节的话,可以访问它的官网apidocjs.com。本例中的代码可以在这里下载