普通视图

发现新文章,点击刷新页面。
昨天 — 2025年10月14日首页

代码质量工程完全指南 🚀

作者 Holin_浩霖
2025年10月13日 23:43

代码质量工程完全指南 🚀

构建可维护、高质量代码库的完整实践方案

TypeScript 高级用法 ⚙️

1. 泛型约束(Generics & Constraints)🎯

为什么需要泛型约束?

在开发可复用组件时,我们经常需要处理多种数据类型,但又不想失去 TypeScript 的类型安全优势。泛型约束允许我们在保持灵活性的同时,对类型参数施加限制。

解决的问题:

  • 避免使用 any 类型导致类型信息丢失
  • 在通用函数中保持输入输出类型关系
  • 提供更好的 IDE 智能提示和自文档化

缺点与限制:

  • 过度复杂的约束会让错误信息难以理解
  • 嵌套泛型可能导致编译性能下降
  • 初学者可能需要时间适应这种抽象思维
详细代码示例
// 🎯 基础泛型函数
function identity<T>(value: T): T {
  return value;
}

// 使用显式类型参数
const result1 = identity<string>("Hello"); // 类型: string
// 使用类型推断
const result2 = identity(42); // 类型: number

// 🎯 泛型约束 - 确保类型具有特定属性
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(item: T): void {
  console.log(`Length: ${item.length}`);
}

// 这些调用都是合法的
logLength("hello");     // 字符串有 length 属性
logLength([1, 2, 3]);   // 数组有 length 属性
logLength({ length: 5, name: "test" }); // 对象有 length 属性

// 🎯 泛型约束与 keyof 结合
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { 
  id: 1, 
  name: "Alice", 
  email: "alice@example.com" 
};

const userName = getProperty(user, "name");    // 类型: string
const userId = getProperty(user, "id");        // 类型: number

// 🎯 多重约束
interface Serializable {
  serialize(): string;
}

interface Identifiable {
  id: number;
}

function processEntity<T extends Serializable & Identifiable>(entity: T): void {
  console.log(`ID: ${entity.id}`);
  console.log(`Serialized: ${entity.serialize()}`);
}

// 🎯 泛型类示例
class Repository<T extends { id: number }> {
  private items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  findById(id: number): T | undefined {
    return this.items.find(item => item.id === id);
  }

  getAll(): T[] {
    return [...this.items];
  }
}

// 使用泛型类
interface Product {
  id: number;
  name: string;
  price: number;
}

const productRepo = new Repository<Product>();
productRepo.add({ id: 1, name: "Laptop", price: 999 });
const laptop = productRepo.findById(1); // 类型: Product | undefined

2. 条件类型与推断(Conditional Types & infer)🧠

为什么需要条件类型?

条件类型允许我们在类型级别进行条件判断,实现基于输入类型的动态类型转换。这在创建灵活的类型工具和库时特别有用。

解决的问题:

  • 根据条件动态推导类型
  • 从复杂类型中提取子类型
  • 减少重复的类型定义

缺点与限制:

  • 可读性较差,特别是嵌套条件类型
  • 错误信息可能非常复杂难懂
  • 需要深入理解 TypeScript 的类型系统
详细代码示例
// 🧠 基础条件类型
type IsString<T> = T extends string ? true : false;

type Test1 = IsString<"hello">;    // true
type Test2 = IsString<number>;     // false
type Test3 = IsString<string | number>; // boolean

// 🧠 使用 infer 进行类型提取
type ExtractPromiseType<T> = T extends Promise<infer U> ? U : T;

type AsyncString = ExtractPromiseType<Promise<string>>; // string
type JustNumber = ExtractPromiseType<number>;           // number

// 🧠 从函数类型中提取参数和返回类型
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type GetParameters<T> = T extends (...args: infer P) => any ? P : never;

type Func = (a: number, b: string) => boolean;
type Return = GetReturnType<Func>;    // boolean
type Params = GetParameters<Func>;    // [number, string]

// 🧠 分发条件类型(分布式条件类型)
type ToArray<T> = T extends any ? T[] : never;

type StringOrNumberArray = ToArray<string | number>; 
// 等价于: string[] | number[]

type NeverArray = ToArray<never>; // never

// 🧠 排除 null 和 undefined
type NonNullable<T> = T extends null | undefined ? never : T;

type ValidString = NonNullable<string | null>;        // string
type ValidNumber = NonNullable<number | undefined>;   // number

// 🧠 递归条件类型 - DeepPartial
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

interface User {
  id: number;
  profile: {
    name: string;
    settings: {
      theme: string;
      notifications: boolean;
    };
  };
}

type PartialUser = DeepPartial<User>;
// 等价于:
// {
//   id?: number;
//   profile?: {
//     name?: string;
//     settings?: {
//       theme?: string;
//       notifications?: boolean;
//     };
//   };
// }

// 🧠 条件类型与模板字面量结合
type GetterName<T extends string> = T extends `_${infer Rest}` 
  ? `get${Capitalize<Rest>}` 
  : `get${Capitalize<T>}`;

type NameGetter = GetterName<"name">;     // "getName"
type PrivateGetter = GetterName<"_email">; // "getEmail"

3. 映射类型(Mapped Types)🔁

为什么需要映射类型?

映射类型允许我们基于现有类型创建新类型,通过转换每个属性来实现类型的批量操作。

解决的问题:

  • 批量修改类型属性(只读、可选等)
  • 基于现有类型创建变体
  • 减少重复的类型定义代码

缺点与限制:

  • 映射类型不会自动递归处理嵌套对象
  • 复杂的映射类型可能难以理解和调试
  • 某些高级用法需要深入的类型系统知识
详细代码示例
// 🔁 基础映射类型
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

type Partial<T> = {
  [P in keyof T]?: T[P];
};

type Required<T> = {
  [P in keyof T]-?: T[P];
};

// 🔁 键重映射
type Getters<T> = {
  [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};

interface User {
  name: string;
  age: number;
}

type UserGetters = Getters<User>;
// 等价于:
// {
//   getName: () => string;
//   getAge: () => number;
// }

// 🔁 过滤属性
type OnlyFunctions<T> = {
  [K in keyof T as T[K] extends Function ? K : never]: T[K];
};

interface MixedInterface {
  name: string;
  age: number;
  getName(): string;
  setAge(age: number): void;
}

type FunctionsOnly = OnlyFunctions<MixedInterface>;
// 等价于:
// {
//   getName: () => string;
//   setAge: (age: number) => void;
// }

// 🔁 基于值的类型映射
type EventConfig<T extends { kind: string }> = {
  [E in T as E["kind"]]: (event: E) => void;
};

type Event = 
  | { kind: "click"; x: number; y: number }
  | { kind: "keypress"; key: string }
  | { kind: "focus"; element: HTMLElement };

type Config = EventConfig<Event>;
// 等价于:
// {
//   click: (event: { kind: "click"; x: number; y: number }) => void;
//   keypress: (event: { kind: "keypress"; key: string }) => void;
//   focus: (event: { kind: "focus"; element: HTMLElement }) => void;
// }

// 🔁 实用映射类型示例
// 1. 将所有属性变为可空
type Nullable<T> = {
  [P in keyof T]: T[P] | null;
};

// 2. 将函数返回值包装为 Promise
type Promisify<T> = {
  [P in keyof T]: T[P] extends (...args: infer A) => infer R 
    ? (...args: A) => Promise<R> 
    : T[P];
};

// 3. 创建严格的不可变类型
type Immutable<T> = {
  readonly [P in keyof T]: T[P] extends object ? Immutable<T[P]> : T[P];
};

4. 实用工具类型(Utility Types)🧰

为什么需要工具类型?

工具类型提供了常见的类型转换操作,让类型定义更加简洁和可维护。

解决的问题:

  • 减少重复的类型定义
  • 提供标准的类型转换模式
  • 提高代码的可读性和一致性

缺点与限制:

  • 初学者可能需要时间学习各种工具类型
  • 过度使用可能让代码看起来更复杂
  • 某些工具类型的行为可能不符合直觉
详细代码示例
interface User {
  id: number;
  name: string;
  email: string;
  age?: number;
  createdAt: Date;
  updatedAt?: Date;
}

// 🧰 Partial - 所有属性变为可选
type UserUpdate = Partial<User>;
// 等价于:
// {
//   id?: number;
//   name?: string;
//   email?: string;
//   age?: number;
//   createdAt?: Date;
//   updatedAt?: Date;
// }

// 🧰 Required - 所有属性变为必需
type CompleteUser = Required<User>;
// 等价于:
// {
//   id: number;
//   name: string;
//   email: string;
//   age: number;
//   createdAt: Date;
//   updatedAt: Date;
// }

// 🧰 Pick - 选择特定属性
type UserBasicInfo = Pick<User, 'id' | 'name'>;
// 等价于:
// {
//   id: number;
//   name: string;
// }

// 🧰 Omit - 排除特定属性
type UserWithoutDates = Omit<User, 'createdAt' | 'updatedAt'>;
// 等价于:
// {
//   id: number;
//   name: string;
//   email: string;
//   age?: number;
// }

// 🧰 Record - 创建键值映射
type UserMap = Record<number, User>;
// 等价于:
// {
//   [key: number]: User;
// }

type StatusMap = Record<'success' | 'error' | 'loading', boolean>;
// 等价于:
// {
//   success: boolean;
//   error: boolean;
//   loading: boolean;
// }

// 🧰 Extract - 提取匹配的类型
type StringKeys = Extract<keyof User, string>;
// 从 'id' | 'name' | 'email' | 'age' | 'createdAt' | 'updatedAt'
// 提取出所有字符串键(这里全部都是)

// 🧰 Exclude - 排除匹配的类型
type NonFunctionKeys = Exclude<keyof User, Function>;
// 排除函数类型的键(这里没有函数,所以返回所有键)

// 🧰 工具类型组合使用
// 创建用户表单数据类型
type UserFormData = Partial<Pick<User, 'name' | 'email' | 'age'>>;
// 等价于:
// {
//   name?: string;
//   email?: string;
//   age?: number;
// }

// 创建 API 响应类型
type ApiResponse<T> = {
  data: T;
  success: boolean;
  message?: string;
};

type UserResponse = ApiResponse<Omit<User, 'password'>>;

// 🧰 自定义工具类型
// 1. 值类型为特定类型的属性键
type KeysOfType<T, U> = {
  [K in keyof T]: T[K] extends U ? K : never;
}[keyof T];

type StringKeysOfUser = KeysOfType<User, string>; 
// "name" | "email"

// 2. 深度只读
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object 
    ? DeepReadonly<T[P]> 
    : T[P];
};

