3、接口文档与契约:Swagger/OpenAPI规范、接口文档自动生成、前后端契约测试
说实话,我在大屏项目里踩过最大的坑,就是接口文档。
有一次,后端同学改了接口返回字段,没通知前端。结果大屏上线那天,所有图表都显示「数据加载失败」。老板站在大屏前,脸都绿了。嗯,从那以后,我立了个规矩:没有文档的接口,不允许联调。
3.1 为什么需要接口文档?
你想想看,大屏项目通常涉及多个数据源。有的来自实时流,有的来自离线数仓,还有的来自第三方API。如果没有一份清晰的文档,前后端就像在黑暗中跳舞——踩脚是迟早的事。
我个人习惯,把接口文档当作「契约」。它不只是写给前端看的,更是写给未来接手的人看的。说白了,文档就是代码的一部分。
3.2 Swagger/OpenAPI 规范
现在业界最通用的标准就是 OpenAPI 规范(以前叫 Swagger)。它用 JSON 或 YAML 描述整个 API,包括路径、参数、返回值、认证方式等。
我推荐用 YAML 格式,可读性更好。来看一个实际例子:
openapi: 3.0.0
info:
title: 大屏数据接口
version: 1.0.0
description: 实时大屏数据查询接口
paths:
/api/v1/realtime/sales:
get:
summary: 获取实时销售额
parameters:
- name: date
in: query
required: true
schema:
type: string
format: date
- name: region
in: query
required: false
schema:
type: string
responses:
'200':
description: 成功
content:
application/json:
schema:
type: object
properties:
code:
type: integer
data:
type: object
properties:
totalSales:
type: number
growthRate:
type: number
message:
type: string
这段描述,前端一看就知道要传什么参数、返回什么结构。我在项目中遇到过,有些团队喜欢用 JSON 格式,但 YAML 更简洁,尤其当接口数量多的时候,缩进结构一目了然。
3.3 接口文档自动生成
手写文档?别傻了。我见过最惨的案例:文档写了 200 页,代码改了 300 次,文档一次没更新。最后前端拿着过期的文档联调,三天没调通。
正确的做法是:从代码自动生成文档。
以 Java 的 Spring Boot 为例,用 springdoc-openapi 库,加几个注解就行:
@RestController
@RequestMapping("/api/v1/realtime")
@Tag(name = "实时数据接口", description = "大屏实时数据查询")
public class RealtimeController {
@GetMapping("/sales")
@Operation(summary = "获取实时销售额")
public ApiResponse<SalesVO> getSales(
@Parameter(description = "日期", required = true)
@RequestParam String date,
@Parameter(description = "区域")
@RequestParam(required = false) String region) {
// 业务逻辑
}
}
启动项目后,访问 /swagger-ui.html 就能看到完整的文档页面。前端可以直接在页面上测试接口,连 Postman 都省了。
3.4 前后端契约测试
文档有了,但怎么保证双方都遵守契约?
我曾经遇到一个坑:后端说「我返回的字段名是 camelCase」,前端说「我收到的是 snake_case」。查了半天,发现是序列化配置不一致。这种问题,靠人眼检查是看不出来的。
解决方案是契约测试。我推荐用 Pact 框架,它可以在单元测试阶段验证接口是否满足契约。
前端侧(以 JavaScript 为例):
const { Pact } = require('@pact-foundation/pact');
const provider = new Pact({
consumer: 'Frontend',
provider: 'Backend',
port: 1234,
});
describe('Sales API', () => {
beforeAll(() => provider.setup());
afterAll(() => provider.finalize());
test('should return sales data', async () => {
await provider.addInteraction({
state: 'has sales data',
uponReceiving: 'a request for sales',
withRequest: {
method: 'GET',
path: '/api/v1/realtime/sales',
query: { date: '2024-01-01' },
},
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
code: 0,
data: {
totalSales: 1000000,
growthRate: 0.15,
},
},
},
});
const response = await fetch('http://localhost:1234/api/v1/realtime/sales?date=2024-01-01');
const body = await response.json();
expect(body.code).toBe(0);
expect(body.data.totalSales).toBeDefined();
});
});
后端侧(以 Java 为例):
@PactVerification(fragment = "pactFile")
@Test
void verifySalesContract() {
// Pact 会自动启动 Mock Server,验证接口是否满足契约
}
这样做的好处是:任何一方修改了接口,测试就会失败。双方必须协商一致才能通过。说白了,契约测试就是给接口上了一把「锁」。
3.5 大屏场景的特殊考虑
大屏接口和普通 Web 接口不太一样。我总结了几点经验:
- 数据格式要统一:所有接口返回统一格式,比如
{ code, data, message }。前端不用为每个接口写不同的解析逻辑。 - 时间字段用时间戳:大屏经常需要做时间对比,用字符串格式容易出 bug。我习惯用毫秒级时间戳,前端直接 new Date() 就行。
- 空值处理要明确:大屏数据可能为空,接口文档里要明确说明空值时的行为。是返回 null 还是空对象?前端需要提前处理。
- 分页参数要规范:大屏通常不分页,但如果有历史数据查询,分页参数要统一。我推荐用
page和size,而不是offset和limit。
| 场景 | 推荐做法 | 不推荐做法 |
|---|---|---|
| 时间字段 | 毫秒时间戳 | yyyy-MM-dd HH:mm:ss 字符串 |
| 空值处理 | 明确返回 null 或空对象 | 不返回该字段 |
| 分页参数 | page + size | offset + limit |
| 错误码 | 统一错误码表 | 每个接口自定义错误码 |
嗯,最后说一句:文档和契约不是束缚,而是解放。有了它们,前后端可以并行开发,互不干扰。我在团队里推行这套规范后,联调时间从原来的 3 天缩短到了半天。这才是真正的效率提升。