「Ming」是知乎内部一个 Mock 接口及文档管理平台。
它设计简洁, 使用方便, 解决了过去 Mock 接口和文档缺乏管理, 存放混乱, 以及 Mock 接口和文档长时间版本落后于线上接口的问题。
虽然业界也有很多类似的开源平台工具, 但「Ming」的设计实现从团队自身实际出发, 更加精简,符合团队的需要。
背景
在「Ming」诞生前, Mock 接口与接口文档缺乏统一管理, 散落在各种内部系统中,存在一些弊端 。
接口文档缺乏统一管理
有的团队喜欢把接口文档写在内部协作文档平台里, 有的团队喜欢写在 git 代码仓库里, 它们的存放目录及目录深度各有不同。
当后来人想要看接口的历史文档时, 简直无从查起, 许多文档就这样遗失了。
Mock 接口缺乏统一管理
Mock 接口一般是由前端写在自己代码仓库里, 写一些 HTTP 服务的 Node.js 代码, 生成简单的 Mock 接口。
这样做带来的问题首先是繁琐, 为了使用 Mock 接口, 需要经历: 启动本地 Mock 服务, 切换本地开发代理, 修改 Mock 接口代码等操作, 所以编写 Mock 接口这个环节往往是能省就省的;
另外还有的问题同样是历史接口无从查起, 时间长了没有人记得那些 Mock 接口代码是做什么的。
Mock 接口与文档落后于线上接口
需求一旦开发完毕并上线, 相应 Mock 接口和接口文档就会变得无人打理, 开始慢慢被人们遗忘了。
之后需求迭代, 开发人员很有可能忘记更新 Mock 接口和文档, 使得它们与线上接口越发脱节严重, 成为历史的垃圾堆。
接口变动后缺乏测试
工程师在修改旧接口时, 难免会有疏漏, 使接口产生一些 bug, 导致线上故障。 为了避免这样的问题发生, 往往需要付出很大的精力和时间成本编写测试脚本。
开发时应该写测试的道理谁都明白, 可实际情况中我们往往没有这样的时间和精力。
轻量级的解决方案
在业界, 为了解决上面那些在各公司普遍存在的问题, 已经有很多功能类似的接口管理平台出现,这些平台的侧重点各有不同。
「Ming」平台也有独特的侧重点, 那就是「轻量级」。
接口定义
定义接口的过程实际上就是在平台上创建接口的过程, 目的是为了最终生成「文档」和「 Mock 接口」。
但在如何定义接口这个问题上, 不同的接口管理平台所使用的方案是不同的。
大部分接口管理平台将接口定义理解为产出一份结构化的接口描述 Schema
JSON Schema
有些平台是使用 JSON Schema, 规定每个字段是什么类型, 有哪些可选值等等详细描述
一份 JSON Schema 的结构如下:
非常详细, 但太冗长了, 它所表达的内容其实就是一个包含三个数字类型字段 { type, created_at, updated_at } 的 Object 结构。
只要结构再稍微复杂一点, 我们一眼看上去, 绝对没法在短时间內理解, 这不是适合人阅读的语言, 它适合机器使用。
OpenAPI
有些平台推动维护一种叫做 OpenAPI 的规范, 事无巨细地描述了接口的所有信息, 比起 JSON Schema, OpenAPI 规范想要描述的内容范围要更加广泛, 包括 Header Server 等等。
一份描述接口返回值的 OpenAPI 内容如下:
可以看到, OpenAPI 中甚至有 $ref 即引用的功能, 其功能之强大, 规范之详细可以预期。
这些规范的确非常详细, 但却使得接口的定义无比复杂了, 每个接口的定义都长篇累牍, 可读性很差。
当我们想在使用这些定义规范的平台中创建一个接口, 究竟要花费多大的精力? 大概有以下三种思路吧:
熟悉语法然后手写 Schema
操作一个表单, 不断增加字段, 填写字段的类型, 描述, 枚举, 是否 required, 是否可为 null, 值的合法范围等等
接口返回值(JSON)自动生成对应的 JSON Schema
第一种熟悉语法是不可能的, 我们不能保证人人都学会它的语法规则。
第二种填写表单虽然基本不需要懂语法了, 但却过于繁琐, 不停地在鼠标点击和键盘输入之间切换, 消耗大量精力。
并且事实上, 等把接口写完后会发现, 这个接口定义可能比我们最终实现的接口功能还更加详细更加规范, 容易脱离实际。
第三种自动生成呢? 其实「返回值(JSON)」所携带的信息是要比 JSON Schema 少的, 因此这样自动生成的 JSON Schema 一定会信息失真, 仍需要手动修补。
接口返回值即定义
「Ming」的接口定义很简单, 不会对响应体和请求体做复杂抽象。
因此在「Ming」平台上创建一个接口的速度非常迅速:
首先在表单中填写接口路由, 接口名称, 接口描述等必要的基本信息;
然后在 JSON 代码编辑器中贴上接口返回值, 写点注释, 点击提交;
一个接口就创建完成了。
创建接口
注意: 此处的 JSON 代码编辑器其实是 JSON5 编辑器, 因此可以写注释。
权衡利弊
「JSON Schema」和「Open API」作为接口定义更加详细严格, 符合通用标准, 但编辑和创建时过于繁琐, 使用效率较低, 很容易出现工程师为了快速投入使用而过于赶工的情况。
「Ming」的接口定义使用起来更加简单, 效率更高, 但没有严格的接口定义约束。
我相信 OpenAPI 和 JSON Schema 一定是将来会越来越广泛应用的标准, 各种接口管理平台也会尽最大努力减少编写接口定义时的所有不便之处。
但对于一些对接口文档定义不需要这么严格详细的场景, 简单易用, 快速产出成果才是更适合它们的选择, 因此类似于「Ming」这样弱化接口定义的方案会一直存在。
文档生成与 Mock 生成
不同的接口定义方式决定了文档生成和 Mock 生成的过程, 各有利弊。
文档
如果使用 OpenAPI 或 JSON Schema 作为接口定义, 生成的文档会更加详细。
但缺点是类似 JSON Schema 的定义并不能反映接口实际返回的内容, 比如只知道某个字段是字符串类型, 却不知道这个字符串具体是什么样子, 某些情况下可能不够直观。
「Ming」使用实际返回值作为接口定义, 因此它所生成的文档中, 各字段并没有那么详细的定义, 仅有的只是一些注释。
优点是更加直观, 接口字段的实际内容会更容易让人把它和实际业务联系起来。
Mock
JSON Schema 和 OpenAPI 的接口定义不能解决 Mock 接口生成的问题, 需要引入另一套 Mock 生成规则, 比如创建一种 DSL, 或者写 js 代码, 生成 Mock 数据。
「Ming」使用实际返回值作为接口定义, 天然支持生成 Mock 接口, 非常便利。
自动化测试
不同类型的测试有不同的目的, 这里暂时只讨论两种测试:
可用性测试: 判断接口是否正常工作, 如通过测试脚本中的一些断言, 避免引起线上故障
文档同步自动检测: 比较接口定义和线上接口实际返回值, 得出接口文档是否落后于线上接口的结论
可用性测试
大多数接口管理平台提供的测试功能都是可用性测试,使用方式多为:
创建测试任务, 预设一些 Header 如 Cookie, 然后在测试任务中编写断言语句, 让它每隔一小段时间请求线上接口, 检查是否能通过测试脚本。
这种测试需要手动创建, 手动编写测试代码,手动开启, 编写测试脚本的成本较高。
文档同步自动检测
似乎很多接口管理平台并没有「文档同步检测」的概念, 或者说是没有把它拿出来作为一项独立的功能, 更多的是和「可用性检测」混在一起, 界限模糊。
事实上, 在已知接口定义的前提下, 「文档同步检测」是可以自动进行的, 当接口文档创建成功, 并且接口也已经联调结束, 上线投入使用后, 接口平台就可以自动开启这项检测了。
「Ming」使用了接口实际返回值作为接口定义, 如果用来比较线上接口是否同步, 这点信息肯定是不够的。
因此「Ming」在自动创建检测任务时, 使用接口定义中的 JSON 返回值自动生成了一份 JSON Schema, 用于比较这份 JSON Schema 是否落后于线上接口。
权衡利弊
其实「文档同步自动检测」和「可用性测试」并不冲突。
无论哪种接口定义方式,「JSON Schema」还是「OpenAPI」, 或者是「Ming」的「接口返回值即定义」, 都可以实现这两种测试。
由于「Ming」比较接口文档是否同步时使用的 JSON Schema 是自动生成的( 虽然可以通过 JSON 中的「注释」辅助生成更准确的 JSON Schema, 但编写注释不是强制的),
因此它的准确性会有所下降。
Mock 接口的不同状态
实际情况中, 某些接口的返回值是十分复杂的, 会根据请求参数的不同, 或是数据库中数据当前状态的不同, 返回不同的响应体。
为了满足这个需求, 大体上有两种方案:
给接口增加条件语句的功能, 通过条件语句或 DSL 设置在不同情况下的返回值
将接口管理平台的数据拷贝多份, 针对每一份拷贝运行一个沙盒服务, 修改沙盒中的接口定义使其返回不同的数据
实现条件语句
很多接口管理平台都实现了这样的功能, 为 Mock 接口编写条件分支逻辑
举例如下:
像这样直接写 js 代码使得 Mock 接口返回不同的响应体, 状态码等。
这样的功能的确很强大, Mock 接口有了像真实接口一样的动态逻辑, 然而, 随之而来的事情不会像我们想象的那么简单。
使用学习成本过高
首先是语法学习的困难, 至少要学会 js 语法。
另外上述代码中使用了一些不知从何而来的变量和字段, 如 cookie, cookie._type, mockJson, mockJson.data。
想必平台需要提供一份编写代码的文档, 列出所有可以使用的变量及其详细结构, 所有可以使用的功能函数, 所有字段代表的意义。
使用调试成本失控
代码是需要调试的, 没有了本地 IDE, 代码运行的过程开发完全看不到。
因此, 一旦代码中出了错, 调试就变得极为困难, 使用者不明白代码错在哪。
毕竟, 我们是在部分地实现真实接口的逻辑, 出错是难免的。
这会使得这项功能难以使用。
维护成本失控
代码组合是无穷的,
实际情况中使用者写出任何代码都不用奇怪, 为了响应使用者的 oncall 可能会付出很大的人力消耗。
需求是无穷的。
虽然是部分地实现真实接口的逻辑, 但这不代表需求会很简单, 为了满足使用者通过条件语句实现接口各种状态的需求, 需要支持的函数方法和变量会越来越多, 无穷无尽,
这也将带来非常大的维护成本。
沙盒化平台
使用者可以随时启动一个临时的「Ming」沙盒服务, 在沙盒中修改接口返回值, 状态码将不会影响「Ming 」主服务的数据。
一般需要用到这个功能的人是前端和客户端, 他们把沙盒中对应的接口地址在代码中写好, 然后修改沙盒中相应接口, 就可以调试同一接口的不同状态了。
「Ming」所采用的正是这样的方案, 上面所提到的学习成本, 使用成本, 维护成本的问题都不存在。
权衡利弊
「条件语句」方案的优点是让 Mock 接口有能力实现真实接口的部分动态逻辑, 在某些情况下会比较便利,
但缺点是带来了极大的「学习成本」「使用成本」「维护成本」。
「Ming」所采取的「沙盒化」方案的优点正是避免了上述三个方面的成本,
缺点是遇到少数实在是需要 Mock 接口具备动态逻辑的场景时不够灵活。
系统实现
最小原型
前端使用后台项目统一开发框架及组件库快速搭建。
后端使用 Node.js 做接口服务, 利用 Redis 持久化存储做数据库。
Node.js 服务从 Redis 获取所有接口数据, 加载到内存中, 通过匹配路由返回对应的 Mock 接口数据。
但这种每次响应 Mock 请求都把所有数据加载到内存中的方式性能很差,
所以我们增加了缓存优化, 每隔 10s 重新从 Redis 加载所有 Mock 接口数据到内存中, 接口响应时使用缓存中的数据, 接口响应速度大大提升。
相关截图
项目列表
文档中心
接口文档
前端项目集成
后端开发人员开始使用接口平台后, 前端只需把 Mock 地址写进项目代码中就能使用 Mock 接口进行开发了。
但这样不够安全, 前端有极大的把 Mock 接口地址误上线的风险。
因此我们想到接口管理平台可以和前端项目集成, 在线上接口和 Mock 接口之间建立自动映射。
类似 Nginx 反代理路由匹配规则, 我们以「路由长度更长」和「路由中存在参数」为高优先级排序规则, 对所有 Mock 接口进行排序, 逐个匹配线上地址, 返回相应 Mock 接口。
前端项目本地开发则根据环境变量自动切换发请求时的域名, 这样一来, 前端开发时便可放心地使用线上接口的地址。
接口校验
「Ming」在自动创建检测任务时, 使用接口定义中的 JSON 返回值自动生成一份 JSON Schema, 用于比较这份 JSON Schema 是否落后于线上接口。
虽然 JSON Schema 是通过接口返回值自动生成的, 但使用者可以在 JSON 代码中添加「格式注解」辅助 JSON Schema 的生成。
「格式注解」举例如下:
这里的 @optional 就是表示这个字段是可选的, 这行注释会影响最终生成的 JSON Schema 结构。
定时发送接口
通过消息队列与定时器服务实现
定时校验消息队列
校验任务状态机
校验失败后给出修改建议
校验失败可能是接口本身错了, 也可能是自动生成的 JSON Schema 有误。
如果接口本身错了, 那么说明线上接口有误, 提醒相关负责人查看失败原因, 关注接口近期变动。
如果 JSON Schema 错了, 则说明可能需要「格式注解」辅助调整自动生成 JSON Schema 的结果。
所谓的给出修改建议就是提示出可能有效的「格式注解」。
相关截图
接口校验失败时的通知
沙盒化
启动沙盒服务
沙盒服务与主服务的构建部署流程基本相同, 如有区别可使用不同的构建脚本加以区分。
沙盒服务与主服务的数据隔离
由于数据存储于 Redis, 可通过在 Redis Key 前添加不同的前缀来与主服务数据加以区分。
数据拷贝时所进行的复制并覆盖操作比较危险, 因此需对这部分逻辑增加单元测试和运行时校验。
总结
本文介绍了知乎内部使用的 Mock 接口与文档管理平台 Ming , 它的特点在于轻量级, 关注使用效率和维护成本, 避免引入过于复杂的产品逻辑。
比起业界一些开源平台, 「Ming」的功能还不够完善,
在「类似 postman 的接口测试」「可用性测试」「压力测试」「与后端接口开发的深度集成」「公共业务 JSON 结构的引用」「简单动态逻辑支持」 等方面还有很多拓展的空间。
如果你感兴趣, 欢迎在评论区交流。
评论