// 3. 异步函数包装
type AsyncFunction<T extends (...args: any[]) => any> = 
  (...args: Parameters<T>) => Promise<ReturnType<T>>;

5. 模板字面量类型 ✂️

为什么需要模板字面量类型?

模板字面量类型允许在类型级别进行字符串操作,创建精确的字符串字面量类型。

解决的问题:

  • 创建精确的字符串联合类型
  • 基于模式生成类型安全的字符串
  • 减少运行时字符串验证的需要

缺点与限制:

  • 复杂的模板类型可能影响编译性能
  • 错误信息可能难以理解
  • 某些字符串操作在类型级别有限制
详细代码示例
// ✂️ 基础模板字面量类型
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
type ApiVersion = 'v1' | 'v2' | 'v3';

type ApiEndpoint = `/${ApiVersion}/${string}`;
type FullEndpoint = `${HttpMethod} ${ApiEndpoint}`;

// 使用示例
type UserEndpoint = `GET /v1/users` | `POST /v1/users` | `GET /v1/users/${string}`;

// ✂️ 字符串操作类型
// Uppercase, Lowercase, Capitalize, Uncapitalize
type UpperCaseMethod = Uppercase<HttpMethod>; 
// "GET" | "POST" | "PUT" | "DELETE" | "PATCH"

type EventName = 'click' | 'change' | 'submit';
type EventHandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onChange" | "onSubmit"

// ✂️ 路径参数提取
type ExtractPathParams<T extends string> = 
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractPathParams<`/${Rest}`>
    : T extends `${string}:${infer Param}`
    ? Param
    : never;

type RouteParams = ExtractPathParams<'/users/:userId/posts/:postId'>;
// "userId" | "postId"

// ✂️ 配置键生成
type FeatureFlags = 'darkMode' | 'notifications' | 'analytics';
type ConfigKeys = `feature_${Uppercase<FeatureFlags>}`;
// "feature_DARKMODE" | "feature_NOTIFICATIONS" | "feature_ANALYTICS"

// ✂️ CSS 类名生成
type Color = 'primary' | 'secondary' | 'success' | 'danger';
type Size = 'sm' | 'md' | 'lg';

type ButtonClass = `btn-${Color}-${Size}`;
// "btn-primary-sm" | "btn-primary-md" | "btn-primary-lg" | ...

// ✂️ 高级模式匹配
type ParseQueryString<T extends string> = 
  T extends `${infer Key}=${infer Value}&${infer Rest}`
    ? { [K in Key]: Value } & ParseQueryString<Rest>
    : T extends `${infer Key}=${infer Value}`
    ? { [K in Key]: Value }
    : {};

type QueryParams = ParseQueryString<'name=John&age=30&city=NY'>;
// 等价于:
// {
//   name: "John";
//   age: "30";
//   city: "NY";
// }

// ✂️ 自动生成 API 客户端类型
type Resource = 'users' | 'posts' | 'comments';
type Action = 'create' | 'read' | 'update' | 'delete';

type ApiAction<T extends Resource> = {
  [K in Action as `${K}${Capitalize<T>}`]: () => Promise<void>;
};

type UserApi = ApiAction<'users'>;
// 等价于:
// {
//   createUsers: () => Promise<void>;
//   readUsers: () => Promise<void>;
//   updateUsers: () => Promise<void>;
//   deleteUsers: () => Promise<void>;
// }

6. 类型推断与保护 🔎

为什么需要类型保护?

类型保护允许我们在运行时检查值的类型,并让 TypeScript 编译器理解这些检查,从而在特定代码块中缩小类型范围。

解决的问题:

  • 安全地处理联合类型
  • 减少类型断言的使用
  • 提供更好的开发体验和代码安全性

缺点与限制:

  • 需要编写额外的运行时检查代码
  • 复杂的类型保护可能难以维护
  • 某些模式可能无法被 TypeScript 正确推断
详细代码示例
// 🔎 基础类型保护
const isString = (value: unknown): value is string => {
  return typeof value === 'string';
};

const isNumber = (value: unknown): value is number => {
  return typeof value === 'number' && !isNaN(value);
};

const isArray = <T>(value: unknown): value is T[] => {
  return Array.isArray(value);
};

// 🔎 自定义类型保护
interface Cat {
  type: 'cat';
  meow(): void;
  climbTrees(): void;
}

interface Dog {
  type: 'dog';
  bark(): void;
  fetch(): void;
}

type Animal = Cat | Dog;

const isCat = (animal: Animal): animal is Cat => {
  return animal.type === 'cat';
};

const isDog = (animal: Animal): animal is Dog => {
  return animal.type === 'dog';
};

function handleAnimal(animal: Animal) {
  if (isCat(animal)) {
    animal.meow();        // TypeScript 知道这是 Cat
    animal.climbTrees();  // 可以安全调用
  } else {
    animal.bark();        // TypeScript 知道这是 Dog
    animal.fetch();       // 可以安全调用
  }
}

// 🔎  discriminated unions(可区分联合)
type NetworkState = 
  | { state: 'loading' }
  | { state: 'success'; data: string }
  | { state: 'error'; error: Error };

function handleNetworkState(state: NetworkState) {
  switch (state.state) {
    case 'loading':
      console.log('Loading...');
      break;
    case 'success':
      console.log('Data:', state.data);  // TypeScript 知道有 data 属性
      break;
    case 'error':
      console.log('Error:', state.error.message);  // TypeScript 知道有 error 属性
      break;
  }
}

// 🔎 使用 in 操作符进行类型保护
interface AdminUser {
  role: 'admin';
  permissions: string[];
  manageUsers(): void;
}

interface RegularUser {
  role: 'user';
  preferences: object;
}

type User = AdminUser | RegularUser;

function handleUser(user: User) {
  if ('permissions' in user) {
    user.manageUsers();  // TypeScript 知道这是 AdminUser
  } else {
    console.log(user.preferences);  // TypeScript 知道这是 RegularUser
  }
}

// 🔎 类型断言的最佳实践
// 方式1: as 语法
const element1 = document.getElementById('my-input') as HTMLInputElement;

// 方式2: 尖括号语法(不推荐在 JSX 中使用)
const element2 = <HTMLInputElement>document.getElementById('my-input');

// 方式3: 非空断言(谨慎使用)
const element3 = document.getElementById('my-input')!;

// 方式4: 安全的类型断言函数
function assertIsHTMLElement(element: unknown): asserts element is HTMLElement {
  if (!(element instanceof HTMLElement)) {
    throw new Error('Not an HTMLElement');
  }
}

const element4 = document.getElementById('my-input');
assertIsHTMLElement(element4);
element4.style.color = 'red';  // 现在可以安全访问

