--- # 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: ; rel="successor-version" { "id": 123, "name": "John Doe" } ``` **弃用标头:** - `Deprecation: true` — 表示该版本已弃用 - `Sunset: ` — 该版本将于何时移除(RFC 8594) - `Link: ; 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 中使用一致的版本