接口向前兼容的演变思路.
假设接口 /api/group/users 以 GET 协议返回如下内容,意为:全量返回组内用户。
1 | { |
现需要提供分页查询的功能,该如何实现?
分页查询的响应格式很多,也很简单,例如:
1 | { |
查询 API 也很容易构建,例如 /api/group/users?page=1&limit=4,同样保持 GET 协议。
一切很简单,但这就够了吗?
这可能会带来 BREAKING CHANGE。
如果 page 和 limit 后端给予默认值,比如 1 和 4,BREAKING CHANGE 就实际发生了。
这会导致低版本客户端无法正常使用,常见于后端更新发版、前端尚未发版、既有网站或移动端仍在使用时,异常发生。
而如果 page 和 limit 后端不给予默认值,并根据是否传递该参数来判断接口的版本,BREAKING CHANGE 就不会发生:
1 | # querys, request params, make it like a dict type for a example |
现在,接口既能满足曾经的功能,又能额外提供分页查询的功能,够了吗?
够不够的关键点在于,是否需要丢弃全量查询的功能。
如果本就是为了丢弃全量查询,用分页查询来替代,那这里是不够的。
矛盾:要丢弃全量查询的功能,为何还要避免 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):
- 当版本号处于
1.3.2和N.0.0之间时,需要保留全量查询和分页查询的功能,伪代码如上。 - 当版本号处于
N.0.0和M.0.0之间时,后台输出警告,提示接口仍然被使用。 - 当版本号大于
M.0.0时,后端只提供分页的功能,page 和 limit 给予默认值,不再提供全量查询。
到现在为止,够了吗?
如果把版本号判断,外加数据查询,都放在接口逻辑内部,功能上能满足。
但,还可以解耦。
以 Python 相关的 Web 框架为例,甚至可以把该接口的实现放到 URL 装载期间,不同版本时,挂载不同的函数处理入口。
甚至,django-rest-framework 这类的框架,提供了更简易的接口版本限制的功能。
至此,已经完成了分页查询,有计划丢弃(或者不丢弃),那,够了吗?
既然版本号的判断和数据查询,我们需要解耦,那么两个明显不同的功能(一个全量,一个分页)放到一个接口里面,也是可以拆开的。
/api/group/users 提供全量查询
/v2/api/group/users 提供分页查询
然后各自可以独立地通过版本号,进行限制。