// 🔎 复杂的类型保护示例
interface ApiSuccess<T> {
  status: 'success';
  data: T;
  timestamp: Date;
}

interface ApiError {
  status: 'error';
  error: string;
  code: number;
}

type ApiResponse<T> = ApiSuccess<T> | ApiError;

function isApiSuccess<T>(
  response: ApiResponse<T>
): response is ApiSuccess<T> {
  return response.status === 'success';
}

async function fetchData<T>(url: string): Promise<T> {
  const response: ApiResponse<T> = await fetch(url).then(res => res.json());
  
  if (isApiSuccess(response)) {
    return response.data;  // TypeScript 知道这是 ApiSuccess<T>
  } else {
    throw new Error(`API Error ${response.code}: ${response.error}`);
  }
}

// 🔎 类型保护与错误处理
class ValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'ValidationError';
  }
}

class NetworkError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'NetworkError';
  }
}

type AppError = ValidationError | NetworkError;

const isValidationError = (error: Error): error is ValidationError => {
  return error.name === 'ValidationError';
};

function handleError(error: AppError) {
  if (isValidationError(error)) {
    console.log('Validation error:', error.message);
    // 可以访问 ValidationError 特有的属性或方法
  } else {
    console.log('Network error:', error.message);
    // 可以访问 NetworkError 特有的属性或方法
  }
}

测试策略 🧪

测试金字塔架构

graph TB
    A[单元测试 70%] --> B[集成测试 20%]
    B --> C[E2E 测试 10%]
    
    subgraph A [单元测试 - 快速反馈]
        A1[工具函数]
        A2[React 组件]
        A3[自定义 Hooks]
        A4[工具类]
    end
    
    subgraph B [集成测试 - 模块协作]
        B1[组件集成]
        B2[API 集成]
        B3[状态管理]
        B4[路由测试]
    end
    
    subgraph C [E2E 测试 - 用户流程]
        C1[关键业务流程]
        C2[跨页面交互]
        C3[性能测试]
        C4[兼容性测试]
    end
    
    style A fill:#e3f2fd
    style B fill:#f3e5f5
    style C fill:#e8f5e8

1. 单元测试:Jest + React Testing Library

为什么需要单元测试?

单元测试确保代码的最小单元(函数、组件)按预期工作,提供快速反馈和代码质量保障。

解决的问题:

  • 快速发现回归问题
  • 提供代码文档和示例
  • 支持重构和代码演进

缺点与限制:

  • 不能完全模拟真实用户行为
  • 过度 mock 可能导致测试与实现耦合
  • 维护测试需要额外工作量
详细配置与示例
// 🧪 Jest 配置文件示例
// jest.config.js
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/test/setup.ts'],
  moduleNameMapping: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
  },
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
    '!src/index.tsx',
    '!src/reportWebVitals.ts',
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
  transform: {
    '^.+\.(ts|tsx)$': 'ts-jest',
  },
};

// 🧪 测试工具函数示例
// src/utils/format.test.ts
import { formatDate, capitalize, debounce } from './format';

describe('format utilities', () => {
  describe('formatDate', () => {
    it('格式化日期为 YYYY-MM-DD', () => {
      const date = new Date('2023-12-25');
      expect(formatDate(date)).toBe('2023-12-25');
    });

    it('处理无效日期', () => {
      expect(formatDate(new Date('invalid'))).toBe('Invalid Date');
    });
  });

  describe('capitalize', () => {
    it('将字符串首字母大写', () => {
      expect(capitalize('hello world')).toBe('Hello world');
    });

    it('处理空字符串', () => {
      expect(capitalize('')).toBe('');
    });
  });

  describe('debounce', () => {
    jest.useFakeTimers();

    it('防抖函数延迟执行', () => {
      const mockFn = jest.fn();
      const debouncedFn = debounce(mockFn, 100);

      debouncedFn();
      debouncedFn();
      debouncedFn();

      expect(mockFn).not.toHaveBeenCalled();

      jest.advanceTimersByTime(100);
      expect(mockFn).toHaveBeenCalledTimes(1);
    });
  });
});

// 🧪 React 组件测试示例
// src/components/Button/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';

describe('Button Component', () => {
  const defaultProps = {
    onClick: jest.fn(),
    children: 'Click me',
  };

  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('渲染按钮文本', () => {
    render(<Button {...defaultProps} />);
    expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
  });

  it('点击时触发回调', async () => {
    const user = userEvent.setup();
    render(<Button {...defaultProps} />);

    await user.click(screen.getByRole('button'));
    expect(defaultProps.onClick).toHaveBeenCalledTimes(1);
  });

  it('禁用状态下不触发点击', async () => {
    const user = userEvent.setup();
    render(<Button {...defaultProps} disabled />);

    await user.click(screen.getByRole('button'));
    expect(defaultProps.onClick).not.toHaveBeenCalled();
  });

  it('显示加载状态', () => {
    render(<Button {...defaultProps} loading />);
    
    const button = screen.getByRole('button');
    expect(button).toBeDisabled();
    expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
  });

  it('应用正确的 CSS 类', () => {
    render(<Button {...defaultProps} variant="primary" size="large" />);
    
    const button = screen.getByRole('button');
    expect(button).toHaveClass('btn-primary', 'btn-large');
  });
});

// 🧪 自定义 Hook 测试
// src/hooks/useCounter/useCounter.test.ts
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

describe('useCounter', () => {
  it('使用初始值初始化', () => {
    const { result } = renderHook(() => useCounter(5));
    expect(result.current.count).toBe(5);
  });

  it('默认初始值为 0', () => {
    const { result } = renderHook(() => useCounter());
    expect(result.current.count).toBe(0);
  });

  it('递增计数器', () => {
    const { result } = renderHook(() => useCounter(0));
    
    act(() => {
      result.current.increment();
    });
    
    expect(result.current.count).toBe(1);
  });

  it('递减计数器', () => {
    const { result } = renderHook(() => useCounter(2));
    
    act(() => {
      result.current.decrement();
    });
    
    expect(result.current.count).toBe(1);
  });

  it('重置计数器', () => {
    const { result } = renderHook(() => useCounter(10));
    
    act(() => {
      result.current.increment();
      result.current.reset();
    });
    
    expect(result.current.count).toBe(10);
  });

  it('设置特定值', () => {
    const { result } = renderHook(() => useCounter(0));
    
    act(() => {
      result.current.setCount(42);
    });
    
    expect(result.current.count).toBe(42);
  });
});

2. 集成测试 🔗

为什么需要集成测试?

集成测试验证多个模块如何协同工作,确保系统各部分正确集成。

解决的问题:

  • 发现模块间的集成问题
  • 验证数据流和状态管理
  • 确保 API 集成正常工作

缺点与限制:

  • 执行速度比单元测试慢
  • 设置和维护更复杂
  • 可能需要真实的外部依赖
详细代码示例
// 🔗 组件集成测试示例
// src/components/UserProfile/UserProfile.integration.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserProfile } from './UserProfile';
import { UserProvider } from '@/contexts/UserContext';
import { NotificationProvider } from '@/contexts/NotificationContext';
import { server } from '@/mocks/server';

// 设置 API Mock
beforeAll(() => server.listen());
afterEach(() => {
  server.resetHandlers();
  jest.clearAllMocks();
});
afterAll(() => server.close());

describe('UserProfile Integration', () => {
  const renderWithProviders = (component: React.ReactElement) => {
    return render(
      <UserProvider>
        <NotificationProvider>
          {component}
        </NotificationProvider>
      </UserProvider>
    );
  };

  it('加载并显示用户信息', async () => {
    renderWithProviders(<UserProfile userId="123" />);

    // 验证加载状态
    expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();

    // 等待数据加载完成
    await waitFor(() => {
      expect(screen.getByText('张三')).toBeInTheDocument();
    });

    // 验证用户信息显示
    expect(screen.getByText('zhangsan@example.com')).toBeInTheDocument();
    expect(screen.getByText('高级用户')).toBeInTheDocument();
  });

  it('编辑用户信息', async () => {
    const user = userEvent.setup();
    renderWithProviders(<UserProfile userId="123" />);

    // 等待数据加载
    await screen.findByText('张三');

    // 点击编辑按钮
    await user.click(screen.getByRole('button', { name: /编辑/i }));

    // 验证表单显示
    expect(screen.getByLabelText(/姓名/i)).toBeInTheDocument();
    expect(screen.getByLabelText(/邮箱/i)).toBeInTheDocument();

    // 修改信息
    await user.clear(screen.getByLabelText(/姓名/i));
    await user.type(screen.getByLabelText(/姓名/i), '李四');

    // 提交表单
    await user.click(screen.getByRole('button', { name: /保存/i }));

    // 验证成功消息
    await waitFor(() => {
      expect(screen.getByText('用户信息更新成功')).toBeInTheDocument();
    });

    // 验证数据更新
    expect(screen.getByText('李四')).toBeInTheDocument();
  });

  it('处理网络错误', async () => {
    // 模拟 API 错误
    server.use(
      rest.get('/api/users/123', (req, res, ctx) => {
        return res(ctx.status(500), ctx.json({ error: '服务器错误' }));
      })
    );

    renderWithProviders(<UserProfile userId="123" />);

    // 验证错误处理
    await waitFor(() => {
      expect(screen.getByText('加载失败,请重试')).toBeInTheDocument();
    });

    // 验证重试功能
    const retryButton = screen.getByRole('button', { name: /重试/i });
    await userEvent.click(retryButton);

    // 注意:这里需要重新 mock 成功的响应
  });
});

