跳到正文
返回

一个HTTP接口的变更

编辑此页

接口向前兼容的演变思路.

假设接口 /api/group/users 以 GET 协议返回如下内容,意为:全量返回组内用户。

{
  "data": [
    { "id": 1, "name": "a" },
    { "id": 2, "name": "b" },
    { "id": 3, "name": "c" },
    { "id": 4, "name": "d" }
  ]
}

现需要提供分页查询的功能,该如何实现?

分页查询的响应格式很多,也很简单,例如:

{
  "current_page": 1,
  "pages": 10,
  "limit": 4,
  "data": [
    { "id": 1, "name": "a" },
    { "id": 2, "name": "b" },
    { "id": 3, "name": "c" },
    { "id": 4, "name": "d" }
  ]
}

查询 API 也很容易构建,例如 /api/group/users?page=1&limit=4,同样保持 GET 协议。

一切很简单,但这就够了吗?

这可能会带来 BREAKING CHANGE。

如果 page 和 limit 后端给予默认值,比如 1 和 4,BREAKING CHANGE 就实际发生了。

这会导致低版本客户端无法正常使用,常见于后端更新发版、前端尚未发版、既有网站或移动端仍在使用时,异常发生。

而如果 page 和 limit 后端不给予默认值,并根据是否传递该参数来判断接口的版本,BREAKING CHANGE 就不会发生:

# querys, request params, make it like a dict type for a example
if "page" in querys or "limit" in querys:
    page = querys.get("page", 1)
    limit = querys.get("limit", 4)
    # do page query
    return {
        "pages": 10,
        "current_page": page,
        "limit": limit,
        "data": [
            { "id": 1, "name": "a" },
            { "id": 2, "name": "b" },
            { "id": 3, "name": "c" },
            { "id": 4, "name": "d" }
        ]
    }
else:
    # do all query
    return {
        "data": [
            { "id": 1, "name": "a" },
            { "id": 2, "name": "b" },
            { "id": 3, "name": "c" },
            { "id": 4, "name": "d" }
        ]
    }

现在,接口既能满足曾经的功能,又能额外提供分页查询的功能,够了吗?

够不够的关键点在于,是否需要丢弃全量查询的功能。

如果本就是为了丢弃全量查询,用分页查询来替代,那这里是不够的。

矛盾:要丢弃全量查询的功能,为何还要避免 BREAKING CHANGE?

答:有计划的丢弃。

语义化的版本,如 m.n.j,j 通常应对 fix,即修订 BUG;n 通常对应 feat,即新增功能。

规范一些的项目中,n 和 j 变化,一般是向后兼容的,即兼容低版本。

但 m,通常表示存在一些不兼容的改动,不能完全向后兼容。

这里的不兼容,可能是 fix bug 时发生的,也可能是完成 feat 时带来的。

以 commit 规范为例,feat!: xxxxx 这里的 ! 就代表本次 commit 引入了不兼容改动。

当通过版本管理工具,比如 standard-version 来自动根据 commit 生成 tag 时,当发现 commit 中标注不兼容时,就会自动跃迁版本号。比如 1.3.2,就会变成 2.0.0。

那进一步的改善空间为(假设当前版本号为 1.3.2):

到现在为止,够了吗?

如果把版本号判断,外加数据查询,都放在接口逻辑内部,功能上能满足。

但,还可以解耦。

以 Python 相关的 Web 框架为例,甚至可以把该接口的实现放到 URL 装载期间,不同版本时,挂载不同的函数处理入口。

甚至,django-rest-framework 这类的框架,提供了更简易的接口版本限制的功能。

至此,已经完成了分页查询,有计划丢弃(或者不丢弃),那,够了吗?

既然版本号的判断和数据查询,我们需要解耦,那么两个明显不同的功能(一个全量,一个分页)放到一个接口里面,也是可以拆开的。

/api/group/users 提供全量查询

/v2/api/group/users 提供分页查询

然后各自可以独立地通过版本号,进行限制。


编辑此页
分享这篇:

上一篇
Less is More 具象化