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