⚠️ Alpha内测版本警告:此为早期内部构建版本,尚不完整且可能存在错误,欢迎大家提Issue反馈问题或建议
Skip to content

12.7.2 别让升级搞坏旧应用——向后兼容:破坏性变更的避免

一句话破题

向后兼容意味着"新版本不破坏旧代码"——这需要在设计阶段就考虑未来的演进。

什么是破坏性变更

变更类型示例是否破坏性
添加可选字段响应中新增 meta 字段
添加必填字段请求必须包含 userId
删除字段移除 legacyId 字段
重命名字段user_nameuserName
改变类型id: numberid: string
改变行为排序顺序变化可能

添加字段而非删除

typescript
// 旧版本响应
interface UserV1 {
  id: number;
  name: string;
}

// 新版本响应(向后兼容)
interface UserV2 {
  id: number;
  name: string;
  email?: string;      // 新增可选字段
  displayName?: string; // 新增可选字段
}

// 客户端代码仍然有效
function displayUser(user: UserV1) {
  console.log(user.name); // 仍然有效
}

使用适配器模式

当必须改变结构时,提供适配层:

typescript
// 内部使用新结构
interface UserInternal {
  id: string; // 改为 string
  profile: {
    displayName: string;
    email: string;
  };
}

// 对外提供适配的旧结构
function toV1Response(user: UserInternal): UserV1 {
  return {
    id: parseInt(user.id), // 转回 number
    name: user.profile.displayName,
  };
}

function toV2Response(user: UserInternal): UserV2 {
  return {
    id: user.id, // 使用新的 string 类型
    displayName: user.profile.displayName,
    email: user.profile.email,
  };
}

功能开关

使用功能开关逐步迁移:

typescript
interface FeatureFlags {
  useNewUserFormat: boolean;
  enableEmailField: boolean;
}

async function getUser(id: string, flags: FeatureFlags) {
  const user = await db.user.findUnique({ where: { id } });
  
  if (flags.useNewUserFormat) {
    return {
      id: user.id,
      profile: {
        displayName: user.name,
        email: flags.enableEmailField ? user.email : undefined,
      },
    };
  }
  
  // 返回旧格式
  return {
    id: parseInt(user.id),
    name: user.name,
  };
}

兼容性测试

typescript
import { describe, it, expect } from 'vitest';

describe('API 兼容性测试', () => {
  it('v2 响应应该兼容 v1 客户端', async () => {
    const v2Response = await fetch('/api/v2/users/1');
    const data = await v2Response.json();
    
    // v1 客户端期望的字段必须存在
    expect(data).toHaveProperty('id');
    expect(data).toHaveProperty('name');
    expect(typeof data.id).toBe('number'); // v1 期望 number
  });
  
  it('新增字段不应破坏旧客户端', async () => {
    const response = await fetch('/api/v2/users/1');
    const data = await response.json();
    
    // 旧客户端忽略新字段,不应报错
    const oldClientCode = () => {
      console.log(data.name);
    };
    
    expect(oldClientCode).not.toThrow();
  });
});

AI 协作指南

  • 核心意图:让 AI 帮你评估变更是否破坏兼容性。
  • 需求定义公式"请分析这个 API 变更是否是破坏性的,如果是,请建议如何保持向后兼容。"
  • 关键术语向后兼容 (backward compatibility)破坏性变更 (breaking change)适配器模式

避坑指南

  • 添加而非删除:永远不要删除字段,只添加新字段。
  • 可选而非必填:新字段尽量设为可选。
  • 文档标注:明确标注哪些字段可能在未来版本变化。