7.6 KiB
7.6 KiB
API 版本控制策略
为什么要对 API 进行版本控制?
API 版本控制允许你在演进 API 的同时,保持对现有客户端的向后兼容性。破坏性变更需要发布新版本。
破坏性变更
需要发布新版本的变更:
- 删除或重命名字段
- 更改字段类型(如字符串改为整数)
- 在请求中新增必填字段
- 更改响应结构
- 删除端点
- 更改同一场景下的 HTTP 状态码
- 更改认证机制
非破坏性变更
不需要发布新版本的安全变更:
- 新增端点
- 新增可选请求字段
- 在响应中新增字段(客户端应忽略未知字段)
- 修复 Bug
- 性能优化
- 为已有资源新增 HTTP 方法
版本控制策略
1. URI 版本控制
最常见且最直观的方式。版本号作为 URL 路径的一部分。
GET /v1/users/123
GET /v2/users/123
优点:
- 在 URL 中清晰可见
- 易于理解与实现
- 路由和缓存简单
- 可同时运行多个版本
缺点:
- 违反 REST 原则(同一资源对应不同 URI)
- 修改版本号需要更新客户端代码
- 可能导致 URI 泛滥
实现方式:
/v1/users
/v1/products
/v2/users # 包含破坏性变更的新版本
/v2/products
2. 标头版本控制
版本号通过 HTTP 标头(Accept 标头或自定义标头)指定。
Accept 标头:
GET /users/123
Accept: application/vnd.myapi.v1+json
GET /users/123
Accept: application/vnd.myapi.v2+json
自定义标头:
GET /users/123
API-Version: 1
GET /users/123
API-Version: 2
优点:
- URI 保持稳定
- 更符合 REST 风格(同一资源对应同一 URI)
- 将版本控制与资源标识分离
缺点:
- 不够直观(调试难度更大)
- 路由更复杂
- 在浏览器中测试困难
- 缓存更复杂
3. 查询参数版本控制
版本号通过查询参数指定。
GET /users/123?version=1
GET /users/123?version=2
# 或
GET /users/123?api-version=1
GET /users/123?api-version=2
优点:
- 实现简单
- 易于测试
- 在 URL 中可见
缺点:
- 污染查询字符串
- 不够语义化(版本不是筛选条件)
- 可能与其他查询参数冲突
4. 内容协商
客户端通过内容协商指定所需的版本。
GET /users/123
Accept: application/vnd.myapi+json; version=1
GET /users/123
Accept: application/vnd.myapi+json; version=2
优点:
- 非常符合 REST 风格
- 灵活的内容类型协商
- URI 稳定
缺点:
- 实现复杂
- 对开发者不够直观
- 测试难度更大
推荐方案
推荐大多数 API 使用 URI 版本控制,原因如下:
- 最明确、最易于发现
- 易于理解和调试
- 实现和维护简单
- 版本之间界限清晰
/v1/users
/v2/users
/v3/users
版本格式
仅使用主版本号
公开 API 使用简单的主版本号(v1、v2、v3):
/v1/users
/v2/users
优点:
- 简单明了
- 易于沟通
- 促使开发者慎重考虑破坏性变更
基于日期的版本号
部分 API 使用日期作为版本号:
/2024-01-01/users
/2024-06-15/users
使用者: Stripe、GitHub API
优点:
- 版本发布时间一目了然
- 时间线清晰易懂
- 不会混淆主版本号与次版本号
缺点:
- 对客户端不够直观
- 难以了解具体变更内容
版本生命周期
1. 引入阶段
新版本与旧版本同时发布:
/v1/users # 仍受支持
/v2/users # 新版本可用
发布新版本时需公告:
- 解释变更的博客文章
- 迁移指南
- 破坏性变更列表
- v1 版本的弃用时间表
2. 弃用阶段
将旧版本标记为已弃用,但保持其继续运行:
GET /v1/users/123
响应:
Deprecation: true
Sunset: Wed, 15 Jan 2025 00:00:00 GMT
Link: </v2/users/123>; rel="successor-version"
{
"id": 123,
"name": "John Doe"
}
弃用标头:
Deprecation: true— 表示该版本已弃用Sunset: <date>— 该版本将于何时移除(RFC 8594)Link: <url>; rel="successor-version"— 指向新版本
3. 退役阶段
旧版本在公布的日期正式下线。
对已弃用的端点返回 410 Gone:
GET /v1/users/123
响应:410 Gone
{
"error": {
"code": "VERSION_SUNSET",
"message": "API v1 已于 2025-01-15 退役。请使用 v2。",
"documentation_url": "https://api.example.com/docs/migration-v1-to-v2"
}
}
弃用策略
推荐时间线
- 宣布弃用 — 至少在退役前 6 个月
- 支持期 — 两个版本同时运行 6-12 个月
- 退役日期 — 提前明确告知具体日期
- 宽限期 — 在完全关闭前,提供 30 天的 410 Gone 响应
沟通渠道
- API 响应标头
- 发送给注册开发者的邮件
- 博客文章和更新日志
- 控制台通知
- 文档更新
- 状态页面公告
迁移策略
提供迁移指南
# 从 v1 迁移至 v2
## 破坏性变更
### 用户资源变更
**v1:**
```json
{
"id": 123,
"name": "John Doe",
"email": "john@example.com"
}
v2:
{
"id": 123,
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com"
}
迁移步骤:
- 将
name字段拆分为first_name和last_name - 更新客户端代码以使用新字段
### 提供辅助工具
- 迁移脚本
- SDK 更新
- API 差异对比工具
- 兼容层(临时)
## 版本发现
### 根端点
```http
GET /
响应:
{
"versions": {
"v1": {
"status": "deprecated",
"sunset_date": "2025-01-15",
"documentation_url": "https://api.example.com/docs/v1"
},
"v2": {
"status": "current",
"documentation_url": "https://api.example.com/docs/v2"
},
"v3": {
"status": "beta",
"documentation_url": "https://api.example.com/docs/v3"
}
}
}
版本信息端点
GET /v2/version
响应:
{
"version": "v2",
"released": "2024-01-15",
"status": "stable",
"sunset_date": null
}
OpenAPI 版本控制
每个版本独立规范文件
openapi-v1.yaml
openapi-v2.yaml
openapi-v3.yaml
每个规范文件都是完整且独立的。
单一规范多服务器
openapi: 3.1.0
info:
title: My API
version: 2.0.0
servers:
- url: https://api.example.com/v1
description: 版本 1(已弃用)
- url: https://api.example.com/v2
description: 版本 2(当前版本)
最佳实践
- 从第一天起就做版本控制 — 从 /v1 开始,而不是 /api
- 仅使用主版本号 — 使用 v1、v2、v3(而不是 v1.1、v1.2)
- 提供较长的弃用期 — 给客户端留出迁移时间(6-12 个月)
- 沟通要清晰 — 使用标头、文档、邮件
- 维护旧版本 — 至少同时支持两个版本
- 记录变更 — 提供详细的迁移指南
- 使用语义化版本控制 — 用于内部/SDK 版本控制
- 永远不要在没有警告的情况下破坏兼容性 — 始终提前公告破坏性变更
- 提供辅助工具 — 迁移脚本、更新后的 SDK
- 监控使用情况 — 追踪哪些版本正在被使用
反模式
避免以下错误:
- 破坏性变更但不升级版本 — 破坏现有客户端
- 版本过多 — 维护噩梦(最多保持 2-3 个活跃版本)
- 弃用期过短 — 让开发者感到困扰
- 没有迁移路径 — 让升级变得痛苦
- 突然退役 — 毫无预警地破坏生产应用
- 版本控制策略不一致 — 不同端点使用不同策略
- 对单个端点进行版本控制 — 应在整个 API 中使用一致的版本