// 🔗 API 集成测试
// src/services/api.integration.test.ts
import { fetchUser, updateUser, deleteUser } from './userApi';
import { server } from '@/mocks/server';

describe('User API Integration', () => {
  beforeAll(() => server.listen());
  afterEach(() => server.resetHandlers());
  afterAll(() => server.close());

  it('成功获取用户信息', async () => {
    const user = await fetchUser('123');
    
    expect(user).toEqual({
      id: '123',
      name: '测试用户',
      email: 'test@example.com',
      role: 'user',
    });
  });

  it('处理 404 错误', async () => {
    server.use(
      rest.get('/api/users/999', (req, res, ctx) => {
        return res(ctx.status(404));
      })
    );

    await expect(fetchUser('999')).rejects.toThrow('用户不存在');
  });

  it('更新用户信息', async () => {
    const updates = { name: '新名字', email: 'new@example.com' };
    const updatedUser = await updateUser('123', updates);
    
    expect(updatedUser.name).toBe('新名字');
    expect(updatedUser.email).toBe('new@example.com');
  });
});

// 🔗 状态管理集成测试
// src/store/userStore.integration.test.ts
import { renderHook, act } from '@testing-library/react';
import { useUserStore } from './userStore';
import { server } from '@/mocks/server';

describe('User Store Integration', () => {
  beforeAll(() => server.listen());
  afterEach(() => {
    server.resetHandlers();
    // 重置 store 状态
    const { result } = renderHook(() => useUserStore());
    act(() => result.current.reset());
  });
  afterAll(() => server.close());

  it('登录流程', async () => {
    const { result } = renderHook(() => useUserStore());

    expect(result.current.user).toBeNull();
    expect(result.current.isLoading).toBe(false);

    // 执行登录
    await act(async () => {
      await result.current.login('test@example.com', 'password');
    });

    // 验证登录结果
    expect(result.current.user).toEqual({
      id: '123',
      name: '测试用户',
      email: 'test@example.com',
    });
    expect(result.current.isLoading).toBe(false);
  });

  it('登录失败处理', async () => {
    // 模拟登录失败
    server.use(
      rest.post('/api/login', (req, res, ctx) => {
        return res(ctx.status(401), ctx.json({ error: '认证失败' }));
      })
    );

    const { result } = renderHook(() => useUserStore());

    await act(async () => {
      await expect(
        result.current.login('wrong@example.com', 'wrong')
      ).rejects.toThrow('认证失败');
    });

    expect(result.current.user).toBeNull();
    expect(result.current.error).toBe('认证失败');
  });
});

3. E2E 测试:Playwright 🌍

为什么需要 E2E 测试?

E2E 测试模拟真实用户行为,验证整个应用程序从开始到结束的工作流程。

解决的问题:

  • 验证完整的用户流程
  • 发现集成和环境相关问题
  • 确保关键业务功能正常工作

缺点与限制:

  • 执行速度最慢
  • 测试脆弱,容易受 UI 变化影响
  • 调试和维护成本较高
详细配置与示例
// 🌍 Playwright 配置文件
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [
    ['html'],
    ['json', { outputFile: 'test-results.json' }],
  ],
  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    {
      name: 'mobile-chrome',
      use: { ...devices['Pixel 5'] },
    },
  ],
  webServer: {
    command: 'npm run start',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

// 🌍 关键用户流程测试
// e2e/critical-flows.spec.ts
import { test, expect } from '@playwright/test';

test.describe('关键用户流程', () => {
  test('用户完整注册流程', async ({ page }) => {
    // 1. 访问首页
    await page.goto('/');
    await expect(page).toHaveTitle('我的应用');
    
    // 2. 导航到注册页
    await page.click('text=注册');
    await expect(page).toHaveURL(/.*\/register/);
    
    // 3. 填写注册表单
    await page.fill('[data-testid="email"]', 'test@example.com');
    await page.fill('[data-testid="password"]', 'Password123!');
    await page.fill('[data-testid="confirmPassword"]', 'Password123!');
    await page.fill('[data-testid="fullName"]', '测试用户');
    
    // 4. 提交表单
    await page.click('button[type="submit"]');
    
    // 5. 验证重定向和成功消息
    await expect(page).toHaveURL(/.*\/dashboard/);
    await expect(page.locator('[data-testid="welcome-message"]'))
      .toContainText('欢迎,测试用户');
  });

  test('购物车完整流程', async ({ page }) => {
    await page.goto('/products');
    
    // 1. 浏览商品
    await expect(page.locator('[data-testid="product-list"]')).toBeVisible();
    
    // 2. 搜索商品
    await page.fill('[data-testid="search-input"]', '笔记本电脑');
    await page.click('[data-testid="search-button"]');
    
    // 3. 添加商品到购物车
    const firstProduct = page.locator('[data-testid="product-item"]').first();
    await firstProduct.locator('[data-testid="add-to-cart"]').click();
    
    // 验证购物车数量更新
    await expect(page.locator('[data-testid="cart-count"]')).toContainText('1');
    
    // 4. 前往购物车
    await page.click('[data-testid="cart-icon"]');
    await expect(page).toHaveURL(/.*\/cart/);
    
    // 5. 验证购物车内容
    await expect(page.locator('[data-testid="cart-item"]')).toHaveCount(1);
    await expect(page.locator('[data-testid="total-price"]')).toBeVisible();
    
    // 6. 结账流程
    await page.click('text=去结账');
    await expect(page).toHaveURL(/.*\/checkout/);
    
    // 7. 填写配送信息
    await page.fill('[data-testid="shipping-name"]', '收货人');
    await page.fill('[data-testid="shipping-address"]', '收货地址');
    await page.fill('[data-testid="shipping-phone"]', '13800138000');
    
    // 8. 选择支付方式并提交订单
    await page.click('[data-testid="payment-method-alipay"]');
    await page.click('[data-testid="place-order"]');
    
    // 9. 验证订单完成
    await expect(page).toHaveURL(/.*\/order-success/);
    await expect(page.locator('[data-testid="success-message"]'))
      .toContainText('订单提交成功');
  });

  test('用户登录和权限控制', async ({ page }) => {
    // 1. 访问受保护页面
    await page.goto('/dashboard');
    
    // 2. 验证重定向到登录页
    await expect(page).toHaveURL(/.*\/login/);
    
    // 3. 登录
    await page.fill('[data-testid="email"]', 'user@example.com');
    await page.fill('[data-testid="password"]', 'password');
    await page.click('button[type="submit"]');
    
    // 4. 验证成功登录并重定向
    await expect(page).toHaveURL(/.*\/dashboard/);
    
    // 5. 验证用户菜单显示
    await page.click('[data-testid="user-menu"]');
    await expect(page.locator('[data-testid="user-name"]'))
      .toContainText('当前用户');
  });
});

// 🌍 页面对象模型 (Page Object Model)
// e2e/pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly submitButton: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.locator('[data-testid="email"]');
    this.passwordInput = page.locator('[data-testid="password"]');
    this.submitButton = page.locator('button[type="submit"]');
    this.errorMessage = page.locator('[data-testid="error-message"]');
  }

  async goto() {
    await this.page.goto('/login');
  }

  async login(email: string, password: string) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
  }

  async getErrorMessage() {
    return this.errorMessage.textContent();
  }
}

// 🌍 使用页面对象的测试
// e2e/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';

test.describe('登录功能', () => {
  let loginPage: LoginPage;

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    await loginPage.goto();
  });

  test('成功登录', async ({ page }) => {
    await loginPage.login('user@example.com', 'password');
    
    await expect(page).toHaveURL(/.*\/dashboard/);
    await expect(page.locator('[data-testid="welcome-message"]')).toBeVisible();
  });

  test('登录失败显示错误信息', async () => {
    await loginPage.login('wrong@example.com', 'wrong');
    
    const errorMessage = await loginPage.getErrorMessage();
    expect(errorMessage).toContain('邮箱或密码错误');
  });

  test('表单验证', async () => {
    await loginPage.login('', '');
    
    await expect(loginPage.emailInput).toHaveAttribute('aria-invalid', 'true');
    await expect(loginPage.passwordInput).toHaveAttribute('aria-invalid', 'true');
  });
});

