前不久我写过一篇用 OpenAPI 做 API 优先开发的泛泛而谈,这篇是具体版:我们团队正在试行的计划工作流,外加配套的 YAML 示例。
简单说,这套设计优先的流程每个 Sprint 走一遍:
- 为本 Sprint 的 API 工作定两名负责人,后端一名、前端一名。
- 两人对照本 Sprint 的需求编写(或更新)OpenAPI 接口定义,反复评审,直到双方都认可。
- 把接口定义提交到仓库。从这一刻起,它就是契约。
- CI 根据提交的接口定义拉起一个模拟服务器。
- 前端对着模拟服务器开发,后端同时实现真实的端点。
- 真实服务部署后,由它生成的规范文件就是下一个 Sprint 接口定义的起点。
事情的起因#
起因是一次回顾会。反馈说得很不客气:API 的规划几乎没有任何可见性。后端按自己对需求的理解去实现端点,前端事后才知道响应长什么样,分歧全都在集成阶段——也就是代价最高的时刻——才暴露出来。于是有了这个提案:把两边都拉进计划阶段,让 OpenAPI 接口定义(schema)成为一切的锚点。
流程#
每周选两名代表,后端一名、前端一名。他们的任务是吃透本次 Sprint 的需求,并让计划用的 OpenAPI 接口定义与之对齐。两人一起评审这份定义,把对不上的地方改掉。等这一步做完,在动手之前,双方就已经确切知道要造的是什么了。
修订定稿后,把接口定义提交到仓库,CI/CD 会根据这份 yaml 生成模拟 API。Sprint 结束时,再切一份覆盖下个 Sprint API 需求的新 yaml,循环往复。
工具备忘#
可视化 yaml 用 Swagger Editor 就够了:把文件传上去,就能看到渲染好的 Swagger 文档。
CI/CD 流水线需要能接收 yaml 并据此拉起一个模拟服务器。Stoplight 的 Prism 用的人很多,也支持 OpenAPI 3.1:
npm install -g @stoplight/prism-cli
prism mock openapi.yaml这样接口定义里的所有路径都会在 http://127.0.0.1:4010 上提供服务,返回的就是定义里的 examples,比如请求 GET /lists/10,拿到的就是附录里那条示例列表。它还会替你守住契约:示例声明了 API key,所以不带 X-API-Key 请求头的请求会直接收到 401,而这正是前端应该照着去做的行为。在容器里跑的话,记得加上 -h 0.0.0.0,外面才访问得到。openapi-mock 是个更轻量的替代品,会用随机数据填充响应。
拉起模拟服务器之前,最好先给接口定义做一遍 lint。契约写坏了,构建直接失败,总比让前端一头雾水强。Spectral 只要一行规则集就能跑起来:
npm install -g @stoplight/spectral-cli
echo 'extends: ["spectral:oas"]' > .spectral.yaml
spectral lint openapi.yaml最后,这份接口定义纯粹是计划产物。真实 API 部署之后,应该由各服务自己生成 openapi.json,下载下来再喂给下一轮计划。
为什么值得费这个劲#
因为争论被挪到了代价最低的地方。写接口定义的时候前后端工程师都在场,误解还没变成代码就被抓住了。API 需求一次谈妥,不用在 Sprint 中途反复扯皮;有了模拟服务器,前端立刻就能开工,不用干等;关键细节被漏掉的机会也小得多——它们得同时躲过两双眼睛和一份白纸黑字的契约才行。
这里没有任何革命性的东西。只是把计划做得足够具体,具体到机器能把它当成模拟服务还给你——事实证明,一份计划需要的具体程度,恰好就是这个程度。
附录A - yaml示例#
openapi: 3.1.0
info:
title: MyApp Sprint 3
description: Planning schema for sprint 3. Drives the mock server only.
version: 0.3.0
servers:
- url: https://api-mock.example.com/v1
tags:
- name: lists
description: Saved lists feature (PROJ-123)
security:
- api_key: []
paths:
/lists:
get:
tags: [lists]
summary: Return the current user's saved lists
operationId: getLists
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/List'
/lists/{listId}:
get:
tags: [lists]
summary: Find a list by ID
operationId: getListById
parameters:
- name: listId
in: path
required: true
schema:
type: integer
format: int64
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/List'
'404':
description: List not found
components:
schemas:
List:
type: object
required: [id, title]
properties:
id:
type: integer
format: int64
examples: [10]
title:
type: string
examples: [Weekend reading]
itemCount:
type: integer
examples: [7]
securitySchemes:
api_key:
type: apiKey
name: X-API-Key
in: header