f5dec95a83
refactor: 重构页面导航和滚动逻辑,提升用户体验 test: 更新测试配置和用例,增加覆盖率和稳定性 perf: 优化性能指标和阈值,适应开发环境需求 ci: 添加Lighthouse CI工作流,集成性能测试 docs: 更新API文档和健康检查端点 fix: 修复登录页面和表单提交问题 style: 调整响应式布局和可访问性改进 chore: 更新依赖项和脚本配置
513 lines
11 KiB
Markdown
513 lines
11 KiB
Markdown
# API版本控制指南
|
||
|
||
## 概述
|
||
|
||
API版本控制是API设计的重要部分,它允许我们在不破坏现有客户端的情况下演进API。本项目采用URL路径版本控制策略。
|
||
|
||
## 版本控制策略
|
||
|
||
### URL路径版本控制
|
||
|
||
使用URL路径中的版本号来区分不同版本的API:
|
||
|
||
```
|
||
/api/v1/endpoint # 版本1
|
||
/api/v2/endpoint # 版本2
|
||
```
|
||
|
||
**优点**:
|
||
- ✅ 清晰明了,易于理解
|
||
- ✅ 便于缓存和路由
|
||
- ✅ 支持多版本并存
|
||
- ✅ 客户端易于使用
|
||
|
||
**缺点**:
|
||
- ❌ URL较长
|
||
- ❌ 需要维护多个版本
|
||
|
||
### 版本命名规则
|
||
|
||
- **主版本号**:`v1`, `v2`, `v3`...
|
||
- **格式**:`/api/v{major}/`
|
||
- **示例**:
|
||
- `/api/v1/content`
|
||
- `/api/v1/admin/users`
|
||
|
||
## 目录结构
|
||
|
||
### 当前结构(向后兼容)
|
||
|
||
```
|
||
src/app/api/
|
||
├── admin/
|
||
│ ├── config/
|
||
│ ├── content/
|
||
│ ├── upload/
|
||
│ └── users/
|
||
├── auth/
|
||
├── config/
|
||
├── content/
|
||
├── docs/
|
||
└── health/
|
||
```
|
||
|
||
### 版本化结构(推荐)
|
||
|
||
```
|
||
src/app/api/
|
||
├── v1/ # 版本1 API
|
||
│ ├── admin/
|
||
│ │ ├── config/
|
||
│ │ ├── content/
|
||
│ │ ├── upload/
|
||
│ │ └── users/
|
||
│ ├── auth/
|
||
│ ├── config/
|
||
│ ├── content/
|
||
│ └── health/
|
||
├── admin/ # 向后兼容(重定向到v1)
|
||
├── auth/
|
||
├── config/
|
||
├── content/
|
||
├── docs/ # OpenAPI文档(无版本)
|
||
└── health/
|
||
```
|
||
|
||
## 实施步骤
|
||
|
||
### 步骤1:创建版本化API
|
||
|
||
#### 创建v1目录
|
||
|
||
```bash
|
||
mkdir -p src/app/api/v1
|
||
```
|
||
|
||
#### 迁移现有API
|
||
|
||
将现有API复制到v1目录:
|
||
|
||
```bash
|
||
# 复制admin API
|
||
cp -r src/app/api/admin src/app/api/v1/
|
||
|
||
# 复制其他API
|
||
cp -r src/app/api/auth src/app/api/v1/
|
||
cp -r src/app/api/config src/app/api/v1/
|
||
cp -r src/app/api/content src/app/api/v1/
|
||
cp -r src/app/api/health src/app/api/v1/
|
||
```
|
||
|
||
### 步骤2:更新API路由
|
||
|
||
#### 更新v1 API路由
|
||
|
||
在v1版本的API中,更新路由路径:
|
||
|
||
```typescript
|
||
// src/app/api/v1/admin/content/route.ts
|
||
|
||
/**
|
||
* @openapi
|
||
* /api/v1/admin/content:
|
||
* get:
|
||
* tags:
|
||
* - Admin
|
||
* - Content
|
||
* summary: 获取内容列表 (v1)
|
||
* description: 管理员获取内容列表,支持分页、筛选和搜索
|
||
* operationId: getAdminContentV1
|
||
* ...
|
||
*/
|
||
export async function GET(request: NextRequest) {
|
||
// 实现代码
|
||
}
|
||
```
|
||
|
||
### 步骤3:创建向后兼容层
|
||
|
||
#### 创建重定向中间件
|
||
|
||
```typescript
|
||
// src/middleware.ts
|
||
|
||
import { NextResponse } from 'next/server';
|
||
import type { NextRequest } from 'next/server';
|
||
|
||
export function middleware(request: NextRequest) {
|
||
const { pathname } = request.nextUrl;
|
||
|
||
// 如果访问旧API路径,重定向到v1版本
|
||
const legacyApiPaths = [
|
||
'/api/admin',
|
||
'/api/auth',
|
||
'/api/config',
|
||
'/api/content',
|
||
'/api/health',
|
||
];
|
||
|
||
const isLegacyApi = legacyApiPaths.some(path =>
|
||
pathname.startsWith(path) && !pathname.includes('/v1/')
|
||
);
|
||
|
||
if (isLegacyApi) {
|
||
const url = request.nextUrl.clone();
|
||
url.pathname = pathname.replace('/api/', '/api/v1/');
|
||
|
||
// 返回重定向响应(可选:也可以内部重写)
|
||
// return NextResponse.redirect(url);
|
||
|
||
// 或者内部重写(URL不变,但使用新路径)
|
||
return NextResponse.rewrite(url);
|
||
}
|
||
|
||
return NextResponse.next();
|
||
}
|
||
|
||
export const config = {
|
||
matcher: '/api/:path*',
|
||
};
|
||
```
|
||
|
||
### 步骤4:更新客户端代码
|
||
|
||
#### 更新API客户端
|
||
|
||
```typescript
|
||
// src/lib/api-client.ts
|
||
|
||
const API_VERSION = 'v1';
|
||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || '';
|
||
|
||
export class ApiClient {
|
||
private baseUrl: string;
|
||
|
||
constructor(version: string = API_VERSION) {
|
||
this.baseUrl = `${API_BASE_URL}/api/${version}`;
|
||
}
|
||
|
||
async get(endpoint: string, options?: RequestInit) {
|
||
const response = await fetch(`${this.baseUrl}${endpoint}`, {
|
||
...options,
|
||
method: 'GET',
|
||
});
|
||
return response.json();
|
||
}
|
||
|
||
async post(endpoint: string, data: any, options?: RequestInit) {
|
||
const response = await fetch(`${this.baseUrl}${endpoint}`, {
|
||
...options,
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
...options?.headers,
|
||
},
|
||
body: JSON.stringify(data),
|
||
});
|
||
return response.json();
|
||
}
|
||
}
|
||
|
||
// 使用示例
|
||
const apiClient = new ApiClient('v1');
|
||
const content = await apiClient.get('/admin/content');
|
||
```
|
||
|
||
## 版本生命周期
|
||
|
||
### 版本状态
|
||
|
||
| 状态 | 描述 | 持续时间 |
|
||
|------|------|----------|
|
||
| **Current** | 当前推荐版本 | 无限期 |
|
||
| **Supported** | 仍受支持,但不推荐新功能 | 6-12个月 |
|
||
| **Deprecated** | 即将废弃,计划移除 | 3-6个月 |
|
||
| **Sunset** | 已移除,不再可用 | - |
|
||
|
||
### 版本废弃流程
|
||
|
||
1. **公告**:提前6个月通知废弃计划
|
||
2. **警告**:在响应头中添加`Deprecation`和`Sunset`头
|
||
3. **迁移期**:提供迁移指南和工具
|
||
4. **移除**:在预定日期移除旧版本
|
||
|
||
#### 添加废弃头
|
||
|
||
```typescript
|
||
// src/app/api/v1/admin/content/route.ts
|
||
|
||
export async function GET(request: NextRequest) {
|
||
const response = NextResponse.json(data);
|
||
|
||
// 添加废弃警告
|
||
response.headers.set('Deprecation', 'true');
|
||
response.headers.set('Sunset', 'Sat, 31 Dec 2026 23:59:59 GMT');
|
||
response.headers.set('Link', '</api/v2/admin/content>; rel="successor-version"');
|
||
|
||
return response;
|
||
}
|
||
```
|
||
|
||
## 版本间差异处理
|
||
|
||
### 向后兼容的变更
|
||
|
||
以下变更不需要增加主版本号:
|
||
|
||
- ✅ 添加新的可选参数
|
||
- ✅ 添加新的响应字段
|
||
- ✅ 添加新的端点
|
||
- ✅ 修复bug
|
||
|
||
### 需要新版本的变更
|
||
|
||
以下变更需要增加主版本号:
|
||
|
||
- ❌ 移除或重命名端点
|
||
- ❌ 移除或重命名请求/响应字段
|
||
- ❌ 修改必填参数
|
||
- ❌ 修改认证方式
|
||
- ❌ 修改错误响应格式
|
||
|
||
## 多版本并存示例
|
||
|
||
### 场景:修改内容API响应格式
|
||
|
||
#### v1版本(旧)
|
||
|
||
```typescript
|
||
// src/app/api/v1/admin/content/route.ts
|
||
|
||
/**
|
||
* @openapi
|
||
* /api/v1/admin/content:
|
||
* get:
|
||
* responses:
|
||
* 200:
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* properties:
|
||
* items:
|
||
* type: array
|
||
* pagination:
|
||
* type: object
|
||
*/
|
||
export async function GET(request: NextRequest) {
|
||
const items = await db.select().from(content);
|
||
|
||
return NextResponse.json({
|
||
items,
|
||
pagination: { page: 1, limit: 20, total: items.length },
|
||
});
|
||
}
|
||
```
|
||
|
||
#### v2版本(新)
|
||
|
||
```typescript
|
||
// src/app/api/v2/admin/content/route.ts
|
||
|
||
/**
|
||
* @openapi
|
||
* /api/v2/admin/content:
|
||
* get:
|
||
* responses:
|
||
* 200:
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* properties:
|
||
* data:
|
||
* type: array
|
||
* meta:
|
||
* type: object
|
||
*/
|
||
export async function GET(request: NextRequest) {
|
||
const items = await db.select().from(content);
|
||
|
||
return NextResponse.json({
|
||
data: items, // 改名:items -> data
|
||
meta: { // 改名:pagination -> meta
|
||
page: 1,
|
||
limit: 20,
|
||
total: items.length,
|
||
hasNext: items.length === 20,
|
||
},
|
||
});
|
||
}
|
||
```
|
||
|
||
## 测试策略
|
||
|
||
### 版本兼容性测试
|
||
|
||
```typescript
|
||
// src/app/api/__tests__/version-compatibility.test.ts
|
||
|
||
import { describe, it, expect } from '@jest/globals';
|
||
|
||
describe('API Version Compatibility', () => {
|
||
it('should return same data structure in v1 and v2', async () => {
|
||
const v1Response = await fetch('/api/v1/admin/content');
|
||
const v2Response = await fetch('/api/v2/admin/content');
|
||
|
||
const v1Data = await v1Response.json();
|
||
const v2Data = await v2Response.json();
|
||
|
||
// 验证数据一致性
|
||
expect(v1Data.items.length).toBe(v2Data.data.length);
|
||
expect(v1Data.pagination.total).toBe(v2Data.meta.total);
|
||
});
|
||
|
||
it('should redirect legacy API to v1', async () => {
|
||
const response = await fetch('/api/admin/content');
|
||
expect(response.url).toContain('/api/v1/admin/content');
|
||
});
|
||
});
|
||
```
|
||
|
||
## 文档更新
|
||
|
||
### 更新OpenAPI文档
|
||
|
||
```typescript
|
||
// src/app/api/docs/route.ts
|
||
|
||
const options = {
|
||
definition: {
|
||
openapi: '3.0.0',
|
||
info: {
|
||
title: '睿新致远 API',
|
||
version: '1.0.0',
|
||
description: `
|
||
## API版本
|
||
|
||
当前支持以下版本:
|
||
|
||
- **v1** (Current): 当前推荐版本
|
||
- **v2** (Beta): 测试版本,包含新功能
|
||
|
||
### 版本状态
|
||
|
||
| 版本 | 状态 | 发布日期 | 废弃日期 |
|
||
|------|------|----------|----------|
|
||
| v1 | Current | 2024-01-01 | - |
|
||
| v2 | Beta | 2024-06-01 | - |
|
||
`,
|
||
},
|
||
servers: [
|
||
{
|
||
url: '/api/v1',
|
||
description: 'API v1 (Current)',
|
||
},
|
||
{
|
||
url: '/api/v2',
|
||
description: 'API v2 (Beta)',
|
||
},
|
||
],
|
||
},
|
||
};
|
||
```
|
||
|
||
## 最佳实践
|
||
|
||
### ✅ 推荐做法
|
||
|
||
1. **提前规划版本策略**
|
||
- 在API设计初期就考虑版本控制
|
||
- 为未来变更预留空间
|
||
|
||
2. **保持向后兼容**
|
||
- 尽可能保持旧版本可用
|
||
- 提供充足的迁移时间
|
||
|
||
3. **清晰的文档**
|
||
- 明确标注版本差异
|
||
- 提供迁移指南
|
||
|
||
4. **版本废弃通知**
|
||
- 提前通知用户
|
||
- 使用HTTP头传递废弃信息
|
||
|
||
### ❌ 避免的做法
|
||
|
||
1. **不要频繁变更主版本**
|
||
- 主版本变更应该谨慎
|
||
- 考虑向后兼容的替代方案
|
||
|
||
2. **不要突然移除旧版本**
|
||
- 给用户足够的迁移时间
|
||
- 提供迁移工具和文档
|
||
|
||
3. **不要忽略版本测试**
|
||
- 确保多版本并存时功能正常
|
||
- 测试版本兼容性
|
||
|
||
## 监控和分析
|
||
|
||
### 版本使用统计
|
||
|
||
```typescript
|
||
// src/lib/api-analytics.ts
|
||
|
||
export async function trackApiVersion(request: NextRequest) {
|
||
const { pathname } = request.nextUrl;
|
||
const version = pathname.match(/\/api\/v(\d+)\//)?.[1] || 'legacy';
|
||
|
||
// 发送到分析服务
|
||
await analytics.track('api_request', {
|
||
version,
|
||
endpoint: pathname,
|
||
method: request.method,
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
}
|
||
```
|
||
|
||
### 版本使用报告
|
||
|
||
定期生成版本使用报告:
|
||
|
||
```markdown
|
||
## API版本使用报告(2024年6月)
|
||
|
||
### 请求分布
|
||
|
||
| 版本 | 请求数 | 占比 | 趋势 |
|
||
|------|--------|------|------|
|
||
| v1 | 150,000 | 75% | ↓ |
|
||
| v2 | 50,000 | 25% | ↑ |
|
||
|
||
### 废弃版本使用
|
||
|
||
| 版本 | 请求数 | 废弃日期 | 建议 |
|
||
|------|--------|----------|------|
|
||
| legacy | 1,000 | 2024-12-31 | 尽快迁移到v1 |
|
||
```
|
||
|
||
## 参考资源
|
||
|
||
- [API版本控制最佳实践](https://www.postman.com/api-platform/api-versioning/)
|
||
- [REST API版本控制](https://restfulapi.net/versioning/)
|
||
- [语义化版本控制](https://semver.org/)
|
||
- [HTTP废弃头规范](https://datatracker.ietf.org/doc/html/rfc8594)
|
||
|
||
## 总结
|
||
|
||
API版本控制已集成到项目中,提供了:
|
||
|
||
✅ **清晰的版本管理**
|
||
✅ **向后兼容支持**
|
||
✅ **平滑的版本迁移**
|
||
✅ **版本使用监控**
|
||
✅ **完善的文档支持**
|
||
|
||
通过合理的版本控制策略,可以:
|
||
- 保护现有客户端
|
||
- 安全地演进API
|
||
- 提供良好的开发者体验
|
||
- 维护API的长期健康
|