// 🌍 CI 集成配置
// .github/workflows/e2e.yml
name: E2E Tests
on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Install Playwright
        run: npx playwright install --with-deps
        
      - name: Build application
        run: npm run build
        
      - name: Run E2E tests
        run: npx playwright test
        
      - name: Upload test results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

4. 视觉回归测试 🖼️

为什么需要视觉测试?

视觉测试确保 UI 组件在不同版本间保持一致的视觉外观,捕捉意外的样式变化。

解决的问题:

  • 检测意外的视觉回归
  • 确保跨浏览器一致性
  • 验证响应式设计

缺点与限制:

  • 对微小变化敏感,可能产生误报
  • 需要维护基线图片
  • 执行速度较慢
详细配置与示例
// 🖼️ Storybook 配置
// .storybook/main.js
module.exports = {
  stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
  addons: [
    '@storybook/addon-essentials',
    '@storybook/addon-a11y',
    '@storybook/addon-interactions',
    '@storybook/addon-viewport',
  ],
  framework: '@storybook/react-vite',
  typescript: {
    check: false,
    reactDocgen: 'react-docgen-typescript',
  },
  staticDirs: ['../public'],
};

// 🖼️ 组件 Stories
// src/components/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
  title: 'UI/Button',
  component: Button,
  parameters: {
    layout: 'centered',
    chromatic: { 
      disable: false,
      viewports: [375, 768, 1200],
    },
  },
  tags: ['autodocs'],
  argTypes: {
    variant: {
      control: { type: 'select' },
      options: ['primary', 'secondary', 'danger'],
    },
    size: {
      control: { type: 'select' },
      options: ['small', 'medium', 'large'],
    },
    disabled: {
      control: { type: 'boolean' },
    },
    loading: {
      control: { type: 'boolean' },
    },
  },
};

export default meta;
type Story = StoryObj<typeof Button>;

export const Primary: Story = {
  args: {
    variant: 'primary',
    children: '主要按钮',
  },
};

export const Secondary: Story = {
  args: {
    variant: 'secondary',
    children: '次要按钮',
  },
};

export const Danger: Story = {
  args: {
    variant: 'danger',
    children: '危险操作',
  },
};

export const Small: Story = {
  args: {
    size: 'small',
    children: '小按钮',
  },
};

export const Large: Story = {
  args: {
    size: 'large',
    children: '大按钮',
  },
};

export const Disabled: Story = {
  args: {
    disabled: true,
    children: '禁用按钮',
  },
};

export const Loading: Story = {
  args: {
    loading: true,
    children: '加载中',
  },
};

// 🖼️ 交互测试 Stories
// src/components/Modal/Modal.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within, expect } from '@storybook/test';
import { Modal } from './Modal';

const meta: Meta<typeof Modal> = {
  title: 'UI/Modal',
  component: Modal,
  parameters: {
    layout: 'centered',
  },
};

export default meta;
type Story = StoryObj<typeof Modal>;

export const Default: Story = {
  args: {
    title: '示例弹窗',
    children: '这是弹窗的内容',
    isOpen: true,
  },
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    
    // 验证弹窗标题
    await expect(canvas.getByText('示例弹窗')).toBeInTheDocument();
    
    // 验证弹窗内容
    await expect(canvas.getByText('这是弹窗的内容')).toBeInTheDocument();
  },
};

export const WithInteractions: Story = {
  args: {
    title: '交互测试',
    children: '点击关闭按钮应该关闭弹窗',
    isOpen: true,
  },
  play: async ({ canvasElement, args }) => {
    const canvas = within(canvasElement);
    
    // 点击关闭按钮
    const closeButton = canvas.getByLabelText('关闭');
    await userEvent.click(closeButton);
    
    // 验证 onClose 被调用
    await expect(args.onClose).toHaveBeenCalled();
  },
};

// 🖼️ Chromatic 配置
// .storybook/chromatic.config.js
import { defineConfig } from 'chromatic';

export default defineConfig({
  projectId: 'your-project-id',
  storybook: {
    build: {
      outputDir: 'storybook-static',
    },
  },
  // 只在 main 分支上自动接受更改
  autoAcceptChanges: process.env.BRANCH === 'main',
  // 设置视觉测试的阈值
  diffThreshold: 0.2,
  // 需要手动审核的 stories
  storiesToReview: [
    'UI/Button--Primary',
    'UI/Modal--Default',
  ],
});

// 🖼️ package.json 脚本
{
  "scripts": {
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build",
    "chromatic": "chromatic --exit-zero-on-changes",
    "test:visual": "npm run build-storybook && chromatic"
  }
}

// 🖼️ CI 集成配置
// .github/workflows/visual.yml
name: Visual Tests
on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  visual:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          
      - uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Build Storybook
        run: npm run build-storybook
        
      - name: Publish to Chromatic
        uses: chromaui/action@v1
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
          exitOnceUploaded: true
          autoAcceptChanges: ${{ github.ref == 'refs/heads/main' }}

工程规范 📋

1. ESLint 配置 🛡️

为什么需要 ESLint?

ESLint 通过静态分析识别代码中的问题和模式违规,确保代码质量和一致性。

解决的问题:

  • 强制执行编码标准
  • 提前发现潜在错误
  • 保持代码风格一致性

缺点与限制:

  • 配置复杂,学习曲线较陡
  • 可能产生误报或漏报
  • 严格的规则可能影响开发速度
详细配置示例
// .eslintrc.js
module.exports = {
  root: true,
  parser: '@typescript-eslint/parser',
  parserOptions: {
    project: './tsconfig.json',
    tsconfigRootDir: __dirname,
    ecmaVersion: 2022,
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true,
    },
  },
  env: {
    browser: true,
    es2022: true,
    node: true,
    jest: true,
  },
  extends: [
    // ESLint 推荐规则
    'eslint:recommended',
    
    // TypeScript 规则
    '@typescript-eslint/recommended',
    '@typescript-eslint/recommended-requiring-type-checking',
    
    // React 规则
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    
    // 可访问性规则
    'plugin:jsx-a11y/recommended',
    
    // 导入排序规则
    'plugin:import/errors',
    'plugin:import/warnings',
    'plugin:import/typescript',
    
    // Prettier 兼容(必须放在最后)
    'prettier',
  ],
  plugins: [
    '@typescript-eslint',
    'react',
    'react-hooks',
    'jsx-a11y',
    'import',
    'prettier',
  ],
  rules: {
    // TypeScript 规则
    '@typescript-eslint/no-unused-vars': 'error',
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/explicit-module-boundary-types': 'off',
    '@typescript-eslint/no-explicit-any': 'warn',
    '@typescript-eslint/prefer-const': 'error',
    '@typescript-eslint/no-floating-promises': 'error',
    '@typescript-eslint/await-thenable': 'error',
    
    // React 规则
    'react/react-in-jsx-scope': 'off',
    'react/prop-types': 'off',
    'react-hooks/rules-of-hooks': 'error',
    'react-hooks/exhaustive-deps': 'warn',
    
    // 导入规则
    'import/order': [
      'error',
      {
        groups: [
          'builtin',
          'external',
          'internal',
          'parent',
          'sibling',
          'index',
        ],
        'newlines-between': 'always',
        alphabetize: { 
          order: 'asc',
          caseInsensitive: true,
        },
      },
    ],
    'import/no-unresolved': 'error',
    'import/no-cycle': 'error',
    
    // 代码质量规则
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
    'prefer-const': 'error',
    'no-var': 'error',
    'object-shorthand': 'error',
    'prefer-template': 'error',
    
    // Prettier 集成
    'prettier/prettier': 'error',
  },
  settings: {
    react: {
      version: 'detect',
    },
    'import/resolver': {
      typescript: {
        alwaysTryTypes: true,
      },
      node: {
        paths: ['src'],
        extensions: ['.js', '.jsx', '.ts', '.tsx'],
      },
    },
  },
  overrides: [
    {
      files: ['**/*.test.{js,jsx,ts,tsx}'],
      env: {
        jest: true,
      },
      rules: {
        '@typescript-eslint/no-explicit-any': 'off',
      },
    },
    {
      files: ['**/*.stories.{js,jsx,ts,tsx}'],
      rules: {
        'import/no-anonymous-default-export': 'off',
      },
    },
  ],
};

// 自定义 ESLint 规则示例
// eslint-plugin-custom-rules/index.js
module.exports = {
  rules: {
    'no-relative-imports': {
      meta: {
        type: 'problem',
        docs: {
          description: '禁止使用相对路径导入',
          category: 'Best Practices',
          recommended: true,
        },
        messages: {
          noRelativeImports: '请使用绝对路径导入,避免使用相对路径',
        },
      },
      create(context) {
        return {
          ImportDeclaration(node) {
            const importPath = node.source.value;
            
            // 检查是否是相对路径
            if (importPath.startsWith('.')) {
              context.report({
                node,
                messageId: 'noRelativeImports',
              });
            }
          },
        };
      },
    },
    
    'no-hardcoded-colors': {
      meta: {
        type: 'problem',
        docs: {
          description: '禁止硬编码颜色值',
          category: 'Best Practices',
          recommended: true,
        },
        messages: {
          noHardcodedColors: '请使用设计系统中的颜色变量,避免硬编码颜色值',
        },
      },
      create(context) {
        return {
          Literal(node) {
            const value = node.value;
            const colorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})|rgb|hsl|rgba|hsla/i;
            
            if (typeof value === 'string' && colorRegex.test(value)) {
              context.report({
                node,
                messageId: 'noHardcodedColors',
              });
            }
          },
        };
      },
    },
  },
};

2. Prettier 配置 🧹

为什么需要 Prettier?

Prettier 自动格式化代码,确保团队代码风格一致,减少格式争议。

解决的问题:

  • 自动统一代码风格
  • 减少代码审查中的格式讨论
  • 提高代码可读性

缺点与限制:

  • 某些自定义格式可能无法配置
  • 可能与现有代码风格冲突
  • 需要团队适应自动化格式
详细配置示例
// .prettierrc.js
module.exports = {
  // 每行最大字符数
  printWidth: 100,
  
  // 缩进使用空格数
  tabWidth: 2,
  
  // 使用空格而不是制表符
  useTabs: false,
  
  // 语句末尾添加分号
  semi: true,
  
  // 使用单引号
  singleQuote: true,
  
  // 对象属性引号使用方式
  quoteProps: 'as-needed',
  
  // JSX 中使用单引号
  jsxSingleQuote: true,
  
  // 尾随逗号(ES5 标准)
  trailingComma: 'es5',
  
  // 对象花括号内的空格
  bracketSpacing: true,
  
  // JSX 标签的闭合括号位置
  bracketSameLine: false,
  
  // 箭头函数参数括号
  arrowParens: 'avoid',
  
  // 格式化范围
  rangeStart: 0,
  rangeEnd: Infinity,
  
  // 不需要在文件顶部添加 @format 标记
  requirePragma: false,
  
  // 不插入 @format 标记
  insertPragma: false,
  
  // 折行标准
  proseWrap: 'preserve',
  
  // HTML 空白敏感性
  htmlWhitespaceSensitivity: 'css',
  
  // Vue 文件脚本和样式标签缩进
  vueIndentScriptAndStyle: false,
  
  // 换行符
  endOfLine: 'lf',
  
  // 嵌入式语言格式化
  embeddedLanguageFormatting: 'auto',
  
  // 单个属性时的括号
  singleAttributePerLine: false,
};

// Prettier 忽略文件
// .prettierignore
# 依赖目录
node_modules/
dist/
build/

# 生成的文件
coverage/
*.log

# 配置文件
*.config.js

# 锁文件
package-lock.json
yarn.lock

# 文档
*.md
*.mdx

# 图片和字体
*.png
*.jpg
*.jpeg
*.gif
*.svg
*.woff
*.woff2

// package.json 中的格式化脚本
{
  "scripts": {
    "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,md,mdx,css,scss}\"",
    "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,md,mdx,css,scss}\"",
    "format:staged": "lint-staged"
  }
}

3. Git Hooks 配置 🪝

为什么需要 Git Hooks?

Git Hooks 在代码提交和推送前自动运行检查,防止低质量代码进入仓库。

解决的问题:

  • 自动化代码质量检查
  • 强制执行代码标准
  • 减少 CI 失败次数

缺点与限制:

  • 可能减慢开发流程
  • 需要团队统一配置
  • 复杂的钩子可能难以调试
详细配置示例
// package.json 中的 Husky 配置
{
  "scripts": {
    "prepare": "husky install",
    "lint": "eslint src --ext .ts,.tsx,.js,.jsx --max-warnings 0",
    "lint:fix": "npm run lint -- --fix",
    "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,md,mdx,css,scss}\"",
    "typecheck": "tsc --noEmit",
    "test": "vitest",
    "test:ci": "vitest run --coverage",
    "validate": "npm run lint && npm run typecheck && npm run test:ci"
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md,mdx,css,scss,yml,yaml}": [
      "prettier --write"
    ],
    "*.{ts,tsx}": [
      "bash -c 'npm run typecheck'"
    ]
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged",
      "pre-push": "npm run test:ci",
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
  }
}

// commitlint 配置
// .commitlint.config.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [
      2,
      'always',
      [
        'feat',     // 新功能
        'fix',      // 修复 bug
        'docs',     // 文档更新
        'style',    // 代码格式调整
        'refactor', // 代码重构
        'test',     // 测试相关
        'chore',    // 构建过程或辅助工具变动
        'perf',     // 性能优化
        'ci',       // CI 配置变更
        'revert',   // 回滚提交
      ],
    ],
    'type-case': [2, 'always', 'lower-case'],
    'type-empty': [2, 'never'],
    'scope-case': [2, 'always', 'lower-case'],
    'subject-empty': [2, 'never'],
    'subject-full-stop': [2, 'never', '.'],
    'header-max-length': [2, 'always', 100],
  },
};

// 手动设置 Git Hooks(Husky v8+)
// .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm run lint-staged

// .husky/pre-push
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm run test:ci

// .husky/commit-msg
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx --no -- commitlint --edit "$1"

// 自定义 Git Hook 脚本示例
// scripts/pre-commit-check.sh
#!/bin/bash

# 检查是否有未解决的合并冲突
if git grep -l '<<<<<<<' -- ':(exclude)package-lock.json' | grep -q .; then
  echo "错误: 发现未解决的合并冲突"
  git grep -l '<<<<<<<' -- ':(exclude)package-lock.json'
  exit 1
fi

# 检查调试语句
if git diff --cached --name-only | xargs grep -l 'console.log\|debugger' | grep -q .; then
  echo "警告: 发现调试语句"
  git diff --cached --name-only | xargs grep -l 'console.log\|debugger'
  read -p "是否继续提交? (y/n) " -n 1 -r
  echo
  if [[ ! $REPLY =~ ^[Yy]$ ]]; then
    exit 1
  fi
fi

# 检查文件大小
MAX_FILE_SIZE=5242880 # 5MB
for file in $(git diff --cached --name-only); do
  if [ -f "$file" ]; then
    size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null)
    if [ "$size" -gt "$MAX_FILE_SIZE" ]; then
      echo "错误: 文件 $file 过大 ($size 字节),最大允许 $MAX_FILE_SIZE 字节"
      exit 1
    fi
  fi
done

echo "预提交检查通过"
exit 0

4. Commitizen 标准化提交 ✍️

为什么需要标准化提交?

标准化提交信息便于自动化生成变更日志、版本管理和代码审查。

解决的问题:

  • 统一的提交信息格式
  • 自动化版本管理
  • 清晰的变更历史

缺点与限制:

  • 需要团队成员适应新流程
  • 可能增加提交的复杂性
  • 某些简单修改可能显得过度正式
详细配置示例
// .cz-config.js
module.exports = {
  types: [
    { value: 'feat', name: 'feat:     新功能' },
    { value: 'fix', name: 'fix:      修复 bug' },
    { value: 'docs', name: 'docs:     文档更新' },
    { value: 'style', name: 'style:    代码格式调整(不影响功能)' },
    { value: 'refactor', name: 'refactor: 代码重构(既不是新功能也不是修复 bug)' },
    { value: 'perf', name: 'perf:     性能优化' },
    { value: 'test', name: 'test:     测试相关' },
    { value: 'chore', name: 'chore:    构建过程或辅助工具变动' },
    { value: 'ci', name: 'ci:        CI 配置变更' },
    { value: 'revert', name: 'revert:   回滚提交' },
  ],
  
  scopes: [
    { name: 'ui', description: '用户界面相关' },
    { name: 'api', description: 'API 相关' },
    { name: 'auth', description: '认证授权相关' },
    { name: 'database', description: '数据库相关' },
    { name: 'config', description: '配置相关' },
    { name: 'deps', description: '依赖更新' },
    { name: 'other', description: '其他' },
  ],
  
  messages: {
    type: '选择提交类型:',
    scope: '选择影响范围 (可选):',
    customScope: '输入自定义范围:',
    subject: '简短描述(必填):\n',
    body: '详细描述(可选). 使用 "|" 换行:\n',
    breaking: '破坏性变化说明(可选):\n',
    footer: '关联关闭的 issue(可选). 例如: #31, #34:\n',
    confirmCommit: '确认提交?',
  },
  
  allowCustomScopes: true,
  allowBreakingChanges: ['feat', 'fix'],
  skipQuestions: ['body', 'footer'],
  subjectLimit: 100,
  
  // 范围验证
  scopeOverrides: {
    fix: [
      { name: 'merge' },
      { name: 'style' },
      { name: 'e2eTest' },
      { name: 'unitTest' },
    ],
  },
};

// 提交信息验证脚本
// scripts/verify-commit-msg.js
const fs = require('fs');
const path = require('path');

// 获取提交信息
const commitMsgFile = process.argv[2];
const commitMsg = fs.readFileSync(commitMsgFile, 'utf8').trim();

// 提交信息格式正则
const commitRegex = /^(feat|fix|docs|style|refactor|perf|test|chore|ci|revert)(\([^)]+\))?: .{1,100}/;

if (!commitRegex.test(commitMsg)) {
  console.error(`
    提交信息格式错误!
    
    正确格式: <type>(<scope>): <subject>
    
    示例:
    - feat(auth): 添加用户登录功能
    - fix(ui): 修复按钮点击无效的问题
    - docs: 更新 README 文档
    
    允许的类型:
    - feat:     新功能
    - fix:      修复 bug
    - docs:     文档更新
    - style:    代码格式调整
    - refactor: 代码重构
    - perf:     性能优化
    - test:     测试相关
    - chore:    构建过程或辅助工具变动
    - ci:       CI 配置变更
    - revert:   回滚提交
  `);
  process.exit(1);
}

console.log('✅ 提交信息格式正确');
process.exit(0);

// 自动化版本管理和变更日志生成
// .versionrc.js
module.exports = {
  types: [
    { type: 'feat', section: '新功能' },
    { type: 'fix', section: 'Bug 修复' },
    { type: 'docs', section: '文档' },
    { type: 'style', section: '代码风格' },
    { type: 'refactor', section: '代码重构' },
    { type: 'perf', section: '性能优化' },
    { type: 'test', section: '测试' },
    { type: 'chore', section: '构建工具' },
    { type: 'ci', section: 'CI 配置' },
    { type: 'revert', section: '回滚' },
  ],
  commitUrlFormat: '{{host}}/{{owner}}/{{repository}}/commit/{{hash}}',
  compareUrlFormat: '{{host}}/{{owner}}/{{repository}}/compare/{{previousTag}}...{{currentTag}}',
  issueUrlFormat: '{{host}}/{{owner}}/{{repository}}/issues/{{id}}',
  userUrlFormat: '{{host}}/{{user}}',
};

// package.json 中的相关脚本
{
  "scripts": {
    "commit": "cz",
    "commit:retry": "git add . && cz --retry",
    "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
    "release": "standard-version",
    "release:minor": "standard-version --release-as minor",
    "release:major": "standard-version --release-as major"
  },
  "config": {
    "commitizen": {
      "path": "./node_modules/cz-conventional-changelog"
    }
  }
}

总结 🎯

通过实施完整的代码质量工程体系,团队可以获得以下收益:

核心优势

  1. 类型安全 - 减少运行时错误,提高代码可靠性
  2. 测试覆盖 - 确保功能正确性,支持持续重构
  3. 规范统一 - 提高代码可读性和可维护性
  4. 自动化流程 - 减少人为错误,提高开发效率

实施建议

  1. 渐进式采用 - 从最急需的环节开始,逐步推广
  2. 团队培训 - 确保所有成员理解并认可质量工程的价值
  3. 持续优化 - 定期回顾和改进质量工程实践
  4. 工具整合 - 将质量检查集成到开发工作流中

成功指标

  • 类型检查通过率 100%
  • 测试覆盖率 > 80%
  • CI/CD 流水线通过率 > 95%
  • 代码审查反馈周期缩短
  • 生产环境 bug 数量显著减少

通过系统化地实施这些代码质量工程实践,团队可以构建出更加健壮、可维护且高质量的软件产品。

昨天以前首页

前端原型与继承全景学习图解版

作者 Holin_浩霖
2025年10月6日 22:57

🧭 前端原型与继承全景学习图解版(整合 + 深入底层原理)


1. 为什么会有“原型链”?(设计动机 & 用处)

背景(一句话) :JavaScript 在早期没有“类(class)”语法时,需要一种轻量、动态的方式让对象复用逻辑 —— 所以用“对象指向对象”的方式实现继承(prototype-based)。

主要用处

  • 方法复用:把方法放在原型上,所有实例共享,节省内存。
  • 动态扩展:运行时可以给原型新增方法,所有实例自动可用。
  • 实现继承:通过链式引用把“父对象”的行为借给子对象(实现“is-a”或“has-shared-behaviour”)。
  • 类型识别:通过 instanceofisPrototypeOf 来判断关系(基于原型链)。

直观比喻:每个对象背后有一个“模板(prototype)”当做影子,访问属性时先看自己,没找到就看影子,影子的影子,依此类推。


2. 核心三角:__proto__prototypeconstructor

function Person(name) {
  this.name = name;
}
const person = new Person('Alice');

ASCII 图(概念关系):

          ┌────────────────────┐
          │   构造函数 Person  │
          │────────────────────│
          │ function Person()  │
          │                    │
          │  ┌───────────────┐ │
          │  │ prototype 对象│◀─────────────┐
          │  └───────────────┘ │            │
          └────────────────────┘            │
                     ▲                      │
                     │ constructor          │
                     │                      │
          ┌────────────────────┐            │
          │  实例对象 person   │────────────┘
          │────────────────────│
          │ { name: "Alice" }  │
          │                    │
          │ __proto__ → Person.prototype
          └────────────────────┘

重要说明

  • prototype:只有函数(通常作为构造函数)有这个属性。它是用来放共享方法/属性的对象。
  • __proto__(规范名为内部槽 [[Prototype]]):每个对象都有,指向其原型对象
  • constructor:只是 prototype 上的一个普通属性,指回构造函数(可被覆盖,非“内置魔法”)。

3. 原型链总览(图 + 解释)

person (实例)
  ├─ own: name: "Alice"
  └─ __proto__ → Person.prototype
         ├─ sayName()
         └─ __proto__ → Object.prototype
                 ├─ toString()
                 ├─ hasOwnProperty()
                 └─ __proto__ → null

要点

  • Object.prototype 是绝大多数对象在原型链上的最终祖先(除了 Object.create(null) 创建的无原型对象)。
  • 访问属性时会沿这条链 逐级向上 搜索,直到找到或走到 null

4. 属性查找与赋值:底层行为(非常重要)

4.1 读取属性(概念化流程)

访问 obj.prop 时(简化伪逻辑):

  1. 如果 obj 本身有名为 propown(自有)属性(数据属性或访问器),返回它或调用其 getter(getter 中的 this 指向访问者,即 obj)。
  2. 否则,令 proto = Object.getPrototypeOf(obj)。若 protonull,返回 undefined
  3. proto 上重复步骤 1(即沿 __proto__ 向上查找)。
  4. 直到找到或到 null

ECMAScript 规范:这由内部方法 [[Get]] / OrdinaryGet / HasProperty 等实现。

4.2 写入属性(赋值)要小心

行为取决于是否存在 setter 以及赋值目标:

  • 若在原型链上某处找到一个 set(访问器属性),则调用该 setter,this 为赋值时的目标(receiver,通常是最初的 obj)。
  • 若没有 setter,赋值总是在 当前对象本身 创建/更新一个 own 数据属性(即会遮蔽原型上的同名属性),不会修改原型上的属性。

示例(展示 setter 行为):

const proto = {
  get x(){ return this._x || 0; },
  set x(v){ this._x = v; }
};
const obj = Object.create(proto);
obj.x = 5;             // 调用了 proto 上的 setter,this 指向 obj
console.log(obj._x);  // 5

4.3 property descriptor(属性描述符)

属性有两类:

  • 数据属性{ value, writable, enumerable, configurable }
  • 访问器(getter/setter)属性{ get, set, enumerable, configurable }

Object.getOwnPropertyDescriptor(obj, 'p') 查看。理解这些字段对调试原型问题非常关键(例如不可写/不可配置会阻止覆盖等)。

4.4 instanceof 的底层实现(伪代码)

obj instanceof Ctor 的逻辑大意是:检查 Ctor.prototype 是否出现在 obj 的原型链上(逐级比较 Object.getPrototypeOf(current))。可模拟:

function myInstanceof(obj, Ctor) {
  let proto = Object.getPrototypeOf(obj);
  const prototype = Ctor.prototype;
  while (proto !== null) {
    if (proto === prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}

5. 继承的五种实现(逐一看底层差别、优缺点与示例)

下面给出每种方式的实现原理图、代码与底层问题


5.1 原型链继承(Prototype Chain)

实现

function Parent(){ this.colors = ['red']; }
function Child(){}
Child.prototype = new Parent();

Child.prototype  === Parent 实例
    ↑
 child.__proto__

底层问题

  • Parent 构造被用于创建 Child.prototype(也就是 Parent 的实例),因此引用类型(colors)被共享给所有 child 实例。
  • 无法传参给 Parent。

适用场景:很少单独使用;教学用途。


5.2 构造函数继承(Constructor Borrowing)

实现

function Parent(name){ this.name = name; }
function Child(name){ Parent.call(this, name); }

核心:在子构造内部执行父构造,把父构造里的 own 属性复制到当前实例(this)。

优点:每个实例都有独立属性,能传参。
缺点:无法继承 Parent.prototype 上的方法(不能复用)。


5.3 组合继承(Combination)

实现

function Child(name){
  Parent.call(this, name);      // 继承属性(实例化)
}
Child.prototype = new Parent(); // 继承方法(但会再次执行 Parent)
Child.prototype.constructor = Child;

本质:把构造函数继承和原型链继承合并。
问题Parent 被调用两次(一次用于 Child.prototype = new Parent(),另一次用于 Parent.call(this)),导致性能或副作用(如果 Parent 初始化有开销或副作用)。


5.4 寄生组合继承(Parasitic Combination — 最优的 ES5 方式)

实现(推荐)

function inherit(Child, Parent) {
  Child.prototype = Object.create(Parent.prototype);
  Child.prototype.constructor = Child;
}

function Child(name){
  Parent.call(this, name); // 只调用一次
}
inherit(Child, Parent);

child.__proto__Child.prototype
                 └── __proto__ → Parent.prototype

优点

  • Parent 只执行一次(在 Parent.call(this)),避免两次调用;
  • 原型方法复用;
  • 属性独立。

结论:这是 ES5 下最常用、最合理的继承模式;ES6 class extends 本质上以此为基础实现语义。


5.5 ES6 class 继承(现代语法糖)

class Parent { ... }
class Child extends Parent {
  constructor() { super(); }
}

底层关系

  • Child.prototype.__proto__ === Parent.prototype(实例链)
  • Child.__proto__ === Parent(构造函数链,支持静态方法继承)

super() 行为

  • 在子类构造中必须先调用 super()(它执行父构造函数并设置 this)。
  • 在方法中使用 super.method() 会按 [[HomeObject]] 和原型关系查找父方法并以当前实例作为 this 调用。

优点:语义清晰、可读性高、同时支持私有字段、静态字段等现代特性。底层仍是原型链。


6. ES6 class 进阶细节(底层语义 & 私有字段)

6.1 class 与原型的等价关系

class 的实例方法被放到 prototype 上,静态方法直接放在构造函数上。类声明只是语法糖,背后仍是 function + prototype 的组合(但有更严格的内部行为,如类方法默认不可枚举、类构造函数不能被当作普通函数直接调用等)。

6.2 私有字段(#x

  • 私有字段不是在 prototype 上,它们是按实例存放在引擎内部私有字段表中(规范中为 PrivateFields)。外部无法通过 obj['#x'] 访问。
  • 私有字段的初始化发生在构造执行期间(类字段初始化阶段),并遵从 temporal behavior(不能在 super() 前使用 this)。

6.3 super 在方法中的查找(简述)

  • 每个类方法有内部 [[HomeObject]](home object)关联,用于 super 时确定查找起点。
  • super.foo() 实际上在父类原型上查找 foo,以当前实例作为 this 进行调用(即 super 的 receiver 是当前实例)。

7. 优缺点汇总 & 能否“打破”原型链?

7.1 优点回顾

  • 内存节约:方法放原型只存一份。
  • 动态特性:运行时可扩展、更灵活。
  • 自然继承:链式结构直观支持“继承”。

7.2 缺点回顾

  • 查找成本:每次属性访问可能沿链多次查找(层级深性能差)。
  • 调试难:属性来源可能在原型上,难以追踪。
  • 共享副作用:错误地把可变数据放在原型会被所有实例共享(常见坑)。
  • 变更影响范围大:改 prototype 会影响所有实例。

7.3 是否可以打破原型链?(可以,但需权衡)

方法 A:创建无原型对象

const dict = Object.create(null);

dict__proto__ === null,没有 hasOwnPropertytoString 等方法,适合“纯字典”。

方法 B:把某对象的原型设为 null

Object.setPrototypeOf(obj, null); // 或 obj.__proto__ = null

→ 断开继承链,但会失去默认行为且性能差(Object.setPrototypeOf 很慢,应避免在热路径中使用)。

方法 C(不是真正“打破”,只是限制)

  • Object.freeze(Object.prototype) 可以防止在 Object.prototype 加入新属性(但不会移除已有原型链),通常不常用。

结论:可以打破(Object.create(null) 最常用),但通常不建议随意打断原型链。现代代码更倾向用模块化、组合(composition)替代复杂的深继承。


8. 常用命令/查验速查表(代码片段)

// 获取原型
Object.getPrototypeOf(obj);

// 检查 own 属性
obj.hasOwnProperty('p');

// 检查 in(会查原型)
'p' in obj;

// instanceof
obj instanceof Constructor;

// 创建以 proto 为原型的新对象
const o = Object.create(proto);

// 设置原型(慢)
Object.setPrototypeOf(obj, proto);

// 查看 own property 描述符
Object.getOwnPropertyDescriptor(obj, 'p');

// 列出 own keys (不包含原型)
Object.getOwnPropertyNames(obj);
Object.keys(obj);

// 列出 prototype 上的方法
Object.getOwnPropertyNames(Object.getPrototypeOf(obj));

9. 调试建议与练习题

调试技巧

  • 在浏览器控制台使用 console.dir(obj),展开查看 __proto__ 链。
  • 使用 Object.getPrototypeOf(obj) 明确获取原型而不是 __proto__(后者为非标准但广泛支持)。
  • Object.getOwnPropertyDescriptors(obj) 一次查看所有 own 属性及其 descriptor。
  • 使用 Reflect.get(obj, 'p', receiver)Reflect.set 理解 getter/setter 的 receiver 行为。

练习题(推荐动手)

  1. 写出 myInstanceof(obj, Ctor) 的实现(见 §4)。
  2. Object.create 实现一个继承 helper 并验证 constructor 指向是否正确。
  3. 比较实例在以下两种写法下内存差异:方法定义在构造函数 vs 方法定义在 prototype 上(创建大量实例观察内存/性能)。
  4. 实现一个类 BankAccount(私有字段)并尝试从外部访问私有字段(应报错)。

10. 总结(学习路线与建议)

  • 先理解三角关系(实例 → __proto__prototypeconstructor),再理解原型链查找(read)与赋值(write)差异。
  • 优先用寄生组合继承(ES5)或 class extends(ES6) 来实现继承。
  • 把可变状态放构造函数把方法放 prototype/class method,避免共享引用陷阱。
  • 不要频繁修改原型链Object.setPrototypeOf 对性能影响大)。
  • 用组合优于继承的思路(composition over inheritance)减少复杂继承层次。

附:三张 ASCII 图(可直接复制到你的 Markdown)

1) 原型链结构总览图

📦 person (实例)
│
├── name: "Alice"           // own property
│
└── __proto__ → Person.prototype
         ├── sayName()      // prototype 方法
         └── __proto__ → Object.prototype
                 ├── toString()
                 └── __proto__ → null

2) 继承演化路线图

Prototype Chain  ──> Constructor Borrowing ──> Combination
       (早期)               (能传参)               (两次调用)
               ↓
    Parasitic Combination (Object.create + call)  ←── 推荐(ES5)
               ↓
           ES6 class extends (语法糖,底层相同)

3) 查找机制流程图

访问 obj.prop
   │
   ▼
obj 是否有 own prop?
 ├─ 是:直接返回(若为 getter,则调用 getter,this 指 obj)
 └─ 否:proto = Object.getPrototypeOf(obj)
       ├─ proto === null → 返回 undefined
       └─ 在 proto 上重复同样步骤(直到找到或到 null
❌
❌