fix: 将 code-review-skill 从子模块转为普通目录
This commit is contained in:
@@ -0,0 +1,419 @@
|
||||
# Angular Code Review Guide
|
||||
|
||||
> Angular 17+ 代码审查指南,覆盖 Signals、Standalone 组件、RxJS 反模式、Zoneless 变更检测、模板最佳实践及性能优化等核心主题。
|
||||
|
||||
## 目录
|
||||
|
||||
- [Signals 与变更检测](#signals-与变更检测)
|
||||
- [Standalone 组件迁移](#standalone-组件迁移)
|
||||
- [RxJS 反模式](#rxjs-反模式)
|
||||
- [Zoneless 变更检测](#zoneless-变更检测)
|
||||
- [模板最佳实践](#模板最佳实践)
|
||||
- [性能优化](#性能优化)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## Signals 与变更检测
|
||||
|
||||
### Signal + OnPush 自动触发变更检测
|
||||
|
||||
```typescript
|
||||
// ❌ 可变状态 + OnPush = 界面不更新
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `<p>{{ data.name }}</p>`,
|
||||
})
|
||||
export class UserProfile {
|
||||
data = { name: 'Alice' };
|
||||
changeName() { this.data.name = 'Bob'; } // UI 不会更新!
|
||||
}
|
||||
|
||||
// ✅ Signal + OnPush = 自动变更检测
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `<p>{{ name() }}</p>`,
|
||||
})
|
||||
export class UserProfile {
|
||||
name = signal('Alice');
|
||||
changeName() { this.name.set('Bob'); } // 自动触发 CD
|
||||
}
|
||||
```
|
||||
|
||||
### @Input() 对象变异不会被 OnPush 检测
|
||||
|
||||
```typescript
|
||||
// ❌ 变异 Input 对象——引用不变,OnPush 不检测
|
||||
@Input() config!: Config;
|
||||
updateConfig() { this.config.theme = 'dark'; }
|
||||
|
||||
// ✅ 创建新引用
|
||||
updateConfig() { this.config = { ...this.config, theme: 'dark' }; }
|
||||
```
|
||||
|
||||
### computed() 用于派生状态
|
||||
|
||||
```typescript
|
||||
// ❌ effect 用于同步状态——反模式,可能触发额外 CD 周期
|
||||
export class CartComponent {
|
||||
total = signal(0);
|
||||
discounted = signal(0);
|
||||
|
||||
constructor() {
|
||||
effect(() => this.discounted.set(this.total() * 0.9));
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ computed 用于派生状态——惰性计算,无副作用
|
||||
export class CartComponent {
|
||||
total = signal(0);
|
||||
discounted = computed(() => this.total() * 0.9);
|
||||
}
|
||||
```
|
||||
|
||||
### effect() 中 Signal 读取在 await 后不会被追踪
|
||||
|
||||
```typescript
|
||||
// ❌ await 之后读取 Signal——依赖未被追踪
|
||||
effect(async () => {
|
||||
const data = await fetchUserData();
|
||||
console.log(`Theme: ${theme()}`); // theme() 未被追踪!
|
||||
});
|
||||
|
||||
// ✅ 在 await 之前同步读取
|
||||
effect(async () => {
|
||||
const currentTheme = theme(); // 同步读取,被追踪
|
||||
const data = await fetchUserData();
|
||||
console.log(`Theme: ${currentTheme}`);
|
||||
});
|
||||
```
|
||||
|
||||
### effect 只在特定场景使用
|
||||
|
||||
```typescript
|
||||
// ❌ 用 effect 同步两个 Signal——永远用 computed
|
||||
effect(() => { this.filtered.set(this.items().filter(i => i.active)); });
|
||||
|
||||
// ✅ effect 的合理场景:DOM 操作、分析日志、订阅外部源
|
||||
effect(() => {
|
||||
const canvas = this.canvasRef.nativeElement;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = this.color();
|
||||
ctx.fillRect(0, 0, this.size(), this.size());
|
||||
});
|
||||
|
||||
// 💡 "There are no situations where effect is good,
|
||||
// only situations where it is appropriate."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Standalone 组件迁移
|
||||
|
||||
### Angular 19+ standalone 是默认值
|
||||
|
||||
```typescript
|
||||
// ❌ Legacy NgModule 组件
|
||||
@Component({
|
||||
selector: 'old-component',
|
||||
standalone: false,
|
||||
})
|
||||
export class OldComponent {}
|
||||
|
||||
// ✅ 现代 Standalone 组件(Angular 19+ standalone 是默认值)
|
||||
@Component({
|
||||
selector: 'user-profile',
|
||||
imports: [ProfilePhoto, RouterLink],
|
||||
template: `<profile-photo /><a routerLink="/edit">Edit</a>`,
|
||||
})
|
||||
export class UserProfile {}
|
||||
```
|
||||
|
||||
### 审查标记
|
||||
|
||||
```typescript
|
||||
// ⚠️ 需要迁移的信号:
|
||||
// 1. standalone: false
|
||||
// 2. @NgModule declarations
|
||||
// 3. 组件通过 NgModule 而非直接 import
|
||||
|
||||
// ✅ 迁移路径:
|
||||
// 1. 删除 standalone: false
|
||||
// 2. 将依赖添加到组件的 imports 数组
|
||||
// 3. 如果不再有 declarations,删除 NgModule
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## RxJS 反模式
|
||||
|
||||
### subscribe() 必须配 takeUntilDestroyed
|
||||
|
||||
```typescript
|
||||
// ❌ 裸 subscribe——内存泄漏!组件销毁后仍继续接收数据
|
||||
@Component({ /* ... */ })
|
||||
export class UserProfile implements OnInit {
|
||||
ngOnInit() {
|
||||
this.data$.subscribe(data => this.processData(data));
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ takeUntilDestroyed——自动在组件销毁时取消(需在构造函数或注入上下文中调用)
|
||||
@Component({ /* ... */ })
|
||||
export class UserProfile {
|
||||
constructor() {
|
||||
this.data$.pipe(takeUntilDestroyed()).subscribe(data => {
|
||||
this.processData(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 在构造函数外使用——传入 DestroyRef
|
||||
@Component({ /* ... */ })
|
||||
export class UserProfile {
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
startListening() {
|
||||
this.data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(/* ... */);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### toSignal 优于 AsyncPipe
|
||||
|
||||
```typescript
|
||||
// ❌ AsyncPipe——需要导入,模板中有 | async
|
||||
@Component({
|
||||
imports: [AsyncPipe],
|
||||
template: `{{ data$ | async }}`,
|
||||
})
|
||||
|
||||
// ✅ toSignal——自动取消订阅,可在任何地方使用
|
||||
export class UserProfile {
|
||||
data = toSignal(this.data$, { initialValue: null });
|
||||
// 模板直接用 data()
|
||||
}
|
||||
```
|
||||
|
||||
### 避免重复 toSignal 调用
|
||||
|
||||
```typescript
|
||||
// ❌ toSignal 每次调用都创建新订阅
|
||||
getData() {
|
||||
return toSignal(this.http.get('/api/data'));
|
||||
}
|
||||
|
||||
// ✅ 存储结果
|
||||
data = toSignal(this.http.get('/api/data'), { initialValue: null });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Zoneless 变更检测
|
||||
|
||||
### 普通属性变异不会被检测(Angular 21+)
|
||||
|
||||
```typescript
|
||||
// ❌ Zoneless 下普通属性赋值不触发 CD
|
||||
export class UserService {
|
||||
user: User | null = null;
|
||||
loadUser() { this.user = fetchResult; } // 不触发!
|
||||
}
|
||||
|
||||
// ✅ Signal 自动触发 CD
|
||||
export class UserService {
|
||||
private _user = signal<User | null>(null);
|
||||
readonly user = this._user.asReadonly();
|
||||
loadUser() { this._user.set(fetchResult); }
|
||||
}
|
||||
```
|
||||
|
||||
### NgZone API 在 Zoneless 中失效
|
||||
|
||||
```typescript
|
||||
// ❌ NgZone.onStable 在 zoneless 中永远不会触发
|
||||
ngZone.onStable.subscribe(() => { /* 永远不触发 */ });
|
||||
|
||||
// ✅ 使用 afterNextRender
|
||||
afterNextRender({ write: () => { /* CD 之后执行 */ } });
|
||||
```
|
||||
|
||||
### Reactive Forms 变异需要 markForCheck
|
||||
|
||||
```typescript
|
||||
// ❌ Reactive Forms 的 setValue/patchValue 在 zoneless 中不自动调度 CD
|
||||
this.form.patchValue({ name: 'Alice' }); // UI 可能不更新
|
||||
|
||||
// ✅ 手动标记或通过 Signal 反映
|
||||
this.form.patchValue({ name: 'Alice' });
|
||||
this.cdr.markForCheck();
|
||||
```
|
||||
|
||||
### Zoneless 下有效的 CD 触发器
|
||||
|
||||
| 触发器 | 说明 |
|
||||
|--------|------|
|
||||
| `signal.set()` / `.update()` | Signal 更新自动触发 |
|
||||
| `ChangeDetectorRef.markForCheck()` | 手动标记 |
|
||||
| `ComponentRef.setInput()` | 输入绑定 |
|
||||
| 模板事件监听器回调 | 用户交互 |
|
||||
|
||||
---
|
||||
|
||||
## 模板最佳实践
|
||||
|
||||
### 复杂逻辑提取为 computed Signal
|
||||
|
||||
```typescript
|
||||
// ❌ 模板中复杂表达式
|
||||
template: `<div *ngIf="items.filter(i => i.active).length > 0 && user.role === 'admin'">`
|
||||
|
||||
// ✅ 提取为 computed
|
||||
filteredItems = computed(() => this.items().filter(i => i.active));
|
||||
shouldShow = computed(() => this.filteredItems().length > 0 && this.user().role === 'admin');
|
||||
template: `@if (shouldShow()) { <div>...</div> }`
|
||||
```
|
||||
|
||||
### 原生绑定优于 NgClass / NgStyle
|
||||
|
||||
```typescript
|
||||
// ❌ NgClass/NgStyle——额外指令开销
|
||||
template: `<div [ngClass]="{active: isActive}" [ngStyle]="{'color': textColor}">`
|
||||
|
||||
// ✅ 原生 class/style 绑定——性能更好
|
||||
template: `<div [class.active]="isActive" [style.color]="textColor">`
|
||||
```
|
||||
|
||||
### 模板专用成员标记 protected
|
||||
|
||||
```typescript
|
||||
// ❂ 模板专用方法暴露为 public
|
||||
export class UserProfile {
|
||||
formatName(name: string) { return name.trim(); }
|
||||
}
|
||||
|
||||
// ✅ 模板专用成员用 protected
|
||||
export class UserProfile {
|
||||
protected formatName(name: string) { return name.trim(); }
|
||||
}
|
||||
```
|
||||
|
||||
### Angular 管理的属性标记 readonly
|
||||
|
||||
```typescript
|
||||
// ❌ input/output/model 可被意外覆盖
|
||||
userId = input<string>();
|
||||
userSaved = output<void>();
|
||||
|
||||
// ✅ readonly 防止意外赋值
|
||||
readonly userId = input<string>();
|
||||
readonly userSaved = output<void>();
|
||||
readonly userName = model<string>();
|
||||
```
|
||||
|
||||
### 命名规范:操作名而非事件名
|
||||
|
||||
```typescript
|
||||
// ❌ 以事件命名
|
||||
template: `<button (click)="handleClick()">Save</button>`
|
||||
|
||||
// ✅ 以操作命名
|
||||
template: `<button (click)="saveUserData()">Save</button>`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能优化
|
||||
|
||||
### effect 是最后手段——优先 computed
|
||||
|
||||
```typescript
|
||||
// ❌ effect 用于状态同步——触发额外 CD,可能无限循环
|
||||
effect(() => {
|
||||
this.filteredItems.set(this.items().filter(i => i.active));
|
||||
});
|
||||
|
||||
// ✅ computed——惰性计算,无副作用,无额外 CD
|
||||
filteredItems = computed(() => this.items().filter(i => i.active));
|
||||
```
|
||||
|
||||
### afterRenderEffect 分离读写阶段
|
||||
|
||||
```typescript
|
||||
// ❌ 无阶段指定 = mixedReadWrite = 额外 DOM 回流
|
||||
afterRenderEffect(() => {
|
||||
const height = el.offsetHeight; // 读
|
||||
el.style.height = height + 10 + 'px'; // 写
|
||||
});
|
||||
|
||||
// ✅ 分离阶段减少回流
|
||||
afterRenderEffect({
|
||||
earlyRead: () => el.offsetHeight,
|
||||
write: (height) => { el.style.height = height() + 10 + 'px'; },
|
||||
read: () => verifyLayout(),
|
||||
});
|
||||
```
|
||||
|
||||
### inject() 优于构造函数注入
|
||||
|
||||
```typescript
|
||||
// ❌ 构造函数注入——多依赖时难以阅读
|
||||
export class UserService {
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private router: Router,
|
||||
private auth: AuthService,
|
||||
) {}
|
||||
}
|
||||
|
||||
// ✅ inject()——更好的类型推断和可读性
|
||||
export class UserService {
|
||||
private http = inject(HttpClient);
|
||||
private router = inject(Router);
|
||||
private auth = inject(AuthService);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### Signals 与变更检测
|
||||
|
||||
- [ ] Signal + OnPush 用于模板状态(非可变对象)
|
||||
- [ ] `@Input()` 对象通过新引用更新(非变异)
|
||||
- [ ] 派生状态用 `computed()`,不用 `effect()`
|
||||
- [ ] `effect()` 中 Signal 读取在 `await` 之前
|
||||
- [ ] `effect()` 只用于 DOM 操作、日志、外部源订阅
|
||||
|
||||
### Standalone 组件
|
||||
|
||||
- [ ] 无 `standalone: false`(Angular 19+)
|
||||
- [ ] 组件通过 `imports` 数组导入依赖
|
||||
- [ ] 无不必要的 `@NgModule`
|
||||
|
||||
### RxJS
|
||||
|
||||
- [ ] `.subscribe()` 配 `takeUntilDestroyed` 或 `async` pipe
|
||||
- [ ] 优先 `toSignal` 而非 `AsyncPipe`
|
||||
- [ ] 无重复 `toSignal` 调用
|
||||
|
||||
### Zoneless
|
||||
|
||||
- [ ] 模板状态通过 Signal 管理(非普通属性)
|
||||
- [ ] 无 `NgZone.onStable` / `NgZone.onMicrotaskEmpty`
|
||||
- [ ] Reactive Forms 变异后有 `markForCheck()`
|
||||
|
||||
### 模板
|
||||
|
||||
- [ ] 复杂逻辑提取为 `computed` Signal
|
||||
- [ ] 使用原生 `[class]`/`[style]` 而非 `NgClass`/`NgStyle`
|
||||
- [ ] 模板专用成员标记 `protected`
|
||||
- [ ] `input`/`output`/`model` 属性标记 `readonly`
|
||||
- [ ] 事件处理器以操作命名(`saveData` 而非 `handleClick`)
|
||||
|
||||
### 性能
|
||||
|
||||
- [ ] `effect()` 不用于状态同步
|
||||
- [ ] `afterRenderEffect` 分离读写阶段
|
||||
- [ ] `inject()` 用于依赖注入
|
||||
@@ -0,0 +1,472 @@
|
||||
# Architecture Review Guide
|
||||
|
||||
架构设计审查指南,帮助评估代码的架构是否合理、设计是否恰当。
|
||||
|
||||
## SOLID 原则检查清单
|
||||
|
||||
### S - 单一职责原则 (SRP)
|
||||
|
||||
**检查要点:**
|
||||
- 这个类/模块是否只有一个改变的理由?
|
||||
- 类中的方法是否都服务于同一个目的?
|
||||
- 如果要向非技术人员描述这个类,能否用一句话说清楚?
|
||||
|
||||
**代码审查中的识别信号:**
|
||||
```
|
||||
⚠️ 类名包含 "And"、"Manager"、"Handler"、"Processor" 等泛化词汇
|
||||
⚠️ 一个类超过 200-300 行代码
|
||||
⚠️ 类有超过 5-7 个公共方法
|
||||
⚠️ 不同的方法操作完全不同的数据
|
||||
```
|
||||
|
||||
**审查问题:**
|
||||
- "这个类负责哪些事情?能否拆分?"
|
||||
- "如果 X 需求变化,哪些方法需要改?如果 Y 需求变化呢?"
|
||||
|
||||
### O - 开闭原则 (OCP)
|
||||
|
||||
**检查要点:**
|
||||
- 添加新功能时,是否需要修改现有代码?
|
||||
- 是否可以通过扩展(继承、组合)来添加新行为?
|
||||
- 是否存在大量的 if/else 或 switch 语句来处理不同类型?
|
||||
|
||||
**代码审查中的识别信号:**
|
||||
```
|
||||
⚠️ switch/if-else 链处理不同类型
|
||||
⚠️ 添加新功能需要修改核心类
|
||||
⚠️ 类型检查 (instanceof, typeof) 散布在代码中
|
||||
```
|
||||
|
||||
**审查问题:**
|
||||
- "如果要添加新的 X 类型,需要修改哪些文件?"
|
||||
- "这个 switch 语句会随着新类型增加而增长吗?"
|
||||
|
||||
### L - 里氏替换原则 (LSP)
|
||||
|
||||
**检查要点:**
|
||||
- 子类是否可以完全替代父类使用?
|
||||
- 子类是否改变了父类方法的预期行为?
|
||||
- 是否存在子类抛出父类未声明的异常?
|
||||
|
||||
**代码审查中的识别信号:**
|
||||
```
|
||||
⚠️ 显式类型转换 (casting)
|
||||
⚠️ 子类方法抛出 NotImplementedException
|
||||
⚠️ 子类方法为空实现或只有 return
|
||||
⚠️ 使用基类的地方需要检查具体类型
|
||||
```
|
||||
|
||||
**审查问题:**
|
||||
- "如果用子类替换父类,调用方代码是否需要修改?"
|
||||
- "这个方法在子类中的行为是否符合父类的契约?"
|
||||
|
||||
### I - 接口隔离原则 (ISP)
|
||||
|
||||
**检查要点:**
|
||||
- 接口是否足够小且专注?
|
||||
- 实现类是否被迫实现不需要的方法?
|
||||
- 客户端是否依赖了它不使用的方法?
|
||||
|
||||
**代码审查中的识别信号:**
|
||||
```
|
||||
⚠️ 接口超过 5-7 个方法
|
||||
⚠️ 实现类有空方法或抛出 NotImplementedException
|
||||
⚠️ 接口名称过于宽泛 (IManager, IService)
|
||||
⚠️ 不同的客户端只使用接口的部分方法
|
||||
```
|
||||
|
||||
**审查问题:**
|
||||
- "这个接口的所有方法是否都被每个实现类使用?"
|
||||
- "能否将这个大接口拆分为更小的专用接口?"
|
||||
|
||||
### D - 依赖倒置原则 (DIP)
|
||||
|
||||
**检查要点:**
|
||||
- 高层模块是否依赖于抽象而非具体实现?
|
||||
- 是否使用依赖注入而非直接 new 对象?
|
||||
- 抽象是否由高层模块定义而非低层模块?
|
||||
|
||||
**代码审查中的识别信号:**
|
||||
```
|
||||
⚠️ 高层模块直接 new 低层模块的具体类
|
||||
⚠️ 导入具体实现类而非接口/抽象类
|
||||
⚠️ 配置和连接字符串硬编码在业务逻辑中
|
||||
⚠️ 难以为某个类编写单元测试
|
||||
```
|
||||
|
||||
**审查问题:**
|
||||
- "这个类的依赖能否在测试时被 mock 替换?"
|
||||
- "如果要更换数据库/API 实现,需要修改多少地方?"
|
||||
|
||||
---
|
||||
|
||||
## 架构反模式识别
|
||||
|
||||
### 致命反模式
|
||||
|
||||
| 反模式 | 识别信号 | 影响 |
|
||||
|--------|----------|------|
|
||||
| **大泥球 (Big Ball of Mud)** | 没有清晰的模块边界,任何代码都可能调用任何其他代码 | 难以理解、修改和测试 |
|
||||
| **上帝类 (God Object)** | 单个类承担过多职责,知道太多、做太多 | 高耦合,难以重用和测试 |
|
||||
| **意大利面条代码** | 控制流程混乱,goto 或深层嵌套,难以追踪执行路径 | 难以理解和维护 |
|
||||
| **熔岩流 (Lava Flow)** | 没人敢动的古老代码,缺乏文档和测试 | 技术债务累积 |
|
||||
|
||||
### 设计反模式
|
||||
|
||||
| 反模式 | 识别信号 | 建议 |
|
||||
|--------|----------|------|
|
||||
| **金锤子 (Golden Hammer)** | 对所有问题使用同一种技术/模式 | 根据问题选择合适的解决方案 |
|
||||
| **过度工程 (Gas Factory)** | 简单问题用复杂方案解决,滥用设计模式 | YAGNI 原则,先简单后复杂 |
|
||||
| **船锚 (Boat Anchor)** | 为"将来可能需要"而写的未使用代码 | 删除未使用代码,需要时再写 |
|
||||
| **复制粘贴编程** | 相同逻辑出现在多处 | 提取公共方法或模块 |
|
||||
|
||||
### 审查问题
|
||||
|
||||
```markdown
|
||||
🔴 [blocking] "这个类有 2000 行代码,建议拆分为多个专注的类"
|
||||
🟡 [important] "这段逻辑在 3 个地方重复,考虑提取为公共方法?"
|
||||
💡 [suggestion] "这个 switch 语句可以用策略模式替代,更易扩展"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 耦合度与内聚性评估
|
||||
|
||||
### 耦合类型(从好到差)
|
||||
|
||||
| 类型 | 描述 | 示例 |
|
||||
|------|------|------|
|
||||
| **消息耦合** ✅ | 通过参数传递数据 | `calculate(price, quantity)` |
|
||||
| **数据耦合** ✅ | 共享简单数据结构 | `processOrder(orderDTO)` |
|
||||
| **印记耦合** ⚠️ | 共享复杂数据结构但只用部分 | 传入整个 User 对象但只用 name |
|
||||
| **控制耦合** ⚠️ | 传递控制标志影响行为 | `process(data, isAdmin=true)` |
|
||||
| **公共耦合** ❌ | 共享全局变量 | 多个模块读写同一个全局状态 |
|
||||
| **内容耦合** ❌ | 直接访问另一模块的内部 | 直接操作另一个类的私有属性 |
|
||||
|
||||
### 内聚类型(从好到差)
|
||||
|
||||
| 类型 | 描述 | 质量 |
|
||||
|------|------|------|
|
||||
| **功能内聚** | 所有元素完成单一任务 | ✅ 最佳 |
|
||||
| **顺序内聚** | 输出作为下一步输入 | ✅ 良好 |
|
||||
| **通信内聚** | 操作相同数据 | ⚠️ 可接受 |
|
||||
| **时间内聚** | 同时执行的任务 | ⚠️ 较差 |
|
||||
| **逻辑内聚** | 逻辑相关但功能不同 | ❌ 差 |
|
||||
| **偶然内聚** | 没有明显关系 | ❌ 最差 |
|
||||
|
||||
### 度量指标参考
|
||||
|
||||
```yaml
|
||||
耦合指标:
|
||||
CBO (类间耦合):
|
||||
好: < 5
|
||||
警告: 5-10
|
||||
危险: > 10
|
||||
|
||||
Ce (传出耦合):
|
||||
描述: 依赖多少外部类
|
||||
好: < 7
|
||||
|
||||
Ca (传入耦合):
|
||||
描述: 被多少类依赖
|
||||
高值意味着: 修改影响大,需要稳定
|
||||
|
||||
内聚指标:
|
||||
LCOM4 (方法缺乏内聚):
|
||||
1: 单一职责 ✅
|
||||
2-3: 可能需要拆分 ⚠️
|
||||
>3: 应该拆分 ❌
|
||||
```
|
||||
|
||||
### 审查问题
|
||||
|
||||
- "这个模块依赖了多少其他模块?能否减少?"
|
||||
- "修改这个类会影响多少其他地方?"
|
||||
- "这个类的方法是否都操作相同的数据?"
|
||||
|
||||
---
|
||||
|
||||
## 分层架构审查
|
||||
|
||||
### Clean Architecture 层次检查
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Frameworks & Drivers │ ← 最外层:Web、DB、UI
|
||||
├─────────────────────────────────────┤
|
||||
│ Interface Adapters │ ← Controllers、Gateways、Presenters
|
||||
├─────────────────────────────────────┤
|
||||
│ Application Layer │ ← Use Cases、Application Services
|
||||
├─────────────────────────────────────┤
|
||||
│ Domain Layer │ ← Entities、Domain Services
|
||||
└─────────────────────────────────────┘
|
||||
↑ 依赖方向只能向内 ↑
|
||||
```
|
||||
|
||||
### 依赖规则检查
|
||||
|
||||
**核心规则:源代码依赖只能指向内层**
|
||||
|
||||
```typescript
|
||||
// ❌ 违反依赖规则:Domain 层依赖 Infrastructure
|
||||
// domain/User.ts
|
||||
import { MySQLConnection } from '../infrastructure/database';
|
||||
|
||||
// ✅ 正确:Domain 层定义接口,Infrastructure 实现
|
||||
// domain/UserRepository.ts (接口)
|
||||
interface UserRepository {
|
||||
findById(id: string): Promise<User>;
|
||||
}
|
||||
|
||||
// infrastructure/MySQLUserRepository.ts (实现)
|
||||
class MySQLUserRepository implements UserRepository {
|
||||
findById(id: string): Promise<User> { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### 审查清单
|
||||
|
||||
**层次边界检查:**
|
||||
- [ ] Domain 层是否有外部依赖(数据库、HTTP、文件系统)?
|
||||
- [ ] Application 层是否直接操作数据库或调用外部 API?
|
||||
- [ ] Controller 是否包含业务逻辑?
|
||||
- [ ] 是否存在跨层调用(UI 直接调用 Repository)?
|
||||
|
||||
**关注点分离检查:**
|
||||
- [ ] 业务逻辑是否与展示逻辑分离?
|
||||
- [ ] 数据访问是否封装在专门的层?
|
||||
- [ ] 配置和环境相关代码是否集中管理?
|
||||
|
||||
### 审查问题
|
||||
|
||||
```markdown
|
||||
🔴 [blocking] "Domain 实体直接导入了数据库连接,违反依赖规则"
|
||||
🟡 [important] "Controller 包含业务计算逻辑,建议移到 Service 层"
|
||||
💡 [suggestion] "考虑使用依赖注入来解耦这些组件"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 设计模式使用评估
|
||||
|
||||
### 何时使用设计模式
|
||||
|
||||
| 模式 | 适用场景 | 不适用场景 |
|
||||
|------|----------|------------|
|
||||
| **Factory** | 需要创建不同类型对象,类型在运行时确定 | 只有一种类型,或类型固定不变 |
|
||||
| **Strategy** | 算法需要在运行时切换,有多种可互换的行为 | 只有一种算法,或算法不会变化 |
|
||||
| **Observer** | 一对多依赖,状态变化需要通知多个对象 | 简单的直接调用即可满足需求 |
|
||||
| **Singleton** | 确实需要全局唯一实例,如配置管理 | 可以通过依赖注入传递的对象 |
|
||||
| **Decorator** | 需要动态添加职责,避免继承爆炸 | 职责固定,不需要动态组合 |
|
||||
|
||||
### 过度设计警告信号
|
||||
|
||||
```
|
||||
⚠️ Patternitis(模式炎)识别信号:
|
||||
|
||||
1. 简单的 if/else 被替换为策略模式 + 工厂 + 注册表
|
||||
2. 只有一个实现的接口
|
||||
3. 为了"将来可能需要"而添加的抽象层
|
||||
4. 代码行数因模式应用而大幅增加
|
||||
5. 新人需要很长时间才能理解代码结构
|
||||
```
|
||||
|
||||
### 审查原则
|
||||
|
||||
```markdown
|
||||
✅ 正确使用模式:
|
||||
- 解决了实际的可扩展性问题
|
||||
- 代码更容易理解和测试
|
||||
- 添加新功能变得更简单
|
||||
|
||||
❌ 过度使用模式:
|
||||
- 为了使用模式而使用
|
||||
- 增加了不必要的复杂度
|
||||
- 违反了 YAGNI 原则
|
||||
```
|
||||
|
||||
### 审查问题
|
||||
|
||||
- "使用这个模式解决了什么具体问题?"
|
||||
- "如果不用这个模式,代码会有什么问题?"
|
||||
- "这个抽象层带来的价值是否大于它的复杂度?"
|
||||
|
||||
---
|
||||
|
||||
## 可扩展性评估
|
||||
|
||||
### 扩展性检查清单
|
||||
|
||||
**功能扩展性:**
|
||||
- [ ] 添加新功能是否需要修改核心代码?
|
||||
- [ ] 是否提供了扩展点(hooks、plugins、events)?
|
||||
- [ ] 配置是否外部化(配置文件、环境变量)?
|
||||
|
||||
**数据扩展性:**
|
||||
- [ ] 数据模型是否支持新增字段?
|
||||
- [ ] 是否考虑了数据量增长的场景?
|
||||
- [ ] 查询是否有合适的索引?
|
||||
|
||||
**负载扩展性:**
|
||||
- [ ] 是否可以水平扩展(添加更多实例)?
|
||||
- [ ] 是否有状态依赖(session、本地缓存)?
|
||||
- [ ] 数据库连接是否使用连接池?
|
||||
|
||||
### 扩展点设计检查
|
||||
|
||||
```typescript
|
||||
// ✅ 好的扩展设计:使用事件/钩子
|
||||
class OrderService {
|
||||
private hooks: OrderHooks;
|
||||
|
||||
async createOrder(order: Order) {
|
||||
await this.hooks.beforeCreate?.(order);
|
||||
const result = await this.save(order);
|
||||
await this.hooks.afterCreate?.(result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 差的扩展设计:硬编码所有行为
|
||||
class OrderService {
|
||||
async createOrder(order: Order) {
|
||||
await this.sendEmail(order); // 硬编码
|
||||
await this.updateInventory(order); // 硬编码
|
||||
await this.notifyWarehouse(order); // 硬编码
|
||||
return await this.save(order);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 审查问题
|
||||
|
||||
```markdown
|
||||
💡 [suggestion] "如果将来需要支持新的支付方式,这个设计是否容易扩展?"
|
||||
🟡 [important] "这里的逻辑是硬编码的,考虑使用配置或策略模式?"
|
||||
📚 [learning] "事件驱动架构可以让这个功能更容易扩展"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 代码结构最佳实践
|
||||
|
||||
### 目录组织
|
||||
|
||||
**按功能/领域组织(推荐):**
|
||||
```
|
||||
src/
|
||||
├── user/
|
||||
│ ├── User.ts (实体)
|
||||
│ ├── UserService.ts (服务)
|
||||
│ ├── UserRepository.ts (数据访问)
|
||||
│ └── UserController.ts (API)
|
||||
├── order/
|
||||
│ ├── Order.ts
|
||||
│ ├── OrderService.ts
|
||||
│ └── ...
|
||||
└── shared/
|
||||
├── utils/
|
||||
└── types/
|
||||
```
|
||||
|
||||
**按技术层组织(不推荐):**
|
||||
```
|
||||
src/
|
||||
├── controllers/ ← 不同领域混在一起
|
||||
│ ├── UserController.ts
|
||||
│ └── OrderController.ts
|
||||
├── services/
|
||||
├── repositories/
|
||||
└── models/
|
||||
```
|
||||
|
||||
### 命名约定检查
|
||||
|
||||
| 类型 | 约定 | 示例 |
|
||||
|------|------|------|
|
||||
| 类名 | PascalCase,名词 | `UserService`, `OrderRepository` |
|
||||
| 方法名 | camelCase,动词 | `createUser`, `findOrderById` |
|
||||
| 接口名 | I 前缀或无前缀 | `IUserService` 或 `UserService` |
|
||||
| 常量 | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` |
|
||||
| 私有属性 | 下划线前缀或无 | `_cache` 或 `#cache` |
|
||||
|
||||
### 文件大小指南
|
||||
|
||||
```yaml
|
||||
建议限制:
|
||||
单个文件: < 300 行
|
||||
单个函数: < 50 行
|
||||
单个类: < 200 行
|
||||
函数参数: < 4 个
|
||||
嵌套深度: < 4 层
|
||||
|
||||
超出限制时:
|
||||
- 考虑拆分为更小的单元
|
||||
- 使用组合而非继承
|
||||
- 提取辅助函数或类
|
||||
```
|
||||
|
||||
### 审查问题
|
||||
|
||||
```markdown
|
||||
🟢 [nit] "这个 500 行的文件可以考虑按职责拆分"
|
||||
🟡 [important] "建议按功能领域而非技术层组织目录结构"
|
||||
💡 [suggestion] "函数名 `process` 不够明确,考虑改为 `calculateOrderTotal`?"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速参考清单
|
||||
|
||||
### 架构审查 5 分钟速查
|
||||
|
||||
```markdown
|
||||
□ 依赖方向是否正确?(外层依赖内层)
|
||||
□ 是否存在循环依赖?
|
||||
□ 核心业务逻辑是否与框架/UI/数据库解耦?
|
||||
□ 是否遵循 SOLID 原则?
|
||||
□ 是否存在明显的反模式?
|
||||
```
|
||||
|
||||
### 红旗信号(必须处理)
|
||||
|
||||
```markdown
|
||||
🔴 God Object - 单个类超过 1000 行
|
||||
🔴 循环依赖 - A → B → C → A
|
||||
🔴 Domain 层包含框架依赖
|
||||
🔴 硬编码的配置和密钥
|
||||
🔴 没有接口的外部服务调用
|
||||
```
|
||||
|
||||
### 黄旗信号(建议处理)
|
||||
|
||||
```markdown
|
||||
🟡 类间耦合度 (CBO) > 10
|
||||
🟡 方法参数超过 5 个
|
||||
🟡 嵌套深度超过 4 层
|
||||
🟡 重复代码块 > 10 行
|
||||
🟡 只有一个实现的接口
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 工具推荐
|
||||
|
||||
| 工具 | 用途 | 语言支持 |
|
||||
|------|------|----------|
|
||||
| **SonarQube** | 代码质量、耦合度分析 | 多语言 |
|
||||
| **NDepend** | 依赖分析、架构规则 | .NET |
|
||||
| **JDepend** | 包依赖分析 | Java |
|
||||
| **Madge** | 模块依赖图 | JavaScript/TypeScript |
|
||||
| **ESLint** | 代码规范、复杂度检查 | JavaScript/TypeScript |
|
||||
| **CodeScene** | 技术债务、热点分析 | 多语言 |
|
||||
|
||||
---
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [Clean Architecture - Uncle Bob](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|
||||
- [SOLID Principles in Code Review - JetBrains](https://blog.jetbrains.com/upsource/2015/08/31/what-to-look-for-in-a-code-review-solid-principles-2/)
|
||||
- [Software Architecture Anti-Patterns](https://medium.com/@christophnissle/anti-patterns-in-software-architecture-3c8970c9c4f5)
|
||||
- [Coupling and Cohesion in System Design](https://www.geeksforgeeks.org/system-design/coupling-and-cohesion-in-system-design/)
|
||||
- [Design Patterns - Refactoring Guru](https://refactoring.guru/design-patterns)
|
||||
@@ -0,0 +1,285 @@
|
||||
# C Code Review Guide
|
||||
|
||||
> C code review guide focused on memory safety, undefined behavior, and portability. Examples assume C11.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Pointer and Buffer Safety](#pointer-and-buffer-safety)
|
||||
- [Ownership and Resource Management](#ownership-and-resource-management)
|
||||
- [Undefined Behavior Pitfalls](#undefined-behavior-pitfalls)
|
||||
- [Integer Types and Overflow](#integer-types-and-overflow)
|
||||
- [Error Handling](#error-handling)
|
||||
- [Concurrency](#concurrency)
|
||||
- [Macros and Preprocessor](#macros-and-preprocessor)
|
||||
- [API Design and Const](#api-design-and-const)
|
||||
- [Tooling and Build Checks](#tooling-and-build-checks)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## Pointer and Buffer Safety
|
||||
|
||||
### Always carry size with buffers
|
||||
|
||||
```c
|
||||
// ❌ Bad: ignores destination size
|
||||
bool copy_name(char *dst, size_t dst_size, const char *src) {
|
||||
strcpy(dst, src);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ✅ Good: validate size and terminate
|
||||
bool copy_name(char *dst, size_t dst_size, const char *src) {
|
||||
size_t len = strlen(src);
|
||||
if (len + 1 > dst_size) {
|
||||
return false;
|
||||
}
|
||||
memcpy(dst, src, len + 1);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Avoid dangerous APIs
|
||||
|
||||
Prefer `snprintf`, `fgets`, and explicit bounds over `gets`, `strcpy`, or `sprintf`.
|
||||
|
||||
```c
|
||||
// ❌ Bad: unbounded write
|
||||
sprintf(buf, "%s", input);
|
||||
|
||||
// ✅ Good: bounded write
|
||||
snprintf(buf, buf_size, "%s", input);
|
||||
```
|
||||
|
||||
### Use the right copy primitive
|
||||
|
||||
```c
|
||||
// ❌ Bad: memcpy with overlapping regions
|
||||
memcpy(dst, src, len);
|
||||
|
||||
// ✅ Good: memmove handles overlap
|
||||
memmove(dst, src, len);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ownership and Resource Management
|
||||
|
||||
### One allocation, one free
|
||||
|
||||
Track ownership and clean up on every error path.
|
||||
|
||||
```c
|
||||
// ✅ Good: cleanup label avoids leaks
|
||||
int load_file(const char *path) {
|
||||
int rc = -1;
|
||||
FILE *f = NULL;
|
||||
char *buf = NULL;
|
||||
|
||||
f = fopen(path, "rb");
|
||||
if (!f) {
|
||||
goto cleanup;
|
||||
}
|
||||
buf = malloc(4096);
|
||||
if (!buf) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (fread(buf, 1, 4096, f) == 0) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
rc = 0;
|
||||
|
||||
cleanup:
|
||||
free(buf);
|
||||
if (f) {
|
||||
fclose(f);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Undefined Behavior Pitfalls
|
||||
|
||||
### Common UB patterns
|
||||
|
||||
```c
|
||||
// ❌ Bad: use after free
|
||||
char *p = malloc(10);
|
||||
free(p);
|
||||
p[0] = 'a';
|
||||
|
||||
// ❌ Bad: uninitialized read
|
||||
int x;
|
||||
if (x > 0) { /* UB */ }
|
||||
|
||||
// ❌ Bad: signed overflow
|
||||
int sum = a + b;
|
||||
```
|
||||
|
||||
### Avoid pointer arithmetic past the object
|
||||
|
||||
```c
|
||||
// ❌ Bad: pointer past the end then dereference
|
||||
int arr[4];
|
||||
int *p = arr + 4;
|
||||
int v = *p; // UB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integer Types and Overflow
|
||||
|
||||
### Avoid signed/unsigned surprises
|
||||
|
||||
```c
|
||||
// ❌ Bad: negative converted to large size_t
|
||||
int len = -1;
|
||||
size_t n = len;
|
||||
|
||||
// ✅ Good: validate before converting
|
||||
if (len < 0) {
|
||||
return -1;
|
||||
}
|
||||
size_t n = (size_t)len;
|
||||
```
|
||||
|
||||
### Check for overflow in size calculations
|
||||
|
||||
```c
|
||||
// ❌ Bad: potential overflow in multiplication
|
||||
size_t bytes = count * sizeof(Item);
|
||||
|
||||
// ✅ Good: check before multiplying
|
||||
if (count > SIZE_MAX / sizeof(Item)) {
|
||||
return NULL;
|
||||
}
|
||||
size_t bytes = count * sizeof(Item);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Always check return values
|
||||
|
||||
```c
|
||||
// ❌ Bad: ignore errors
|
||||
fread(buf, 1, size, f);
|
||||
|
||||
// ✅ Good: handle errors
|
||||
size_t read = fread(buf, 1, size, f);
|
||||
if (read != size && ferror(f)) {
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
### Consistent error contracts
|
||||
|
||||
- Use a clear convention: 0 for success, negative for failure.
|
||||
- Document ownership rules on success and failure.
|
||||
- If using `errno`, set it only for actual failures.
|
||||
|
||||
---
|
||||
|
||||
## Concurrency
|
||||
|
||||
### volatile is not synchronization
|
||||
|
||||
```c
|
||||
// ❌ Bad: data race
|
||||
volatile int stop = 0;
|
||||
void worker(void) {
|
||||
while (!stop) { /* ... */ }
|
||||
}
|
||||
|
||||
// ✅ Good: C11 atomics
|
||||
_Atomic int stop = 0;
|
||||
void worker(void) {
|
||||
while (!atomic_load(&stop)) { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### Use mutexes for shared state
|
||||
|
||||
Protect shared data with `pthread_mutex_t` or equivalent. Avoid holding locks while doing I/O.
|
||||
|
||||
---
|
||||
|
||||
## Macros and Preprocessor
|
||||
|
||||
### Parenthesize arguments
|
||||
|
||||
```c
|
||||
// ❌ Bad: macro with side effects
|
||||
#define MIN(a, b) ((a) < (b) ? (a) : (b))
|
||||
int x = MIN(i++, j++);
|
||||
|
||||
// ✅ Good: static inline function
|
||||
static inline int min_int(int a, int b) {
|
||||
return a < b ? a : b;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Design and Const
|
||||
|
||||
### Const-correctness and sizes
|
||||
|
||||
```c
|
||||
// ✅ Good: explicit size and const input
|
||||
int hash_bytes(const uint8_t *data, size_t len, uint8_t *out);
|
||||
```
|
||||
|
||||
### Document nullability
|
||||
|
||||
Clearly document whether pointers may be NULL. Prefer returning error codes instead of NULL when possible.
|
||||
|
||||
---
|
||||
|
||||
## Tooling and Build Checks
|
||||
|
||||
```bash
|
||||
# Warnings
|
||||
clang -Wall -Wextra -Werror -Wconversion -Wshadow -std=c11 ...
|
||||
|
||||
# Sanitizers (debug builds)
|
||||
clang -fsanitize=address,undefined -fno-omit-frame-pointer -g ...
|
||||
clang -fsanitize=thread -fno-omit-frame-pointer -g ...
|
||||
|
||||
# Static analysis
|
||||
clang-tidy src/*.c -- -std=c11
|
||||
cppcheck --enable=warning,performance,portability src/
|
||||
|
||||
# Formatting
|
||||
clang-format -i src/*.c include/*.h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### Memory and UB
|
||||
- [ ] All buffers have explicit size parameters
|
||||
- [ ] No out-of-bounds access or pointer arithmetic past objects
|
||||
- [ ] No use after free or uninitialized reads
|
||||
- [ ] Signed overflow and shift rules are respected
|
||||
|
||||
### API and Design
|
||||
- [ ] Ownership rules are documented and consistent
|
||||
- [ ] const-correctness is applied for inputs
|
||||
- [ ] Error contracts are clear and consistent
|
||||
|
||||
### Concurrency
|
||||
- [ ] No data races on shared state
|
||||
- [ ] volatile is not used for synchronization
|
||||
- [ ] Locks are held for minimal time
|
||||
|
||||
### Tooling and Tests
|
||||
- [ ] Builds clean with warnings enabled
|
||||
- [ ] Sanitizers run on critical code paths
|
||||
- [ ] Static analysis results are addressed
|
||||
@@ -0,0 +1,488 @@
|
||||
# Universal Code Quality Anti-Patterns
|
||||
|
||||
> 语言无关的代码质量反模式指南,覆盖代码复用、抽象泄漏、参数膨胀、嵌套条件、字符串类型化、TOCTOU、空操作更新等核心主题。适用于所有语言的 PR 审查。
|
||||
|
||||
## 目录
|
||||
|
||||
- [代码复用审查](#代码复用审查)
|
||||
- [参数膨胀](#参数膨胀)
|
||||
- [抽象泄漏](#抽象泄漏)
|
||||
- [字符串类型化](#字符串类型化)
|
||||
- [嵌套条件表达式](#嵌套条件表达式)
|
||||
- [复制粘贴变种](#复制粘贴变种)
|
||||
- [空操作更新](#空操作更新)
|
||||
- [TOCTOU 竞争条件](#toctou-竞争条件)
|
||||
- [过度宽泛操作](#过度宽泛操作)
|
||||
- [冗余状态](#冗余状态)
|
||||
- [通用质量审查清单](#通用质量审查清单)
|
||||
|
||||
---
|
||||
|
||||
## 代码复用审查
|
||||
|
||||
Before accepting new code, search the existing codebase for reusable utilities.
|
||||
|
||||
### 搜索现有工具函数
|
||||
|
||||
```python
|
||||
# ❌ 新写的路径拼接逻辑——项目中已有 PathBuilder
|
||||
def get_config_path(name):
|
||||
base = os.environ.get("APP_ROOT", ".")
|
||||
return os.path.join(base, "config", name + ".json")
|
||||
|
||||
# ✅ 使用已有的 PathBuilder
|
||||
def get_config_path(name):
|
||||
return PathBuilder.config(f"{name}.json")
|
||||
```
|
||||
|
||||
```javascript
|
||||
// ❌ 手写 debounce——项目已有 lodash 或 utils/debounce.ts
|
||||
function debounce(fn, ms) {
|
||||
let timer;
|
||||
return (...args) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => fn(...args), ms);
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ 使用已有的工具函数
|
||||
import { debounce } from "@/utils/debounce";
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- 新增函数是否与已有 utility 重名或功能重叠?
|
||||
- inline 逻辑是否可以提取为已有模块的调用?
|
||||
- 检查相邻文件和 shared/utils 目录
|
||||
|
||||
---
|
||||
|
||||
## 参数膨胀
|
||||
|
||||
### 函数参数不断增长
|
||||
|
||||
```python
|
||||
# ❌ 每次新需求加一个参数
|
||||
def create_user(name, email, role, team, active, avatar_url, timezone):
|
||||
...
|
||||
|
||||
# ✅ 使用配置对象 / dataclass
|
||||
@dataclass
|
||||
class CreateUserParams:
|
||||
name: str
|
||||
email: str
|
||||
role: Role = Role.MEMBER
|
||||
team: str | None = None
|
||||
active: bool = True
|
||||
avatar_url: str | None = None
|
||||
timezone: str = "UTC"
|
||||
|
||||
def create_user(params: CreateUserParams) -> User:
|
||||
...
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ❌ 6+ 个 positional 参数
|
||||
function renderWidget(
|
||||
title: string, width: number, height: number,
|
||||
theme: string, collapsible: boolean, icon: string
|
||||
) { ... }
|
||||
|
||||
// ✅ Options object pattern
|
||||
interface WidgetOptions {
|
||||
title: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
theme?: "light" | "dark";
|
||||
collapsible?: boolean;
|
||||
icon?: string;
|
||||
}
|
||||
function renderWidget(options: WidgetOptions) { ... }
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- 函数参数是否 ≥ 4 个?考虑 options object / dataclass
|
||||
- 新参数是否只是布尔标志?考虑 enum 或 strategy pattern
|
||||
- 是否有 `enable_x`, `disable_y` 这类互斥参数?
|
||||
|
||||
---
|
||||
|
||||
## 抽象泄漏
|
||||
|
||||
### 暴露内部实现细节
|
||||
|
||||
```python
|
||||
# ❌ 返回内部 ORM 对象——调用者被迫了解 SQLAlchemy
|
||||
def get_users():
|
||||
return session.query(User).filter(User.active == True).all()
|
||||
|
||||
# ✅ 返回 domain 对象,隐藏持久化层
|
||||
def get_active_users() -> list[UserDTO]:
|
||||
rows = user_repo.find_active()
|
||||
return [UserDTO.from_row(r) for r in rows]
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ❌ 组件接收 API response 原始结构
|
||||
<UserCard user={apiResponse.data.results[0]} />
|
||||
|
||||
// ✅ 组件接收 domain 类型,adapter 处理映射
|
||||
interface UserSummary {
|
||||
displayName: string;
|
||||
avatarUrl: string;
|
||||
}
|
||||
<UserCard user={adaptUser(apiResponse)} />
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- 函数返回类型是否泄露底层实现(ORM, HTTP client, file format)?
|
||||
- 组件/函数是否依赖外部系统的数据结构?
|
||||
- 是否破坏了已有的抽象边界?
|
||||
|
||||
---
|
||||
|
||||
## 字符串类型化
|
||||
|
||||
### 用原始字符串代替常量/枚举
|
||||
|
||||
```python
|
||||
# ❌ Magic strings 散落各处
|
||||
if status == "active":
|
||||
...
|
||||
if role == "admin":
|
||||
...
|
||||
|
||||
# ✅ 使用 enum
|
||||
class Status(StrEnum):
|
||||
ACTIVE = "active"
|
||||
SUSPENDED = "suspended"
|
||||
ARCHIVED = "archived"
|
||||
|
||||
if user.status == Status.ACTIVE:
|
||||
...
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ❌ Raw string event names——拼写错误不会报错
|
||||
emitter.emit("userCreated", data);
|
||||
emitter.on("usercreated", handler); // bug: typo
|
||||
|
||||
// ✅ 常量或 branded type
|
||||
const Events = {
|
||||
USER_CREATED: "userCreated",
|
||||
USER_SUSPENDED: "userSuspended",
|
||||
} as const;
|
||||
emitter.emit(Events.USER_CREATED, data);
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- 是否用字符串代替了已有的 enum/union type?
|
||||
- 事件名、action type、status 值是否散落在多个文件?
|
||||
- 字符串比较是否 case-sensitive 但未验证?
|
||||
|
||||
---
|
||||
|
||||
## 嵌套条件表达式
|
||||
|
||||
### 三元链和嵌套 if/else
|
||||
|
||||
```python
|
||||
# ❌ 三元链难以阅读
|
||||
label = (
|
||||
"Admin" if role == "admin" else
|
||||
"Manager" if role == "manager" else
|
||||
"Viewer" if role == "viewer" else
|
||||
"Unknown"
|
||||
)
|
||||
|
||||
# ✅ 查找表或 match
|
||||
ROLE_LABELS = {
|
||||
"admin": "Admin",
|
||||
"manager": "Manager",
|
||||
"viewer": "Viewer",
|
||||
}
|
||||
label = ROLE_LABELS.get(role, "Unknown")
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ❌ 嵌套三元
|
||||
const bg = isHovered
|
||||
? isSelected ? "blue" : "gray"
|
||||
: isSelected ? "navy" : "white";
|
||||
|
||||
// ✅ 查找表(lookup map)
|
||||
const bgMap: Record<string, string> = {
|
||||
"true-true": "blue",
|
||||
"true-false": "gray",
|
||||
"false-true": "navy",
|
||||
"false-false": "white",
|
||||
};
|
||||
const bg = bgMap[`${isHovered}-${isSelected}`];
|
||||
```
|
||||
|
||||
```python
|
||||
# ❌ 嵌套 if 3+ 层
|
||||
def process(order):
|
||||
if order is not None:
|
||||
if order.items:
|
||||
for item in order.items:
|
||||
if item.price > 0:
|
||||
...
|
||||
|
||||
# ✅ Early return + guard clauses
|
||||
def process(order):
|
||||
if not order or not order.items:
|
||||
return
|
||||
for item in order.items:
|
||||
if item.price <= 0:
|
||||
continue
|
||||
...
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- 三元表达式是否嵌套 ≥ 2 层?
|
||||
- if/else 嵌套是否 ≥ 3 层?
|
||||
- 能否用 lookup table、early return 或 match 替换?
|
||||
|
||||
---
|
||||
|
||||
## 复制粘贴变种
|
||||
|
||||
### 近乎重复的代码块
|
||||
|
||||
```python
|
||||
# ❌ 两个函数几乎一样,只有字段名不同
|
||||
def format_user(user):
|
||||
return f"{user.first_name} {user.last_name} ({user.email})"
|
||||
|
||||
def format_employee(emp):
|
||||
return f"{emp.first_name} {emp.last_name} ({emp.work_email})"
|
||||
|
||||
# ✅ 统一抽象
|
||||
def format_person(first: str, last: str, email: str) -> str:
|
||||
return f"{first} {last} ({email})"
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ❌ Copy-paste handler 只改了 URL
|
||||
async function deletePost(id: string) {
|
||||
await fetch(`/api/posts/${id}`, { method: "DELETE" });
|
||||
router.push("/posts");
|
||||
}
|
||||
async function deleteComment(id: string) {
|
||||
await fetch(`/api/comments/${id}`, { method: "DELETE" });
|
||||
router.push("/comments");
|
||||
}
|
||||
|
||||
// ✅ 参数化
|
||||
async function deleteResource(resource: string, id: string) {
|
||||
await fetch(`/api/${resource}/${id}`, { method: "DELETE" });
|
||||
router.push(`/${resource}`);
|
||||
}
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- 是否有 ≥ 2 段代码仅变量名/URL/字符串不同?
|
||||
- 能否提取参数化的共享函数?
|
||||
- 是否可以用 template method 或 strategy 消除变种?
|
||||
|
||||
---
|
||||
|
||||
## 空操作更新
|
||||
|
||||
### 无条件触发状态更新
|
||||
|
||||
```typescript
|
||||
// ❌ 每次 poll 都触发 update——即使数据未变
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
fetch("/api/status").then(r => r.json()).then(setStatus);
|
||||
}, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// ✅ 仅在值变化时更新
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
fetch("/api/status")
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
setStatus(prev => isEqual(prev, data) ? prev : data);
|
||||
});
|
||||
}, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
```
|
||||
|
||||
```python
|
||||
# ❌ 每次 loop 都写 DB——即使值未变
|
||||
for item in items:
|
||||
item.status = compute_status(item)
|
||||
session.commit()
|
||||
|
||||
# ✅ 仅在变化时写入
|
||||
for item in items:
|
||||
new_status = compute_status(item)
|
||||
if item.status != new_status:
|
||||
item.status = new_status
|
||||
session.commit()
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- polling / interval / event handler 是否无条件更新?
|
||||
- wrapper function 是否尊重 same-reference return?
|
||||
- DB 写入是否检查了实际变化?
|
||||
|
||||
---
|
||||
|
||||
## TOCTOU 竞争条件
|
||||
|
||||
### Time-of-Check-to-Time-of-Use
|
||||
|
||||
```python
|
||||
# ❌ 先检查后操作——中间文件可能被删除/创建
|
||||
if os.path.exists(path):
|
||||
with open(path) as f:
|
||||
data = f.read()
|
||||
|
||||
# ✅ 直接操作 + 处理异常
|
||||
try:
|
||||
with open(path) as f:
|
||||
data = f.read()
|
||||
except FileNotFoundError:
|
||||
data = None
|
||||
```
|
||||
|
||||
```python
|
||||
# ❌ 检查余额 → 扣款 两步操作不是原子的
|
||||
if account.balance >= amount:
|
||||
account.balance -= amount
|
||||
|
||||
# ✅ 原子操作或锁
|
||||
with account.lock:
|
||||
if account.balance < amount:
|
||||
raise InsufficientFundsError()
|
||||
account.balance -= amount
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ❌ Check-then-act 在 async 环境中不安全
|
||||
if (!fileExists(path)) {
|
||||
await writeFile(path, content);
|
||||
}
|
||||
|
||||
// ✅ 直接操作 + catch
|
||||
try {
|
||||
await writeFile(path, content, { flag: "wx" });
|
||||
} catch (e) {
|
||||
if (e.code === "EEXIST") { /* handle */ }
|
||||
else throw e;
|
||||
}
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- `if exists → operate` 模式是否可替换为 `try operate → catch`?
|
||||
- 多步状态变更是否在事务/锁内?
|
||||
- async 操作中 check 和 act 之间是否有 await?
|
||||
|
||||
---
|
||||
|
||||
## 过度宽泛操作
|
||||
|
||||
### 读取过多数据
|
||||
|
||||
```python
|
||||
# ❌ 读取整个文件再取第一行
|
||||
content = Path("log.txt").read_text()
|
||||
first_line = content.split("\n")[0]
|
||||
|
||||
# ✅ 只读第一行,不加载整个文件
|
||||
with open("log.txt") as f:
|
||||
first_line = f.readline()
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ❌ 加载所有 items 再过滤
|
||||
const allItems = await db.query("SELECT * FROM orders");
|
||||
const pending = allItems.filter(o => o.status === "pending");
|
||||
|
||||
// ✅ 数据库层过滤
|
||||
const pending = await db.query(
|
||||
"SELECT * FROM orders WHERE status = ?", ["pending"]
|
||||
);
|
||||
```
|
||||
|
||||
```python
|
||||
# ❌ 读取整个列表找一条记录
|
||||
users = list(User.objects.all())
|
||||
user = next(u for u in users if u.id == user_id)
|
||||
|
||||
# ✅ 精确查询
|
||||
user = User.objects.get(id=user_id)
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- 是否读取了整个集合/文件再只用一小部分?
|
||||
- 能否将过滤推到数据库/存储层?
|
||||
- API 调用是否支持 pagination/limit 参数?
|
||||
|
||||
---
|
||||
|
||||
## 冗余状态
|
||||
|
||||
### 状态可以被推导
|
||||
|
||||
```typescript
|
||||
// ❌ 同时存储 fullName 和 firstName + lastName
|
||||
interface User {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
fullName: string; // redundant
|
||||
}
|
||||
|
||||
// ✅ fullName 是推导值
|
||||
interface User {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
}
|
||||
const fullName = `${user.firstName} ${user.lastName}`;
|
||||
```
|
||||
|
||||
```python
|
||||
# ❌ 缓存值在源数据变化时可能过时
|
||||
class Order:
|
||||
total: float
|
||||
item_count: int # redundant if len(items) gives the same
|
||||
items: list[Item]
|
||||
|
||||
# ✅ 推导或 property
|
||||
class Order:
|
||||
items: list[Item]
|
||||
|
||||
@property
|
||||
def total(self) -> float:
|
||||
return sum(item.price for item in self.items)
|
||||
|
||||
@property
|
||||
def item_count(self) -> int:
|
||||
return len(self.items)
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- 是否有字段可以从其他字段推导?
|
||||
- 缓存值是否有 invalidation 机制?
|
||||
- observer/effect 是否可以替换为直接调用?
|
||||
|
||||
---
|
||||
|
||||
## 通用质量审查清单
|
||||
|
||||
- [ ] **复用审查**: 搜索了现有 utility/helper,没有重复造轮子?
|
||||
- [ ] **参数数量**: 函数参数 ≤ 3 个?超过则用 options object / dataclass?
|
||||
- [ ] **抽象边界**: 返回类型没有暴露内部实现细节(ORM、HTTP client、file format)?
|
||||
- [ ] **类型安全**: 没有 magic strings 代替已有的 enum/constant/union type?
|
||||
- [ ] **条件深度**: 三元嵌套 ≤ 1 层?if/else 嵌套 ≤ 2 层?
|
||||
- [ ] **DRY**: 没有 copy-paste-with-variation(≥ 2 段近似代码)?
|
||||
- [ ] **空操作防护**: polling / interval / event handler 有 change-detection guard?
|
||||
- [ ] **TOCTOU**: `if exists → operate` 替换为 `try operate → catch`?
|
||||
- [ ] **数据精度**: 没有读取整个集合/文件只为了取子集?
|
||||
- [ ] **冗余状态**: 没有可以从其他字段推导的存储字段?
|
||||
@@ -0,0 +1,136 @@
|
||||
# Code Review Best Practices
|
||||
|
||||
Comprehensive guidelines for conducting effective code reviews.
|
||||
|
||||
## Review Philosophy
|
||||
|
||||
### Goals of Code Review
|
||||
|
||||
**Primary Goals:**
|
||||
- Catch bugs and edge cases before production
|
||||
- Ensure code maintainability and readability
|
||||
- Share knowledge across the team
|
||||
- Enforce coding standards consistently
|
||||
- Improve design and architecture decisions
|
||||
|
||||
**Secondary Goals:**
|
||||
- Mentor junior developers
|
||||
- Build team culture and trust
|
||||
- Document design decisions through discussions
|
||||
|
||||
### What Code Review is NOT
|
||||
|
||||
- A gatekeeping mechanism to block progress
|
||||
- An opportunity to show off knowledge
|
||||
- A place to nitpick formatting (use linters)
|
||||
- A way to rewrite code to personal preference
|
||||
|
||||
## Review Timing
|
||||
|
||||
### When to Review
|
||||
|
||||
| Trigger | Action |
|
||||
|---------|--------|
|
||||
| PR opened | Review within 24 hours, ideally same day |
|
||||
| Changes requested | Re-review within 4 hours |
|
||||
| Blocking issue found | Communicate immediately |
|
||||
|
||||
### Time Allocation
|
||||
|
||||
- **Small PR (<100 lines)**: 10-15 minutes
|
||||
- **Medium PR (100-400 lines)**: 20-40 minutes
|
||||
- **Large PR (>400 lines)**: Request to split, or 60+ minutes
|
||||
|
||||
## Review Depth Levels
|
||||
|
||||
### Level 1: Skim Review (5 minutes)
|
||||
- Check PR description and linked issues
|
||||
- Verify CI/CD status
|
||||
- Look at file changes overview
|
||||
- Identify if deeper review needed
|
||||
|
||||
### Level 2: Standard Review (20-30 minutes)
|
||||
- Full code walkthrough
|
||||
- Logic verification
|
||||
- Test coverage check
|
||||
- Security scan
|
||||
|
||||
### Level 3: Deep Review (60+ minutes)
|
||||
- Architecture evaluation
|
||||
- Performance analysis
|
||||
- Security audit
|
||||
- Edge case exploration
|
||||
|
||||
## Communication Guidelines
|
||||
|
||||
### Tone and Language
|
||||
|
||||
**Use collaborative language:**
|
||||
- "What do you think about..." instead of "You should..."
|
||||
- "Could we consider..." instead of "This is wrong"
|
||||
- "I'm curious about..." instead of "Why didn't you..."
|
||||
|
||||
**Be specific and actionable:**
|
||||
- Include code examples when suggesting changes
|
||||
- Link to documentation or past discussions
|
||||
- Explain the "why" behind suggestions
|
||||
|
||||
### Handling Disagreements
|
||||
|
||||
1. **Seek to understand**: Ask clarifying questions
|
||||
2. **Acknowledge valid points**: Show you've considered their perspective
|
||||
3. **Provide data**: Use benchmarks, docs, or examples
|
||||
4. **Escalate if needed**: Involve senior dev or architect
|
||||
5. **Know when to let go**: Not every hill is worth dying on
|
||||
|
||||
## Review Prioritization
|
||||
|
||||
### Must Fix (Blocking)
|
||||
- Security vulnerabilities
|
||||
- Data corruption risks
|
||||
- Breaking changes without migration
|
||||
- Critical performance issues
|
||||
- Missing error handling for user-facing features
|
||||
|
||||
### Should Fix (Important)
|
||||
- Test coverage gaps
|
||||
- Moderate performance concerns
|
||||
- Code duplication
|
||||
- Unclear naming or structure
|
||||
- Missing documentation for complex logic
|
||||
|
||||
### Nice to Have (Non-blocking)
|
||||
- Style preferences beyond linting
|
||||
- Minor optimizations
|
||||
- Additional test cases
|
||||
- Documentation improvements
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
### Reviewer Anti-Patterns
|
||||
- **Rubber stamping**: Approving without actually reviewing
|
||||
- **Bike shedding**: Debating trivial details extensively
|
||||
- **Scope creep**: "While you're at it, can you also..."
|
||||
- **Ghosting**: Requesting changes then disappearing
|
||||
- **Perfectionism**: Blocking for minor style preferences
|
||||
|
||||
### Author Anti-Patterns
|
||||
- **Mega PRs**: Submitting 1000+ line changes
|
||||
- **No context**: Missing PR description or linked issues
|
||||
- **Defensive responses**: Arguing every suggestion
|
||||
- **Silent updates**: Making changes without responding to comments
|
||||
|
||||
## Metrics and Improvement
|
||||
|
||||
### Track These Metrics
|
||||
- Time to first review
|
||||
- Review cycle time
|
||||
- Number of review rounds
|
||||
- Defect escape rate
|
||||
- Review coverage percentage
|
||||
|
||||
### Continuous Improvement
|
||||
- Hold retrospectives on review process
|
||||
- Share learnings from escaped bugs
|
||||
- Update checklists based on common issues
|
||||
- Celebrate good reviews and catches
|
||||
@@ -0,0 +1,248 @@
|
||||
# Common Bugs Checklist
|
||||
|
||||
Quick-reference bug patterns organized by category. For detailed code examples, explanations, and comprehensive review checklists, see the dedicated language guides linked below.
|
||||
|
||||
## Universal Issues
|
||||
|
||||
### Logic Errors
|
||||
- [ ] Off-by-one errors in loops and array access
|
||||
- [ ] Incorrect boolean logic (De Morgan's law violations)
|
||||
- [ ] Missing null/undefined checks
|
||||
- [ ] Race conditions in concurrent code
|
||||
- [ ] Incorrect comparison operators (`==` vs `===`, `=` vs `==`)
|
||||
- [ ] Integer overflow/underflow
|
||||
- [ ] Floating point comparison issues
|
||||
|
||||
### Resource Management
|
||||
- [ ] Memory leaks (unclosed connections, listeners)
|
||||
- [ ] File handles not closed
|
||||
- [ ] Database connections not released
|
||||
- [ ] Event listeners not removed
|
||||
- [ ] Timers/intervals not cleared
|
||||
|
||||
### Error Handling
|
||||
- [ ] Swallowed exceptions (empty catch blocks)
|
||||
- [ ] Generic exception handling hiding specific errors
|
||||
- [ ] Missing error propagation
|
||||
- [ ] Incorrect error types thrown
|
||||
- [ ] Missing finally/cleanup blocks
|
||||
|
||||
## TypeScript/JavaScript
|
||||
|
||||
- [ ] `==` instead of `===`
|
||||
- [ ] Using `any` — prefer proper types or `unknown` with type guards
|
||||
- [ ] Missing `await` on async calls
|
||||
- [ ] Unhandled promise rejections (no try-catch around await)
|
||||
- [ ] `this` context lost in callbacks
|
||||
- [ ] Missing `key` prop in lists
|
||||
- [ ] Closure capturing stale loop variable
|
||||
- [ ] `parseInt` without radix parameter
|
||||
- [ ] Modifying array/object during iteration
|
||||
|
||||
**Full guide:** [TypeScript Review Guide](typescript.md)
|
||||
|
||||
## React / React 19
|
||||
|
||||
- [ ] Hooks called conditionally or in loops (violates Rules of Hooks)
|
||||
- [ ] `useEffect` dependency array incomplete or incorrect
|
||||
- [ ] `useEffect` missing cleanup function (subscriptions, timers, fetches)
|
||||
- [ ] `useEffect` used for derived state (use `useMemo` instead)
|
||||
- [ ] `useMemo`/`useCallback` over-used or used without `React.memo`
|
||||
- [ ] Component defined inside another component (re-mounts every render)
|
||||
- [ ] Unstable props (inline objects/functions passed to memo components)
|
||||
- [ ] Direct mutation of props
|
||||
- [ ] List missing `key` or using array index as key (reorderable lists)
|
||||
- [ ] Server Component using client APIs (`useState`, `useEffect`, `onClick`)
|
||||
- [ ] `'use client'` on parent making entire subtree client-side
|
||||
- [ ] `useActionState` calling `setState` instead of returning new state
|
||||
- [ ] `useFormStatus` called in same component as `<form>` (must be in child)
|
||||
- [ ] `useOptimistic` used for critical operations (payments, deletions)
|
||||
- [ ] Single Suspense boundary for entire page (slow blocks fast)
|
||||
- [ ] Missing Error Boundary wrapping Suspense
|
||||
- [ ] `use()` Hook receiving a new Promise each render
|
||||
|
||||
**TanStack Query v5:**
|
||||
- [ ] `queryKey` missing parameters that affect data
|
||||
- [ ] Default `staleTime: 0` causing excessive refetches
|
||||
- [ ] `useSuspenseQuery` with `enabled` option (not supported)
|
||||
- [ ] Mutation not invalidating related queries on success
|
||||
- [ ] Optimistic update missing rollback in `onError`
|
||||
- [ ] Using v4 array syntax (`useQuery(['key'], fn)`) instead of v5 object syntax
|
||||
|
||||
**Testing:**
|
||||
- [ ] Using `container.querySelector` instead of `screen.getByRole`
|
||||
- [ ] Using `fireEvent` instead of `userEvent`
|
||||
- [ ] Testing implementation details instead of user-visible behavior
|
||||
- [ ] Using `getBy*` for async content (use `findBy*`)
|
||||
|
||||
**Full guide:** [React Review Guide](react.md)
|
||||
|
||||
## Vue 3
|
||||
|
||||
- [ ] Destructuring `reactive()` object loses reactivity (use `toRefs`)
|
||||
- [ ] Passing `props.x` to composable instead of `() => props.x` or `toRef(props, 'x')`
|
||||
- [ ] `watch` with async callback missing `onCleanup` (race condition)
|
||||
- [ ] `computed` with side effects (mutations, API calls)
|
||||
- [ ] `v-for` using index as `:key` when list can reorder
|
||||
- [ ] `v-if` and `v-for` on the same element
|
||||
- [ ] `defineProps` without TypeScript type declaration
|
||||
- [ ] `withDefaults` object default values not using factory functions
|
||||
- [ ] Directly mutating props instead of emitting events
|
||||
- [ ] `watchEffect` with unclear dependencies causing over-triggering
|
||||
|
||||
**Full guide:** [Vue 3 Review Guide](vue.md)
|
||||
|
||||
## Python
|
||||
|
||||
- [ ] Mutable default arguments (`def f(x=[])`)
|
||||
- [ ] Bare `except:` catching `KeyboardInterrupt` and `SystemExit`
|
||||
- [ ] Shared mutable class attributes (`class C: items = []`)
|
||||
- [ ] Using `is` instead of `==` for value comparison
|
||||
- [ ] Forgetting `self` parameter in methods
|
||||
- [ ] Modifying list while iterating
|
||||
- [ ] String concatenation in loops (use `"".join()`)
|
||||
- [ ] Not closing files (use `with` statement)
|
||||
- [ ] Missing type annotations on public functions
|
||||
|
||||
**Full guide:** [Python Review Guide](python.md)
|
||||
|
||||
## Rust
|
||||
|
||||
**Ownership & Borrowing:**
|
||||
- [ ] Unnecessary `clone()` to work around borrow checker
|
||||
- [ ] `Arc<Mutex<T>>` when single-owner would suffice
|
||||
- [ ] Storing borrows in structs when owned data is simpler
|
||||
- [ ] Unnecessary `RefCell` (runtime checks vs compile-time)
|
||||
|
||||
**Unsafe Code:**
|
||||
- [ ] `unsafe` block without `SAFETY:` comment explaining invariants
|
||||
- [ ] `unsafe fn` without `# Safety` doc section
|
||||
- [ ] Unsafe invariants split across modules
|
||||
|
||||
**Async & Concurrency:**
|
||||
- [ ] Blocking in async context (`std::fs`, `std::thread::sleep`)
|
||||
- [ ] Holding `std::sync::Mutex` across `.await`
|
||||
- [ ] Spawned task missing `'static` lifetime bound
|
||||
- [ ] Dropping a Future without awaiting (forgotten work)
|
||||
|
||||
**Error Handling:**
|
||||
- [ ] `unwrap()`/`expect()` in production code
|
||||
- [ ] Library using `anyhow` instead of `thiserror` (callers can't match)
|
||||
- [ ] Swallowing error context (`map_err(|_| ...)`)
|
||||
- [ ] Ignoring `must_use` return values
|
||||
|
||||
**Performance:**
|
||||
- [ ] Unnecessary `.collect()` — prefer lazy iterators
|
||||
- [ ] String concatenation in loops without `with_capacity`
|
||||
- [ ] `Box<dyn Trait>` when `impl Trait` would work
|
||||
|
||||
**Full guide:** [Rust Review Guide](rust.md)
|
||||
|
||||
## Go
|
||||
|
||||
- [ ] Ignoring errors (`result, _ := SomeFunction()`)
|
||||
- [ ] Goroutine with no exit mechanism (leak)
|
||||
- [ ] Missing or incorrect `context.Context` propagation
|
||||
- [ ] Loop variable capture issue (Go < 1.22)
|
||||
- [ ] `defer` in loops (deferred until function, not loop iteration)
|
||||
- [ ] Variable shadowing
|
||||
- [ ] Map used before initialization
|
||||
- [ ] Error wrapping with `%v` instead of `%w` (breaks `errors.Is`/`errors.As`)
|
||||
|
||||
**Full guide:** [Go Review Guide](go.md)
|
||||
|
||||
## Java / Spring Boot
|
||||
|
||||
- [ ] POJO/DTO with manual boilerplate instead of `record`
|
||||
- [ ] Traditional switch missing `break` (use switch expressions)
|
||||
- [ ] Field injection instead of constructor injection
|
||||
- [ ] JPA N+1 query (missing `fetch join` or `@EntityGraph`)
|
||||
- [ ] Incorrect `equals`/`hashCode` on JPA entities (use business key, not ID)
|
||||
- [ ] `Optional.get()` without `isPresent()` check
|
||||
- [ ] Stream operations with side effects
|
||||
|
||||
**Full guide:** [Java Review Guide](java.md)
|
||||
|
||||
## PHP
|
||||
|
||||
- [ ] Missing `declare(strict_types=1);` in new files
|
||||
- [ ] Weak comparison (`==`, `!=`) in auth, token, payment, or state logic
|
||||
- [ ] `in_array()` / `array_search()` used without strict mode
|
||||
- [ ] SQL built with string concatenation instead of prepared statements
|
||||
- [ ] User input echoed without context-aware escaping
|
||||
- [ ] Passwords stored with `md5()` / `sha1()` instead of `password_hash()`
|
||||
- [ ] Untrusted data passed to `unserialize()`
|
||||
- [ ] PHP 8.2+ dynamic properties used instead of declared properties
|
||||
- [ ] Errors hidden with `@` or swallowed in empty `catch` blocks
|
||||
- [ ] File uploads using client-provided names or missing MIME/size validation
|
||||
|
||||
**Full guide:** [PHP Review Guide](php.md)
|
||||
|
||||
## Swift
|
||||
|
||||
- [ ] Force-unwrap (`!`) or `try!` where safe unwrapping is possible
|
||||
- [ ] Closure capturing `self` strongly without `[weak self]` (retain cycle)
|
||||
- [ ] Reference type (`class`) used where a value type (`struct`) is intended
|
||||
- [ ] Errors swallowed instead of propagated via `throws` / `Result`
|
||||
- [ ] Data race across concurrency boundaries (missing `Sendable`, `@MainActor`, actor isolation)
|
||||
- [ ] Fire-and-forget `Task {}` that is never cancelled or leaks
|
||||
- [ ] `@ObservedObject` used where `@StateObject` is required for ownership
|
||||
- [ ] Implicitly unwrapped optional (`var x: T!`) outside IBOutlets
|
||||
- [ ] Over-broad access control (`public` / `open` where `internal` suffices)
|
||||
|
||||
**Full guide:** [Swift Review Guide](swift.md)
|
||||
|
||||
## C
|
||||
|
||||
- [ ] Pointer/buffer overflow or underflow
|
||||
- [ ] Undefined behavior (use-after-free, double-free, null deref)
|
||||
- [ ] Missing error handling after allocation (`malloc` can return `NULL`)
|
||||
- [ ] Integer overflow in size calculations
|
||||
- [ ] Resource leaks (missing `free`, `fclose`, etc.)
|
||||
- [ ] Missing `static` on file-local functions/variables
|
||||
|
||||
**Full guide:** [C Review Guide](c.md)
|
||||
|
||||
## C++
|
||||
|
||||
- [ ] Missing RAII wrapper for resources
|
||||
- [ ] Violating Rule of 0/3/5 (destructor, copy, move)
|
||||
- [ ] Exception safety issues (no `noexcept` where applicable)
|
||||
- [ ] Dangling references from returned iterators or references
|
||||
- [ ] Unnecessary copies (missing `std::move` or pass-by-reference)
|
||||
|
||||
**Full guide:** [C++ Review Guide](cpp.md)
|
||||
|
||||
## SQL
|
||||
|
||||
- [ ] String concatenation for queries (SQL injection risk) — use parameterized queries
|
||||
- [ ] Missing indexes on filtered/joined columns
|
||||
- [ ] `SELECT *` instead of specific columns
|
||||
- [ ] N+1 query patterns
|
||||
- [ ] Missing `LIMIT` on large tables
|
||||
- [ ] Not handling `NULL` comparisons correctly (`IS NULL` vs `= NULL`)
|
||||
- [ ] Missing transactions for related operations
|
||||
- [ ] Incorrect JOIN types
|
||||
- [ ] Collation / case sensitivity surprises across databases (MySQL vs Postgres defaults)
|
||||
- [ ] Date and timezone handling errors (naive timestamps, server-local `NOW()`, DST)
|
||||
|
||||
**See also:** [Security Review Guide](security-review-guide.md) for SQL injection prevention
|
||||
|
||||
## API Design
|
||||
|
||||
- [ ] Inconsistent resource naming
|
||||
- [ ] Wrong HTTP methods (POST for idempotent operations)
|
||||
- [ ] Missing pagination for list endpoints
|
||||
- [ ] Incorrect status codes
|
||||
- [ ] Missing rate limiting
|
||||
- [ ] Missing input validation and sanitization
|
||||
- [ ] Trusting client-side validation only
|
||||
|
||||
## Testing
|
||||
|
||||
- [ ] Testing implementation details instead of behavior
|
||||
- [ ] Missing edge case tests
|
||||
- [ ] Flaky tests (non-deterministic)
|
||||
- [ ] Tests with external dependencies (no mocks)
|
||||
- [ ] Missing negative tests (error cases)
|
||||
- [ ] Overly complex test setup
|
||||
@@ -0,0 +1,385 @@
|
||||
# C++ Code Review Guide
|
||||
|
||||
> C++ code review guide focused on memory safety, lifetime, API design, and performance. Examples assume C++17/20.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Ownership and RAII](#ownership-and-raii)
|
||||
- [Lifetime and References](#lifetime-and-references)
|
||||
- [Copy and Move Semantics](#copy-and-move-semantics)
|
||||
- [Const-Correctness and API Design](#const-correctness-and-api-design)
|
||||
- [Error Handling and Exception Safety](#error-handling-and-exception-safety)
|
||||
- [Concurrency](#concurrency)
|
||||
- [Performance and Allocation](#performance-and-allocation)
|
||||
- [Templates and Type Safety](#templates-and-type-safety)
|
||||
- [Tooling and Build Checks](#tooling-and-build-checks)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## Ownership and RAII
|
||||
|
||||
### Prefer RAII and smart pointers
|
||||
|
||||
Use RAII to express ownership. Default to `std::unique_ptr`, use `std::shared_ptr` only for shared lifetime.
|
||||
|
||||
```cpp
|
||||
// ❌ Bad: manual new/delete with early returns
|
||||
Foo* make_foo() {
|
||||
Foo* foo = new Foo();
|
||||
if (!foo->Init()) {
|
||||
delete foo;
|
||||
return nullptr;
|
||||
}
|
||||
return foo;
|
||||
}
|
||||
|
||||
// ✅ Good: RAII with unique_ptr
|
||||
std::unique_ptr<Foo> make_foo() {
|
||||
auto foo = std::make_unique<Foo>();
|
||||
if (!foo->Init()) {
|
||||
return {};
|
||||
}
|
||||
return foo;
|
||||
}
|
||||
```
|
||||
|
||||
### Wrap C resources
|
||||
|
||||
```cpp
|
||||
// ✅ Good: wrap FILE* with unique_ptr
|
||||
using FilePtr = std::unique_ptr<FILE, decltype(&fclose)>;
|
||||
|
||||
FilePtr open_file(const char* path) {
|
||||
return FilePtr(fopen(path, "rb"), &fclose);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Lifetime and References
|
||||
|
||||
### Avoid dangling references and views
|
||||
|
||||
`std::string_view` and `std::span` do not own data. Make sure the owner outlives the view.
|
||||
|
||||
```cpp
|
||||
// ❌ Bad: returning string_view to a temporary
|
||||
std::string_view bad_view() {
|
||||
std::string s = make_name();
|
||||
return s; // dangling
|
||||
}
|
||||
|
||||
// ✅ Good: return owning string
|
||||
std::string good_name() {
|
||||
return make_name();
|
||||
}
|
||||
|
||||
// ✅ Good: view tied to caller-owned data
|
||||
std::string_view good_view(const std::string& s) {
|
||||
return s;
|
||||
}
|
||||
```
|
||||
|
||||
### Lambda captures
|
||||
|
||||
```cpp
|
||||
// ❌ Bad: capture reference that escapes
|
||||
std::function<void()> make_task() {
|
||||
int value = 42;
|
||||
return [&]() { use(value); }; // dangling
|
||||
}
|
||||
|
||||
// ✅ Good: capture by value
|
||||
std::function<void()> make_task() {
|
||||
int value = 42;
|
||||
return [value]() { use(value); };
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Copy and Move Semantics
|
||||
|
||||
### Rule of 0/3/5
|
||||
|
||||
Prefer the Rule of 0 by using RAII types. If you own a resource, define or delete copy and move operations.
|
||||
|
||||
```cpp
|
||||
// ❌ Bad: raw ownership with default copy
|
||||
struct Buffer {
|
||||
int* data;
|
||||
size_t size;
|
||||
explicit Buffer(size_t n) : data(new int[n]), size(n) {}
|
||||
~Buffer() { delete[] data; }
|
||||
// copy ctor/assign are implicitly generated -> double delete
|
||||
};
|
||||
|
||||
// ✅ Good: Rule of 0 with std::vector
|
||||
struct Buffer {
|
||||
std::vector<int> data;
|
||||
explicit Buffer(size_t n) : data(n) {}
|
||||
};
|
||||
```
|
||||
|
||||
### Delete unwanted copies
|
||||
|
||||
```cpp
|
||||
struct Socket {
|
||||
Socket() = default;
|
||||
~Socket() { close(); }
|
||||
|
||||
Socket(const Socket&) = delete;
|
||||
Socket& operator=(const Socket&) = delete;
|
||||
Socket(Socket&&) noexcept = default;
|
||||
Socket& operator=(Socket&&) noexcept = default;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Const-Correctness and API Design
|
||||
|
||||
### Use const and explicit
|
||||
|
||||
```cpp
|
||||
class User {
|
||||
public:
|
||||
const std::string& name() const { return name_; }
|
||||
void set_name(std::string name) { name_ = std::move(name); }
|
||||
|
||||
private:
|
||||
std::string name_;
|
||||
};
|
||||
|
||||
struct Millis {
|
||||
explicit Millis(int v) : value(v) {}
|
||||
int value;
|
||||
};
|
||||
```
|
||||
|
||||
### Avoid object slicing
|
||||
|
||||
```cpp
|
||||
struct Shape { virtual ~Shape() = default; };
|
||||
struct Circle : Shape { void draw() const; };
|
||||
|
||||
// ❌ Bad: slices Circle into Shape
|
||||
void draw(Shape shape);
|
||||
|
||||
// ✅ Good: pass by reference
|
||||
void draw(const Shape& shape);
|
||||
```
|
||||
|
||||
### Use override and final
|
||||
|
||||
```cpp
|
||||
struct Base {
|
||||
virtual void run() = 0;
|
||||
};
|
||||
|
||||
struct Worker final : Base {
|
||||
void run() override {}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling and Exception Safety
|
||||
|
||||
### Prefer RAII for cleanup
|
||||
|
||||
```cpp
|
||||
// ✅ Good: RAII handles cleanup on exceptions
|
||||
void process() {
|
||||
std::vector<int> data = load_data(); // safe cleanup
|
||||
do_work(data);
|
||||
}
|
||||
```
|
||||
|
||||
### Do not throw from destructors
|
||||
|
||||
```cpp
|
||||
struct File {
|
||||
~File() noexcept { close(); }
|
||||
void close();
|
||||
};
|
||||
```
|
||||
|
||||
### Use expected results for normal failures
|
||||
|
||||
```cpp
|
||||
// ✅ Expected error: use optional or expected
|
||||
std::optional<int> parse_int(const std::string& s) {
|
||||
try {
|
||||
return std::stoi(s);
|
||||
} catch (...) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Concurrency
|
||||
|
||||
### Protect shared data
|
||||
|
||||
```cpp
|
||||
// ❌ Bad: data race
|
||||
int counter = 0;
|
||||
void inc() { counter++; }
|
||||
|
||||
// ✅ Good: atomic
|
||||
std::atomic<int> counter{0};
|
||||
void inc() { counter.fetch_add(1, std::memory_order_relaxed); }
|
||||
```
|
||||
|
||||
### Use RAII locks
|
||||
|
||||
```cpp
|
||||
std::mutex mu;
|
||||
std::vector<int> data;
|
||||
|
||||
void add(int v) {
|
||||
std::lock_guard<std::mutex> lock(mu);
|
||||
data.push_back(v);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance and Allocation
|
||||
|
||||
### Avoid repeated allocations
|
||||
|
||||
```cpp
|
||||
// ❌ Bad: repeated reallocation
|
||||
std::vector<int> build(int n) {
|
||||
std::vector<int> out;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
out.push_back(i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ✅ Good: reserve upfront
|
||||
std::vector<int> build(int n) {
|
||||
std::vector<int> out;
|
||||
out.reserve(static_cast<size_t>(n));
|
||||
for (int i = 0; i < n; ++i) {
|
||||
out.push_back(i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
```
|
||||
|
||||
### String concatenation
|
||||
|
||||
```cpp
|
||||
// ❌ Bad: repeated allocation
|
||||
std::string join(const std::vector<std::string>& parts) {
|
||||
std::string out;
|
||||
for (const auto& p : parts) {
|
||||
out += p;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ✅ Good: reserve total size
|
||||
std::string join(const std::vector<std::string>& parts) {
|
||||
size_t total = 0;
|
||||
for (const auto& p : parts) {
|
||||
total += p.size();
|
||||
}
|
||||
std::string out;
|
||||
out.reserve(total);
|
||||
for (const auto& p : parts) {
|
||||
out += p;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Templates and Type Safety
|
||||
|
||||
### Prefer constrained templates (C++20)
|
||||
|
||||
```cpp
|
||||
// ❌ Bad: overly generic
|
||||
template <typename T>
|
||||
T add(T a, T b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
// ✅ Good: constrained
|
||||
template <typename T>
|
||||
requires std::is_integral_v<T>
|
||||
T add(T a, T b) {
|
||||
return a + b;
|
||||
}
|
||||
```
|
||||
|
||||
### Use static_assert for invariants
|
||||
|
||||
```cpp
|
||||
template <typename T>
|
||||
struct Packet {
|
||||
static_assert(std::is_trivially_copyable_v<T>,
|
||||
"Packet payload must be trivially copyable");
|
||||
T payload;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tooling and Build Checks
|
||||
|
||||
```bash
|
||||
# Warnings
|
||||
clang++ -Wall -Wextra -Werror -Wconversion -Wshadow -std=c++20 ...
|
||||
|
||||
# Sanitizers (debug builds)
|
||||
clang++ -fsanitize=address,undefined -fno-omit-frame-pointer -g ...
|
||||
clang++ -fsanitize=thread -fno-omit-frame-pointer -g ...
|
||||
|
||||
# Static analysis
|
||||
clang-tidy src/*.cpp -- -std=c++20
|
||||
|
||||
# Formatting
|
||||
clang-format -i src/*.cpp include/*.h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### Safety and Lifetime
|
||||
- [ ] Ownership is explicit (RAII, unique_ptr by default)
|
||||
- [ ] No dangling references or views
|
||||
- [ ] Rule of 0/3/5 followed for resource-owning types
|
||||
- [ ] No raw new/delete in business logic
|
||||
- [ ] Destructors are noexcept and do not throw
|
||||
|
||||
### API and Design
|
||||
- [ ] const-correctness is applied consistently
|
||||
- [ ] Constructors are explicit where needed
|
||||
- [ ] Override/final used for virtual functions
|
||||
- [ ] No object slicing (pass by ref or pointer)
|
||||
|
||||
### Concurrency
|
||||
- [ ] Shared data is protected (mutex or atomics)
|
||||
- [ ] Locking order is consistent
|
||||
- [ ] No blocking while holding locks
|
||||
|
||||
### Performance
|
||||
- [ ] Unnecessary allocations avoided (reserve, move)
|
||||
- [ ] Copies avoided in hot paths
|
||||
- [ ] Algorithmic complexity is reasonable
|
||||
|
||||
### Tooling and Tests
|
||||
- [ ] Builds clean with warnings enabled
|
||||
- [ ] Sanitizers run on critical code paths
|
||||
- [ ] Static analysis (clang-tidy) results are addressed
|
||||
@@ -0,0 +1,521 @@
|
||||
# C# / .NET Code Review Guide
|
||||
|
||||
> C# / .NET 8 代码审查指南,覆盖 C# 12 新特性、异步编程、EF Core 性能、ASP.NET Core 最佳实践、依赖注入、LINQ 等核心主题。
|
||||
|
||||
## 目录
|
||||
|
||||
- [C# 12 新特性](#c-12-新特性)
|
||||
- [异步编程](#异步编程)
|
||||
- [EF Core 性能](#ef-core-性能)
|
||||
- [ASP.NET Core 最佳实践](#aspnet-core-最佳实践)
|
||||
- [依赖注入](#依赖注入)
|
||||
- [LINQ 最佳实践](#linq-最佳实践)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## C# 12 新特性
|
||||
|
||||
### Primary Constructors(非 record 类型)
|
||||
|
||||
```csharp
|
||||
// ❌ 样板代码过多的传统构造函数
|
||||
public class ProductService
|
||||
{
|
||||
private readonly ProductDbContext _db;
|
||||
private readonly ILogger<ProductService> _logger;
|
||||
|
||||
public ProductService(ProductDbContext db, ILogger<ProductService> logger)
|
||||
{
|
||||
_db = db;
|
||||
_logger = logger;
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Primary Constructor——简洁的依赖注入
|
||||
public class ProductService(ProductDbContext db, ILogger<ProductService> logger)
|
||||
{
|
||||
public async Task<Product?> GetAsync(int id)
|
||||
=> await db.Products.FindAsync(id);
|
||||
}
|
||||
|
||||
// ⚠️ 注意:primary constructor 参数不是属性,不能被重新赋值
|
||||
// ⚠️ 如果需要长期存储,显式声明字段
|
||||
public class OrderService(OrderDbContext db)
|
||||
{
|
||||
private readonly OrderDbContext _db = db; // 显式捕获
|
||||
}
|
||||
```
|
||||
|
||||
### Collection Expressions
|
||||
|
||||
```csharp
|
||||
// ❌ 传统集合初始化
|
||||
int[] nums = new int[] { 1, 2, 3 };
|
||||
List<string> names = new List<string> { "alice", "bob" };
|
||||
|
||||
// ✅ 集合表达式
|
||||
int[] nums = [1, 2, 3];
|
||||
List<string> names = ["alice", "bob"];
|
||||
Span<char> span = ['a', 'b'];
|
||||
|
||||
// ✅ 展开运算符
|
||||
int[] merged = [..nums, 4, 5];
|
||||
```
|
||||
|
||||
### Default Lambda Parameters
|
||||
|
||||
```csharp
|
||||
// ❌ 重载 lambda
|
||||
var add = (int a, int b) => a + b;
|
||||
var addDefault = (int a) => a + 1;
|
||||
|
||||
// ✅ 默认参数
|
||||
var add = (int a, int b = 1) => a + b;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 异步编程
|
||||
|
||||
### Task.Wait() / .Result / async void 是严重反模式
|
||||
|
||||
```csharp
|
||||
// ❌ Task.Wait() —— 死锁风险(同步阻塞异步操作)
|
||||
public ActionResult<Data> Get(int id)
|
||||
{
|
||||
var data = _service.GetDataAsync(id).Result; // 死锁!
|
||||
return Ok(data);
|
||||
}
|
||||
|
||||
// ❌ async void —— 异常无法捕获,会崩溃进程
|
||||
public async void HandleEvent()
|
||||
{
|
||||
await _service.ProcessAsync(); // 异常直接崩溃
|
||||
}
|
||||
|
||||
// ✅ async Task —— 全链路异步
|
||||
public async Task<ActionResult<Data>> Get(int id)
|
||||
{
|
||||
var data = await _service.GetDataAsync(id);
|
||||
return Ok(data);
|
||||
}
|
||||
```
|
||||
|
||||
### ConfigureAwait(false) 用于库代码
|
||||
|
||||
```csharp
|
||||
// ❌ 库代码不必要地捕获 SynchronizationContext
|
||||
public class LibraryService
|
||||
{
|
||||
public async Task<string> GetDataAsync()
|
||||
{
|
||||
var response = await _httpClient.GetAsync("/api/data");
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 库代码使用 ConfigureAwait(false) 避免死锁
|
||||
public class LibraryService
|
||||
{
|
||||
public async Task<string> GetDataAsync()
|
||||
{
|
||||
var response = await _httpClient.GetAsync("/api/data").ConfigureAwait(false);
|
||||
return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### CancellationToken 传播
|
||||
|
||||
```csharp
|
||||
// ❌ 丢弃 CancellationToken
|
||||
public async Task<List<User>> SearchAsync(string query)
|
||||
{
|
||||
return await _db.Users.Where(u => u.Name.Contains(query)).ToListAsync();
|
||||
}
|
||||
|
||||
// ✅ 全链路传递 CancellationToken
|
||||
public async Task<List<User>> SearchAsync(string query, CancellationToken ct = default)
|
||||
{
|
||||
return await _db.Users
|
||||
.Where(u => u.Name.Contains(query))
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
```
|
||||
|
||||
### Async Disposal
|
||||
|
||||
```csharp
|
||||
// ❌ 同步 dispose 异步资源
|
||||
public class DataClient : IDisposable
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
_httpClient.Dispose(); // 可能丢弃正在进行的请求
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ IAsyncDisposable
|
||||
public class DataClient : IAsyncDisposable
|
||||
{
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _stream.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 调用方使用 await using
|
||||
await using var client = new DataClient();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## EF Core 性能
|
||||
|
||||
### N+1 查询问题
|
||||
|
||||
```csharp
|
||||
// ❌ 经典 N+1——每个 Blog 触发一次查询获取 Posts
|
||||
foreach (var blog in await context.Blogs.ToListAsync())
|
||||
{
|
||||
foreach (var post in blog.Posts) // 每次循环都查询数据库!
|
||||
{
|
||||
Console.WriteLine(post.Title);
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Eager Loading + 投影
|
||||
await foreach (var blog in context.Blogs
|
||||
.Select(b => new { b.Url, b.Posts })
|
||||
.AsAsyncEnumerable())
|
||||
{
|
||||
foreach (var post in blog.Posts)
|
||||
Console.WriteLine(post.Title);
|
||||
}
|
||||
```
|
||||
|
||||
### 过度获取(不投影)
|
||||
|
||||
```csharp
|
||||
// ❌ 加载所有列——只需要 Url 时加载了全部字段
|
||||
var urls = await context.Blogs.ToListAsync();
|
||||
|
||||
// ✅ 只投影需要的字段
|
||||
var urls = await context.Blogs
|
||||
.Select(b => b.Url)
|
||||
.ToListAsync();
|
||||
```
|
||||
|
||||
### 缺少分页
|
||||
|
||||
```csharp
|
||||
// ❌ 无界结果集
|
||||
var posts = await context.Posts
|
||||
.Where(p => p.Title.StartsWith("A"))
|
||||
.ToListAsync(); // 可能有百万条记录!
|
||||
|
||||
// ✅ 限制结果数量
|
||||
var posts = await context.Posts
|
||||
.Where(p => p.Title.StartsWith("A"))
|
||||
.OrderBy(p => p.Id)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
```
|
||||
|
||||
### Cartesian Explosion(JOIN 笛卡尔爆炸)
|
||||
|
||||
```csharp
|
||||
// ❌ 多个 Include 创建大量重复数据
|
||||
var blogs = await context.Blogs
|
||||
.Include(b => b.Posts)
|
||||
.Include(b => b.Tags)
|
||||
.ToListAsync(); // 每行重复 Blog 数据
|
||||
|
||||
// ✅ 使用 AsSplitQuery 拆分查询
|
||||
var blogs = await context.Blogs
|
||||
.Include(b => b.Posts)
|
||||
.Include(b => b.Tags)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
```
|
||||
|
||||
### 只读场景缺少 AsNoTracking
|
||||
|
||||
```csharp
|
||||
// ❌ 默认跟踪——只读查询也付出跟踪开销
|
||||
var products = await context.Products.ToListAsync();
|
||||
|
||||
// ✅ AsNoTracking——跳过变更跟踪,更快且更省内存
|
||||
var products = await context.Products
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
```
|
||||
|
||||
### 列上函数阻止索引使用
|
||||
|
||||
```csharp
|
||||
// ✅ 可以使用索引——sargable
|
||||
var posts1 = await context.Posts
|
||||
.Where(p => p.Title.StartsWith("A"))
|
||||
.ToListAsync();
|
||||
|
||||
// ❌ 无法使用索引——全表扫描
|
||||
var posts2 = await context.Posts
|
||||
.Where(p => p.Title.EndsWith("A"))
|
||||
.ToListAsync();
|
||||
|
||||
// ❌ 列上套函数——全表扫描
|
||||
var posts3 = await context.Posts
|
||||
.Where(p => p.Title.ToLower() == "foo")
|
||||
.ToListAsync();
|
||||
```
|
||||
|
||||
### 同步 vs 异步数据库访问
|
||||
|
||||
```csharp
|
||||
// ❌ 同步数据库调用——阻塞线程
|
||||
var products = context.Products.ToList();
|
||||
context.SaveChanges();
|
||||
|
||||
// ✅ 异步数据库调用
|
||||
var products = await context.Products.ToListAsync();
|
||||
await context.SaveChangesAsync();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ASP.NET Core 最佳实践
|
||||
|
||||
### HttpClient 误用
|
||||
|
||||
```csharp
|
||||
// ❌ 每次请求创建新的 HttpClient——socket 耗尽
|
||||
using var client = new HttpClient();
|
||||
var response = await client.GetAsync("https://api.example.com/data");
|
||||
|
||||
// ✅ IHttpClientFactory 注入
|
||||
public class MyService
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
public MyService(HttpClient client) => _client = client; // 从工厂注入
|
||||
}
|
||||
```
|
||||
|
||||
### HttpContext 在后台线程中使用
|
||||
|
||||
```csharp
|
||||
// ❌ 在后台任务中捕获 scoped 服务——请求结束后已释放
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await context.SaveChangesAsync(); // ObjectDisposedException!
|
||||
});
|
||||
|
||||
// ✅ 创建新的 scope
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await using var scope = serviceScopeFactory.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await db.SaveChangesAsync();
|
||||
});
|
||||
```
|
||||
|
||||
### Request.Form 同步访问
|
||||
|
||||
```csharp
|
||||
// ❌ 同步读取 Form——sync over async
|
||||
var form = HttpContext.Request.Form;
|
||||
|
||||
// ✅ 异步读取
|
||||
var form = await HttpContext.Request.ReadFormAsync();
|
||||
```
|
||||
|
||||
### 异常用于控制流
|
||||
|
||||
```csharp
|
||||
// ❌ 用异常判断是否存在——异常开销大,比直接检查慢得多
|
||||
try
|
||||
{
|
||||
var user = await _db.Users.FirstAsync(u => u.Id == id);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
// ✅ 使用检查而非异常
|
||||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == id);
|
||||
if (user is null) return NotFound();
|
||||
```
|
||||
|
||||
### 响应头在 Body 之后设置
|
||||
|
||||
```csharp
|
||||
// ❌ body 已发送后再设置 header——抛异常
|
||||
await next(context);
|
||||
context.Response.Headers["X-Custom"] = "value"; // 可能抛异常!
|
||||
|
||||
// ✅ 使用 OnStarting 回调
|
||||
context.Response.OnStarting(() =>
|
||||
{
|
||||
context.Response.Headers["X-Custom"] = "value";
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
await next(context);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 依赖注入
|
||||
|
||||
### Scoped 服务注入 Singleton
|
||||
|
||||
```csharp
|
||||
// ❌ Scoped 服务注入 Singleton——生命周期不匹配
|
||||
services.AddSingleton<BackgroundWorker>();
|
||||
services.AddScoped<IUserRepository, UserRepository>();
|
||||
|
||||
// BackgroundWorker 是 Singleton,UserRepository 是 Scoped
|
||||
// → UserRepository 在多个请求间共享或已释放
|
||||
|
||||
// ✅ 在 Singleton 中通过 IServiceProvider 创建 scope
|
||||
public class BackgroundWorker : BackgroundService
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
|
||||
public BackgroundWorker(IServiceScopeFactory scopeFactory)
|
||||
=> _scopeFactory = scopeFactory;
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken ct)
|
||||
{
|
||||
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||
var repo = scope.ServiceProvider.GetRequiredService<IUserRepository>();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## LINQ 最佳实践
|
||||
|
||||
### ToList 之后再 LINQ
|
||||
|
||||
```csharp
|
||||
// ❌ 先 ToList 再过滤——全表加载到内存
|
||||
var results = context.Posts
|
||||
.Where(p => p.Title.StartsWith("A"))
|
||||
.ToList()
|
||||
.Where(p => SomeClientFilter(p)); // 客户端过滤,已加载全部行
|
||||
|
||||
// ✅ 尽可能让数据库执行过滤
|
||||
var results = await context.Posts
|
||||
.Where(p => p.Title.StartsWith("A") && SomeDbFilter(p))
|
||||
.AsAsyncEnumerable()
|
||||
.Where(p => SomeClientFilter(p)) // 只过滤数据库返回的行
|
||||
.ToListAsync();
|
||||
```
|
||||
|
||||
### Count() vs Any()
|
||||
|
||||
```csharp
|
||||
// ❌ Count() 执行完整查询
|
||||
if (context.Users.Count() > 0) { /* ... */ }
|
||||
|
||||
// ✅ Any() 更高效——遇到第一条记录就返回
|
||||
if (await context.Users.AnyAsync()) { /* ... */ }
|
||||
```
|
||||
|
||||
### 多次枚举 IEnumerable
|
||||
|
||||
```csharp
|
||||
// ❌ IEnumerable 被枚举两次
|
||||
public void Process(IEnumerable<int> numbers)
|
||||
{
|
||||
if (numbers.Any()) // 第一次枚举
|
||||
{
|
||||
foreach (var n in numbers) // 第二次枚举(可能是重新查询)
|
||||
{
|
||||
Console.WriteLine(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 如果需要多次使用,先物化
|
||||
public void Process(IEnumerable<int> numbers)
|
||||
{
|
||||
var list = numbers.ToList(); // 只枚举一次
|
||||
if (list.Any())
|
||||
{
|
||||
foreach (var n in list)
|
||||
{
|
||||
Console.WriteLine(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Select 中的副作用
|
||||
|
||||
```csharp
|
||||
// ❌ Select 中执行副作用——不可预测的执行时机
|
||||
var results = users.Select(u =>
|
||||
{
|
||||
_logger.LogInformation($"Processing {u.Name}"); // 副作用!
|
||||
return u.Email;
|
||||
}).ToList();
|
||||
|
||||
// ✅ 副作用放在 foreach 中
|
||||
foreach (var user in users)
|
||||
{
|
||||
_logger.LogInformation("Processing {Name}", user.Name);
|
||||
}
|
||||
var results = users.Select(u => u.Email).ToList();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### C# 12 新特性
|
||||
|
||||
- [ ] Primary constructor 参数不被重新赋值
|
||||
- [ ] 集合表达式语法一致(不混用新旧风格)
|
||||
|
||||
### 异步编程
|
||||
|
||||
- [ ] 无 `Task.Wait()`、`.Result`、`async void`
|
||||
- [ ] 库代码使用 `ConfigureAwait(false)`
|
||||
- [ ] `CancellationToken` 全链路传递
|
||||
- [ ] 异步资源使用 `IAsyncDisposable` / `await using`
|
||||
- [ ] 不混用同步和异步数据访问
|
||||
|
||||
### EF Core
|
||||
|
||||
- [ ] 无 N+1 查询(导航属性在循环中访问)
|
||||
- [ ] 投影 `Select()` 避免过度获取
|
||||
- [ ] 分页:`ToListAsync()` 前有 `Take()`/`Skip()`
|
||||
- [ ] 多个 `Include()` 使用 `AsSplitQuery()`
|
||||
- [ ] 只读查询使用 `AsNoTracking()`
|
||||
- [ ] 列上无函数调用阻止索引使用
|
||||
- [ ] 数据库调用全部异步
|
||||
|
||||
### ASP.NET Core
|
||||
|
||||
- [ ] HttpClient 通过 `IHttpClientFactory` 获取
|
||||
- [ ] 后台任务中不直接使用 scoped 服务
|
||||
- [ ] 使用 `ReadFormAsync` 代替 `Request.Form`
|
||||
- [ ] 异常不用于控制流
|
||||
- [ ] 响应头通过 `OnStarting` 设置
|
||||
|
||||
### 依赖注入
|
||||
|
||||
- [ ] Scoped 服务不注入 Singleton
|
||||
- [ ] 后台任务创建新 scope
|
||||
|
||||
### LINQ
|
||||
|
||||
- [ ] 无不必要的 `ToList()` 后再 LINQ
|
||||
- [ ] `Any()` 代替 `Count() > 0`
|
||||
- [ ] IEnumerable 不被多次枚举(或先物化)
|
||||
- [ ] Select 中无副作用
|
||||
@@ -0,0 +1,661 @@
|
||||
# CSS / Less / Sass Review Guide
|
||||
|
||||
CSS 及预处理器代码审查指南,覆盖性能、可维护性、响应式设计和浏览器兼容性。
|
||||
|
||||
## CSS 变量 vs 硬编码
|
||||
|
||||
### 应该使用变量的场景
|
||||
|
||||
```css
|
||||
/* ❌ 硬编码 - 难以维护 */
|
||||
.button {
|
||||
background: #3b82f6;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.card {
|
||||
border: 1px solid #3b82f6;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* ✅ 使用 CSS 变量 */
|
||||
:root {
|
||||
--color-primary: #3b82f6;
|
||||
--radius-md: 8px;
|
||||
}
|
||||
.button {
|
||||
background: var(--color-primary);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
.card {
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
```
|
||||
|
||||
### 变量命名规范
|
||||
|
||||
```css
|
||||
/* 推荐的变量分类 */
|
||||
:root {
|
||||
/* 颜色 */
|
||||
--color-primary: #3b82f6;
|
||||
--color-primary-hover: #2563eb;
|
||||
--color-text: #1f2937;
|
||||
--color-text-muted: #6b7280;
|
||||
--color-bg: #ffffff;
|
||||
--color-border: #e5e7eb;
|
||||
|
||||
/* 间距 */
|
||||
--spacing-xs: 4px;
|
||||
--spacing-sm: 8px;
|
||||
--spacing-md: 16px;
|
||||
--spacing-lg: 24px;
|
||||
--spacing-xl: 32px;
|
||||
|
||||
/* 字体 */
|
||||
--font-size-sm: 14px;
|
||||
--font-size-base: 16px;
|
||||
--font-size-lg: 18px;
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
/* 圆角 */
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* 阴影 */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
|
||||
/* 过渡 */
|
||||
--transition-fast: 150ms ease;
|
||||
--transition-normal: 300ms ease;
|
||||
}
|
||||
```
|
||||
|
||||
### 变量作用域建议
|
||||
|
||||
```css
|
||||
/* ✅ 组件级变量 - 减少全局污染 */
|
||||
.card {
|
||||
--card-padding: var(--spacing-md);
|
||||
--card-radius: var(--radius-md);
|
||||
|
||||
padding: var(--card-padding);
|
||||
border-radius: var(--card-radius);
|
||||
}
|
||||
|
||||
/* ⚠️ 避免频繁用 JS 动态修改变量 - 影响性能 */
|
||||
```
|
||||
|
||||
### 审查清单
|
||||
|
||||
- [ ] 颜色值是否使用变量?
|
||||
- [ ] 间距是否来自设计系统?
|
||||
- [ ] 重复值是否提取为变量?
|
||||
- [ ] 变量命名是否语义化?
|
||||
|
||||
---
|
||||
|
||||
## !important 使用规范
|
||||
|
||||
### 何时可以使用
|
||||
|
||||
```css
|
||||
/* ✅ 工具类 - 明确需要覆盖 */
|
||||
.hidden { display: none !important; }
|
||||
.sr-only { position: absolute !important; }
|
||||
|
||||
/* ✅ 覆盖第三方库样式(无法修改源码时) */
|
||||
.third-party-modal {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
/* ✅ 打印样式 */
|
||||
@media print {
|
||||
.no-print { display: none !important; }
|
||||
}
|
||||
```
|
||||
|
||||
### 何时禁止使用
|
||||
|
||||
```css
|
||||
/* ❌ 解决特异性问题 - 应该重构选择器 */
|
||||
.button {
|
||||
background: blue !important; /* 为什么需要 !important? */
|
||||
}
|
||||
|
||||
/* ❌ 覆盖自己写的样式 */
|
||||
.card { padding: 20px; }
|
||||
.card { padding: 30px !important; } /* 直接修改原规则 */
|
||||
|
||||
/* ❌ 在组件样式中 */
|
||||
.my-component .title {
|
||||
font-size: 24px !important; /* 破坏组件封装 */
|
||||
}
|
||||
```
|
||||
|
||||
### 替代方案
|
||||
|
||||
```css
|
||||
/* 问题:需要覆盖 .btn 的样式 */
|
||||
|
||||
/* ❌ 使用 !important */
|
||||
.my-btn {
|
||||
background: red !important;
|
||||
}
|
||||
|
||||
/* ✅ 提高特异性 */
|
||||
button.my-btn {
|
||||
background: red;
|
||||
}
|
||||
|
||||
/* ✅ 使用更具体的选择器 */
|
||||
.container .my-btn {
|
||||
background: red;
|
||||
}
|
||||
|
||||
/* ✅ 使用 :where() 降低被覆盖样式的特异性 */
|
||||
:where(.btn) {
|
||||
background: blue; /* 特异性为 0 */
|
||||
}
|
||||
.my-btn {
|
||||
background: red; /* 可以正常覆盖 */
|
||||
}
|
||||
```
|
||||
|
||||
### 审查问题
|
||||
|
||||
```markdown
|
||||
🔴 [blocking] "发现 15 处 !important,请说明每处的必要性"
|
||||
🟡 [important] "这个 !important 可以通过调整选择器特异性来解决"
|
||||
💡 [suggestion] "考虑使用 CSS Layers (@layer) 来管理样式优先级"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能考虑
|
||||
|
||||
### 🔴 高危性能问题
|
||||
|
||||
#### 1. `transition: all` 问题
|
||||
|
||||
```css
|
||||
/* ❌ 性能杀手 - 浏览器检查所有可动画属性 */
|
||||
.button {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* ✅ 明确指定属性 */
|
||||
.button {
|
||||
transition: background-color 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* ✅ 多属性时使用变量 */
|
||||
.button {
|
||||
--transition-duration: 0.3s;
|
||||
transition:
|
||||
background-color var(--transition-duration) ease,
|
||||
box-shadow var(--transition-duration) ease,
|
||||
transform var(--transition-duration) ease;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. box-shadow 动画
|
||||
|
||||
```css
|
||||
/* ❌ 每帧触发重绘 - 严重影响性能 */
|
||||
.card {
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
.card:hover {
|
||||
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* ✅ 使用伪元素 + opacity */
|
||||
.card {
|
||||
position: relative;
|
||||
}
|
||||
.card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
}
|
||||
.card:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 触发布局(Reflow)的属性
|
||||
|
||||
```css
|
||||
/* ❌ 动画这些属性会触发布局重计算 */
|
||||
.bad-animation {
|
||||
transition: width 0.3s, height 0.3s, top 0.3s, left 0.3s, margin 0.3s;
|
||||
}
|
||||
|
||||
/* ✅ 只动画 transform 和 opacity(仅触发合成) */
|
||||
.good-animation {
|
||||
transition: transform 0.3s, opacity 0.3s;
|
||||
}
|
||||
|
||||
/* 位移用 translate 代替 top/left */
|
||||
.move {
|
||||
transform: translateX(100px); /* ✅ */
|
||||
/* left: 100px; */ /* ❌ */
|
||||
}
|
||||
|
||||
/* 缩放用 scale 代替 width/height */
|
||||
.grow {
|
||||
transform: scale(1.1); /* ✅ */
|
||||
/* width: 110%; */ /* ❌ */
|
||||
}
|
||||
```
|
||||
|
||||
### 🟡 中等性能问题
|
||||
|
||||
#### 复杂选择器
|
||||
|
||||
```css
|
||||
/* ❌ 过深的嵌套 - 选择器匹配慢 */
|
||||
.page .container .content .article .section .paragraph span {
|
||||
color: red;
|
||||
}
|
||||
|
||||
/* ✅ 扁平化 */
|
||||
.article-text {
|
||||
color: red;
|
||||
}
|
||||
|
||||
/* ❌ 通配符选择器 */
|
||||
* { box-sizing: border-box; } /* 影响所有元素 */
|
||||
[class*="icon-"] { display: inline; } /* 属性选择器较慢 */
|
||||
|
||||
/* ✅ 限制范围 */
|
||||
.icon-box * { box-sizing: border-box; }
|
||||
```
|
||||
|
||||
#### 大量阴影和滤镜
|
||||
|
||||
```css
|
||||
/* ⚠️ 复杂阴影影响渲染性能 */
|
||||
.heavy-shadow {
|
||||
box-shadow:
|
||||
0 1px 2px rgba(0,0,0,0.1),
|
||||
0 2px 4px rgba(0,0,0,0.1),
|
||||
0 4px 8px rgba(0,0,0,0.1),
|
||||
0 8px 16px rgba(0,0,0,0.1),
|
||||
0 16px 32px rgba(0,0,0,0.1); /* 5 层阴影 */
|
||||
}
|
||||
|
||||
/* ⚠️ 滤镜消耗 GPU */
|
||||
.blur-heavy {
|
||||
filter: blur(20px) brightness(1.2) contrast(1.1);
|
||||
backdrop-filter: blur(10px); /* 更消耗性能 */
|
||||
}
|
||||
```
|
||||
|
||||
### 性能优化建议
|
||||
|
||||
```css
|
||||
/* 使用 will-change 提示浏览器(谨慎使用) */
|
||||
.animated-element {
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
/* 动画完成后移除 will-change */
|
||||
.animated-element.idle {
|
||||
will-change: auto;
|
||||
}
|
||||
|
||||
/* 使用 contain 限制重绘范围 */
|
||||
.card {
|
||||
contain: layout paint; /* 告诉浏览器内部变化不影响外部 */
|
||||
}
|
||||
```
|
||||
|
||||
### 审查清单
|
||||
|
||||
- [ ] 是否使用 `transition: all`?
|
||||
- [ ] 是否动画 width/height/top/left?
|
||||
- [ ] box-shadow 是否被动画?
|
||||
- [ ] 选择器嵌套是否超过 3 层?
|
||||
- [ ] 是否有不必要的 `will-change`?
|
||||
|
||||
---
|
||||
|
||||
## 响应式设计检查点
|
||||
|
||||
### Mobile First 原则
|
||||
|
||||
```css
|
||||
/* ✅ Mobile First - 基础样式针对移动端 */
|
||||
.container {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 逐步增强 */
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
padding: 24px;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.container {
|
||||
padding: 32px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* ❌ Desktop First - 需要覆盖更多样式 */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
padding: 32px;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.container {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.container {
|
||||
padding: 16px;
|
||||
flex-direction: column;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 断点建议
|
||||
|
||||
```css
|
||||
/* 推荐断点(基于内容而非设备) */
|
||||
:root {
|
||||
--breakpoint-sm: 640px; /* 大手机 */
|
||||
--breakpoint-md: 768px; /* 平板竖屏 */
|
||||
--breakpoint-lg: 1024px; /* 平板横屏/小笔记本 */
|
||||
--breakpoint-xl: 1280px; /* 桌面 */
|
||||
--breakpoint-2xl: 1536px; /* 大桌面 */
|
||||
}
|
||||
|
||||
/* 使用示例 */
|
||||
@media (min-width: 768px) { /* md */ }
|
||||
@media (min-width: 1024px) { /* lg */ }
|
||||
```
|
||||
|
||||
### 响应式审查清单
|
||||
|
||||
- [ ] 是否采用 Mobile First?
|
||||
- [ ] 断点是否基于内容断裂点而非设备?
|
||||
- [ ] 是否避免断点重叠?
|
||||
- [ ] 文字是否使用相对单位(rem/em)?
|
||||
- [ ] 触摸目标是否足够大(≥44px)?
|
||||
- [ ] 是否测试了横竖屏切换?
|
||||
|
||||
### 常见问题
|
||||
|
||||
```css
|
||||
/* ❌ 固定宽度 */
|
||||
.container {
|
||||
width: 1200px;
|
||||
}
|
||||
|
||||
/* ✅ 最大宽度 + 弹性 */
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
/* ❌ 固定高度的文本容器 */
|
||||
.text-box {
|
||||
height: 100px; /* 文字可能溢出 */
|
||||
}
|
||||
|
||||
/* ✅ 最小高度 */
|
||||
.text-box {
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
/* ❌ 小触摸目标 */
|
||||
.small-button {
|
||||
padding: 4px 8px; /* 太小,难以点击 */
|
||||
}
|
||||
|
||||
/* ✅ 足够的触摸区域 */
|
||||
.touch-button {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 浏览器兼容性
|
||||
|
||||
### 需要检查的特性
|
||||
|
||||
| 特性 | 兼容性 | 建议 |
|
||||
|------|--------|------|
|
||||
| CSS Grid | 现代浏览器 ✅ | IE 需要 Autoprefixer + 测试 |
|
||||
| Flexbox | 广泛支持 ✅ | 旧版需要前缀 |
|
||||
| CSS Variables | 现代浏览器 ✅ | IE 不支持,需要回退 |
|
||||
| `gap` (flexbox) | 较新 ⚠️ | Safari 14.1+ |
|
||||
| `:has()` | 较新 ⚠️ | Firefox 121+ |
|
||||
| `container queries` | 较新 ⚠️ | 2023 年后的浏览器 |
|
||||
| `@layer` | 较新 ⚠️ | 检查目标浏览器 |
|
||||
|
||||
### 回退策略
|
||||
|
||||
```css
|
||||
/* CSS 变量回退 */
|
||||
.button {
|
||||
background: #3b82f6; /* 回退值 */
|
||||
background: var(--color-primary); /* 现代浏览器 */
|
||||
}
|
||||
|
||||
/* Flexbox gap 回退 */
|
||||
.flex-container {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
/* 旧浏览器回退 */
|
||||
.flex-container > * + * {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
/* Grid 回退 */
|
||||
.grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@supports (display: grid) {
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Autoprefixer 配置
|
||||
|
||||
```javascript
|
||||
// postcss.config.js
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('autoprefixer')({
|
||||
// 根据 browserslist 配置
|
||||
grid: 'autoplace', // 启用 Grid 前缀(IE 支持)
|
||||
flexbox: 'no-2009', // 只用现代 flexbox 语法
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
// package.json
|
||||
{
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead",
|
||||
"not ie 11" // 根据项目需求
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 审查清单
|
||||
|
||||
- [ ] 是否检查了 [Can I Use](https://caniuse.com)?
|
||||
- [ ] 新特性是否有回退方案?
|
||||
- [ ] 是否配置了 Autoprefixer?
|
||||
- [ ] browserslist 是否符合项目要求?
|
||||
- [ ] 是否在目标浏览器中测试?
|
||||
|
||||
---
|
||||
|
||||
## Less / Sass 特定问题
|
||||
|
||||
### 嵌套深度
|
||||
|
||||
```scss
|
||||
/* ❌ 过深嵌套 - 编译后选择器过长 */
|
||||
.page {
|
||||
.container {
|
||||
.content {
|
||||
.article {
|
||||
.title {
|
||||
color: red; // 编译为 .page .container .content .article .title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ✅ 最多 3 层 */
|
||||
.article {
|
||||
&__title {
|
||||
color: red;
|
||||
}
|
||||
|
||||
&__content {
|
||||
p { margin-bottom: 1em; }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Mixin vs Extend vs 变量
|
||||
|
||||
```scss
|
||||
@use 'sass:color';
|
||||
|
||||
/* 变量 - 用于单个值 */
|
||||
$primary-color: #3b82f6;
|
||||
|
||||
/* Mixin - 用于可配置的代码块 */
|
||||
@mixin button-variant($bg, $text) {
|
||||
background: $bg;
|
||||
color: $text;
|
||||
&:hover {
|
||||
// Dart Sass 已弃用全局 darken()/lighten(),改用 color 模块
|
||||
background: color.adjust($bg, $lightness: -10%);
|
||||
// color.scale($bg, $lightness: -10%) 按比例调整,深浅过渡更自然
|
||||
}
|
||||
}
|
||||
|
||||
/* Extend - 用于共享相同样式(谨慎使用) */
|
||||
%visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%); /* clip: rect() 已弃用,改用 clip-path */
|
||||
white-space: nowrap; /* 避免内容被挤成一列后撑开布局 */
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
@extend %visually-hidden;
|
||||
}
|
||||
|
||||
/* ⚠️ @extend 的问题 */
|
||||
// 可能产生意外的选择器组合
|
||||
// 不能在 @media 中使用
|
||||
// 优先使用 mixin
|
||||
```
|
||||
|
||||
### 审查清单
|
||||
|
||||
- [ ] 嵌套是否超过 3 层?
|
||||
- [ ] 是否滥用 @extend?
|
||||
- [ ] Mixin 是否过于复杂?
|
||||
- [ ] 编译后的 CSS 大小是否合理?
|
||||
|
||||
---
|
||||
|
||||
## 快速审查清单
|
||||
|
||||
### 🔴 必须修复
|
||||
|
||||
```markdown
|
||||
□ transition: all
|
||||
□ 动画 width/height/top/left/margin
|
||||
□ 大量 !important
|
||||
□ 硬编码的颜色/间距重复 >3 次
|
||||
□ 选择器嵌套 >4 层
|
||||
```
|
||||
|
||||
### 🟡 建议修复
|
||||
|
||||
```markdown
|
||||
□ 缺少响应式处理
|
||||
□ 使用 Desktop First
|
||||
□ 复杂 box-shadow 被动画
|
||||
□ 缺少浏览器兼容回退
|
||||
□ CSS 变量作用域过大
|
||||
```
|
||||
|
||||
### 🟢 优化建议
|
||||
|
||||
```markdown
|
||||
□ 可以使用 CSS Grid 简化布局
|
||||
□ 可以使用 CSS 变量提取重复值
|
||||
□ 可以使用 @layer 管理优先级
|
||||
□ 可以添加 contain 优化性能
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 工具推荐
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| [Stylelint](https://stylelint.io/) | CSS 代码检查 |
|
||||
| [PurgeCSS](https://purgecss.com/) | 移除未使用 CSS |
|
||||
| [Autoprefixer](https://autoprefixer.github.io/) | 自动添加前缀 |
|
||||
| [CSS Stats](https://cssstats.com/) | 分析 CSS 统计 |
|
||||
| [Can I Use](https://caniuse.com/) | 浏览器兼容性查询 |
|
||||
|
||||
---
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [CSS Performance Optimization - MDN](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Performance/CSS)
|
||||
- [What a CSS Code Review Might Look Like - CSS-Tricks](https://css-tricks.com/what-a-css-code-review-might-look-like/)
|
||||
- [How to Animate Box-Shadow - Tobias Ahlin](https://tobiasahlin.com/blog/how-to-animate-box-shadow/)
|
||||
- [Media Query Fundamentals - MDN](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/CSS_layout/Media_queries)
|
||||
- [Autoprefixer - GitHub](https://github.com/postcss/autoprefixer)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,584 @@
|
||||
# FastAPI Code Review Guide
|
||||
|
||||
> FastAPI code review guide covering dependency injection (`Depends`), Pydantic v2 validation boundaries, async correctness, database session lifecycle and N+1, security, and a test-driven verification workflow that turns the reviewer's in-process test client into a tool for *proving* bugs rather than guessing at them.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Dependency Injection (`Depends`)](#dependency-injection-depends)
|
||||
- [Pydantic v2 Models & Validation](#pydantic-v2-models--validation)
|
||||
- [Async Correctness](#async-correctness)
|
||||
- [Database Sessions & N+1](#database-sessions--n1)
|
||||
- [Security](#security)
|
||||
- [Test-Driven Verification](#test-driven-verification)
|
||||
- [Review Checklist](#review-checklist)
|
||||
- [References](#references)
|
||||
|
||||
---
|
||||
|
||||
## Dependency Injection (`Depends`)
|
||||
|
||||
FastAPI's `Depends` is the seam that keeps routes thin and testable. Most review problems here come from doing real work in the route function instead of behind a dependency.
|
||||
|
||||
### Business logic belongs behind a dependency or service, not in the route
|
||||
|
||||
```python
|
||||
# ❌ Bad — DB access, auth, and business rules all inline in the route
|
||||
@app.get("/orders/{order_id}")
|
||||
async def get_order(order_id: int):
|
||||
conn = await asyncpg.connect(DATABASE_URL) # connection created per request
|
||||
row = await conn.fetchrow("SELECT * FROM orders WHERE id = $1", order_id)
|
||||
await conn.close()
|
||||
if row is None:
|
||||
raise HTTPException(404)
|
||||
return dict(row)
|
||||
|
||||
# ✅ Good — the route declares what it needs; the session is injected and pooled
|
||||
async def get_session() -> AsyncIterator[AsyncSession]:
|
||||
async with SessionLocal() as session:
|
||||
yield session
|
||||
|
||||
@app.get("/orders/{order_id}", response_model=OrderOut)
|
||||
async def get_order(order_id: int, session: AsyncSession = Depends(get_session)):
|
||||
order = await session.get(Order, order_id)
|
||||
if order is None:
|
||||
raise HTTPException(status_code=404, detail="Order not found")
|
||||
return order
|
||||
```
|
||||
|
||||
The injected version is also the version you can override in tests (see [Test-Driven Verification](#test-driven-verification)).
|
||||
|
||||
### `yield` dependencies must clean up, and cleanup runs even on error
|
||||
|
||||
```python
|
||||
# ❌ Bad — no cleanup; the session leaks if the route raises
|
||||
async def get_session() -> AsyncSession:
|
||||
return SessionLocal()
|
||||
|
||||
# ✅ Good — the context manager closes the session on success AND on exception
|
||||
async def get_session() -> AsyncIterator[AsyncSession]:
|
||||
async with SessionLocal() as session:
|
||||
yield session
|
||||
```
|
||||
|
||||
Review point: confirm any `yield` dependency holding a resource (DB session, file handle, lock) releases it through a context manager or `try/finally`, so an exception in the route does not leak it.
|
||||
|
||||
### Don't re-create singletons per request
|
||||
|
||||
```python
|
||||
# ❌ Bad — a new HTTP client (and connection pool) per request
|
||||
@app.get("/proxy")
|
||||
async def proxy(client: httpx.AsyncClient = Depends(lambda: httpx.AsyncClient())):
|
||||
...
|
||||
|
||||
# ✅ Good — one client for the app lifetime, injected by reference
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
app.state.http = httpx.AsyncClient()
|
||||
yield
|
||||
await app.state.http.aclose()
|
||||
|
||||
def get_http(request: Request) -> httpx.AsyncClient:
|
||||
return request.app.state.http
|
||||
```
|
||||
|
||||
### Prefer the `Annotated` form and async dependencies
|
||||
|
||||
Since FastAPI 0.95 the idiomatic way to declare a dependency is `Annotated[T, Depends(...)]`, not the default-value form. It is reusable across routes and plays well with type checkers. Also prefer `async def` dependencies: a sync (`def`) dependency runs in the threadpool, which is wasted overhead for a small non-I/O check.
|
||||
|
||||
```python
|
||||
# ⚠️ Older form — still works, but not the current idiom
|
||||
@app.get("/items")
|
||||
async def list_items(session: AsyncSession = Depends(get_session)): ...
|
||||
|
||||
# ✅ Good — Annotated form; define once, reuse everywhere
|
||||
SessionDep = Annotated[AsyncSession, Depends(get_session)]
|
||||
|
||||
@app.get("/items")
|
||||
async def list_items(session: SessionDep): ...
|
||||
```
|
||||
|
||||
### Use dependencies to validate existence and permissions — they're cached per request
|
||||
|
||||
A dependency is the natural place to answer "does this resource exist and may this caller touch it?" Pydantic validates *shape*; a dependency validates against the database. FastAPI caches each dependency's result within a single request, so chaining small dependencies costs nothing extra and removes duplicated lookups.
|
||||
|
||||
```python
|
||||
# ✅ Good — small dependencies chain; valid_post is resolved once per request
|
||||
async def valid_post(post_id: int, session: SessionDep) -> Post:
|
||||
post = await session.get(Post, post_id)
|
||||
if post is None:
|
||||
raise HTTPException(status_code=404, detail="Post not found")
|
||||
return post
|
||||
|
||||
async def owned_post(post: Annotated[Post, Depends(valid_post)], user: CurrentUser) -> Post:
|
||||
if post.owner_id != user.id:
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
return post
|
||||
|
||||
@app.delete("/posts/{post_id}", status_code=204)
|
||||
async def delete_post(post: Annotated[Post, Depends(owned_post)], session: SessionDep):
|
||||
await session.delete(post) # existence + ownership already enforced
|
||||
await session.commit()
|
||||
```
|
||||
|
||||
This is also the cleanest place to fix the auth-vs-authorization bug from the [Security](#security) section: the ownership check moves into a reusable `owned_post` dependency.
|
||||
|
||||
---
|
||||
|
||||
## Pydantic v2 Models & Validation
|
||||
|
||||
### Separate input and output models; never echo the ORM object directly
|
||||
|
||||
```python
|
||||
# ❌ Bad — response_model is the DB model, so hashed_password leaks to the client
|
||||
@app.post("/users", response_model=UserTable)
|
||||
async def create_user(user: UserTable): # also accepts client-set id, is_admin...
|
||||
...
|
||||
|
||||
# ✅ Good — distinct schemas draw the trust boundary
|
||||
class UserCreate(BaseModel):
|
||||
email: EmailStr
|
||||
password: str
|
||||
|
||||
class UserOut(BaseModel):
|
||||
id: int
|
||||
email: EmailStr
|
||||
model_config = ConfigDict(from_attributes=True) # read from ORM safely
|
||||
|
||||
@app.post("/users", response_model=UserOut, status_code=201)
|
||||
async def create_user(payload: UserCreate, session: AsyncSession = Depends(get_session)):
|
||||
...
|
||||
```
|
||||
|
||||
`response_model` is a filter, not just documentation — fields absent from the output model are stripped from the response. Reusing the DB model as the response is the most common way sensitive fields leak.
|
||||
|
||||
### Use distinct Create and Update schemas
|
||||
|
||||
```python
|
||||
# ❌ Bad — one schema for create and update means every field is required on PATCH
|
||||
class ItemSchema(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
# ✅ Good — update is a partial; create requires the full payload
|
||||
class ItemCreate(BaseModel):
|
||||
name: str
|
||||
price: float = Field(gt=0)
|
||||
|
||||
class ItemUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
price: float | None = Field(default=None, gt=0)
|
||||
```
|
||||
|
||||
### Validate at the boundary, not after the DB write
|
||||
|
||||
```python
|
||||
# ❌ Bad — negative quantity reaches the database before anything checks it
|
||||
@app.post("/cart")
|
||||
async def add_to_cart(item_id: int, quantity: int):
|
||||
await save(item_id, quantity) # quantity = -5 silently accepted
|
||||
|
||||
# ✅ Good — the type system rejects it before the handler body runs
|
||||
class CartLine(BaseModel):
|
||||
item_id: int
|
||||
quantity: int = Field(gt=0)
|
||||
|
||||
@app.post("/cart")
|
||||
async def add_to_cart(line: CartLine):
|
||||
await save(line.item_id, line.quantity)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Async Correctness
|
||||
|
||||
This is the axis on which FastAPI differs most from Django and Flask, and the one most worth a reviewer's attention. FastAPI's throughput comes from a single event loop interleaving many concurrent requests. That model only holds if the loop is **never blocked**: one synchronous call on the loop stalls *every* in-flight request, not just its own. Get this wrong across the codebase and FastAPI does not just lose its edge — it performs *worse* than a sync framework like Flask, because Flask's worker-per-request model has no shared loop to choke. The reviewer's job is to keep work on the loop genuinely non-blocking and to treat every escape hatch as a cost, not a fix.
|
||||
|
||||
### Never call blocking code inside an `async def` route
|
||||
|
||||
```python
|
||||
# ❌ Bad — blocking I/O on the loop freezes ALL concurrent requests, not just this one
|
||||
@app.get("/report")
|
||||
async def report():
|
||||
data = requests.get("https://slow-api.example.com").json() # blocking socket
|
||||
time.sleep(2) # blocks the loop
|
||||
return data
|
||||
|
||||
# ✅ Good — await a native-async client; the loop serves other requests meanwhile
|
||||
@app.get("/report")
|
||||
async def report(client: httpx.AsyncClient = Depends(get_http)):
|
||||
resp = await client.get("https://slow-api.example.com")
|
||||
return resp.json()
|
||||
```
|
||||
|
||||
### Prefer native-async SDKs over sync libraries
|
||||
|
||||
The right fix for blocking I/O is almost always a library that speaks `async` natively — not wrapping a sync one. Reach for the async client first; the threadpool is the last resort, not the default.
|
||||
|
||||
| Sync (blocks the loop) | Native-async replacement |
|
||||
|------------------------|--------------------------|
|
||||
| `requests` | `httpx.AsyncClient`, `aiohttp` |
|
||||
| `psycopg2` (sync) | `asyncpg`, SQLAlchemy async engine |
|
||||
| `redis-py` (sync) | `redis.asyncio` |
|
||||
| `pymongo` | `motor` |
|
||||
| `boto3` | `aioboto3` |
|
||||
|
||||
If you find `asyncio.run(...)`, a new event loop, or a manually started thread *inside* a route, that is a red flag — it's an attempt to bolt sync code onto the loop. `asyncio.run()` inside a running loop raises `RuntimeError` outright; the rest quietly burns the performance you adopted FastAPI for.
|
||||
|
||||
```python
|
||||
# ❌ Bad — spinning up a loop/thread to call an async SDK from a sync context
|
||||
@app.get("/users/{uid}")
|
||||
def get_user(uid: int):
|
||||
return asyncio.run(repo.fetch(uid)) # RuntimeError under the running loop
|
||||
|
||||
# ✅ Good — let the route be async and await the native client directly
|
||||
@app.get("/users/{uid}")
|
||||
async def get_user(uid: int):
|
||||
return await repo.fetch(uid)
|
||||
```
|
||||
|
||||
### The threadpool is a bounded escape hatch, not a default
|
||||
|
||||
A plain `def` route — and `run_in_threadpool(...)` — does not run on the loop; FastAPI runs it in a **bounded** worker threadpool (AnyIO's default cap is 40 threads). For an occasional, genuinely-unavoidable blocking call this is the correct tool:
|
||||
|
||||
```python
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
|
||||
@app.get("/legacy")
|
||||
async def legacy():
|
||||
return await run_in_threadpool(blocking_library_call) # only if no async SDK exists
|
||||
```
|
||||
|
||||
But it does not scale the way the loop does. Route every hot path through the threadpool and, under load, all workers block at once; further requests queue behind the cap and throughput collapses. Spawning your own threads or processes to "add concurrency" makes it worse: once live threads exceed the machine's core count, context-switch and GIL contention degrade performance sharply rather than improving it. The escape hatch is for the rare blocking dependency you cannot replace — not a substitute for choosing async SDKs.
|
||||
|
||||
Review heuristic: a `def` route is acceptable for a low-traffic endpoint with no async equivalent. A high-traffic endpoint doing blocking work in a `def` route (or via `run_in_threadpool`) is a scaling bug — flag it and ask for an async SDK.
|
||||
|
||||
### CPU-bound work belongs in a worker process, not the loop or the threadpool
|
||||
|
||||
Neither the event loop nor the threadpool helps CPU-bound work: under the GIL only one thread runs Python bytecode at a time, so a heavy computation blocks just as badly from a threadpool as from the loop. Offload it to a separate process (Celery, Arq, RQ, or `multiprocessing`).
|
||||
|
||||
```python
|
||||
# ❌ Bad — a CPU-heavy job pins a worker; throughput drops for everyone
|
||||
@app.post("/render")
|
||||
async def render(doc: Doc):
|
||||
return heavy_pdf_render(doc) # seconds of pure CPU on the loop
|
||||
|
||||
# ✅ Good — enqueue to a worker process; return a job handle
|
||||
@app.post("/render", status_code=202)
|
||||
async def render(doc: Doc):
|
||||
job = await queue.enqueue(heavy_pdf_render, doc)
|
||||
return {"job_id": job.id}
|
||||
```
|
||||
|
||||
### Don't fire-and-forget unawaited coroutines
|
||||
|
||||
```python
|
||||
# ❌ Bad — coroutine never awaited; the email is never sent (and no error surfaces)
|
||||
@app.post("/signup")
|
||||
async def signup(user: UserCreate):
|
||||
send_welcome_email(user.email) # returns a coroutine, silently dropped
|
||||
|
||||
# ✅ Good — defer post-response work with BackgroundTasks
|
||||
@app.post("/signup")
|
||||
async def signup(user: UserCreate, tasks: BackgroundTasks):
|
||||
tasks.add_task(send_welcome_email, user.email)
|
||||
```
|
||||
|
||||
`BackgroundTasks` runs in-process and offers no retries or persistence — use it only for short, fire-and-forget work (send an email, log an event). Anything long-running or retry-critical (data processing, payments) belongs in a real task queue (Celery/Arq/RQ).
|
||||
|
||||
---
|
||||
|
||||
## Database Sessions & N+1
|
||||
|
||||
### One session per request, injected — not a global
|
||||
|
||||
```python
|
||||
# ❌ Bad — a module-level session is shared across concurrent requests (not safe)
|
||||
session = SessionLocal()
|
||||
|
||||
# ✅ Good — request-scoped session via dependency (see get_session above)
|
||||
@app.get("/items")
|
||||
async def list_items(session: AsyncSession = Depends(get_session)):
|
||||
...
|
||||
```
|
||||
|
||||
### Eager-load relationships to avoid N+1
|
||||
|
||||
```python
|
||||
# ❌ Bad — one query for orders, then one query per order for its customer
|
||||
orders = (await session.execute(select(Order))).scalars().all()
|
||||
return [{"id": o.id, "customer": o.customer.name} for o in orders] # N+1
|
||||
|
||||
# ✅ Good — a single query with the relationship eager-loaded
|
||||
stmt = select(Order).options(selectinload(Order.customer))
|
||||
orders = (await session.execute(stmt)).scalars().all()
|
||||
return [{"id": o.id, "customer": o.customer.name} for o in orders]
|
||||
```
|
||||
|
||||
With async SQLAlchemy, lazy attribute access outside the session often raises instead of silently querying — but the design issue is the same. Look for relationship access inside a loop without an `options(...)` eager load.
|
||||
|
||||
### Paginate list endpoints
|
||||
|
||||
```python
|
||||
# ❌ Bad — returns every row; degrades as the table grows
|
||||
@app.get("/users")
|
||||
async def list_users(session: AsyncSession = Depends(get_session)):
|
||||
return (await session.execute(select(User))).scalars().all()
|
||||
|
||||
# ✅ Good — bounded page with a sane cap
|
||||
@app.get("/users", response_model=list[UserOut])
|
||||
async def list_users(
|
||||
session: AsyncSession = Depends(get_session),
|
||||
limit: int = Query(default=50, le=100),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
):
|
||||
stmt = select(User).limit(limit).offset(offset)
|
||||
return (await session.execute(stmt)).scalars().all()
|
||||
```
|
||||
|
||||
### Aggregate and join in SQL, not in Python
|
||||
|
||||
If a handler pulls rows into memory and then loops to group, count, or join them, the database is being used as dumb storage. Push the work down — the database does set operations far faster, and you transfer less data.
|
||||
|
||||
```python
|
||||
# ❌ Bad — fetch every order, then tally per customer in Python
|
||||
orders = (await session.execute(select(Order))).scalars().all()
|
||||
totals: dict[int, float] = {}
|
||||
for o in orders:
|
||||
totals[o.customer_id] = totals.get(o.customer_id, 0) + o.amount
|
||||
|
||||
# ✅ Good — let the database group and sum
|
||||
stmt = select(Order.customer_id, func.sum(Order.amount)).group_by(Order.customer_id)
|
||||
totals = dict((await session.execute(stmt)).all())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
### A declared auth dependency is not an enforced authorization check
|
||||
|
||||
This is the highest-value thing to look for. `Depends(get_current_user)` proves *who* the caller is — it does **not** prove they may touch *this* resource.
|
||||
|
||||
```python
|
||||
# ❌ Bad — any authenticated user can delete any other user's document
|
||||
@app.delete("/documents/{doc_id}")
|
||||
async def delete_document(
|
||||
doc_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
doc = await session.get(Document, doc_id)
|
||||
await session.delete(doc) # never checks doc.owner_id == user.id
|
||||
await session.commit()
|
||||
|
||||
# ✅ Good — ownership is verified before the mutation
|
||||
@app.delete("/documents/{doc_id}", status_code=204)
|
||||
async def delete_document(
|
||||
doc_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
doc = await session.get(Document, doc_id)
|
||||
if doc is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
if doc.owner_id != user.id:
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
await session.delete(doc)
|
||||
await session.commit()
|
||||
```
|
||||
|
||||
The [Test-Driven Verification](#test-driven-verification) section reproduces exactly this bug with a failing test.
|
||||
|
||||
### Parameterize SQL; never f-string user input
|
||||
|
||||
```python
|
||||
# ❌ Bad — SQL injection
|
||||
await session.execute(text(f"SELECT * FROM users WHERE email = '{email}'"))
|
||||
|
||||
# ✅ Good — bound parameter
|
||||
await session.execute(text("SELECT * FROM users WHERE email = :email"), {"email": email})
|
||||
```
|
||||
|
||||
### Don't widen CORS to credentials + wildcard
|
||||
|
||||
```python
|
||||
# ❌ Bad — wildcard origin together with credentials is rejected by browsers and unsafe
|
||||
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True)
|
||||
|
||||
# ✅ Good — enumerate trusted origins when credentials are allowed
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["https://app.example.com"],
|
||||
allow_credentials=True,
|
||||
)
|
||||
```
|
||||
|
||||
Also check: secrets read from config/env (not hard-coded), `HTTPException` details that don't leak internals (stack traces, SQL), and rate limiting on auth endpoints.
|
||||
|
||||
---
|
||||
|
||||
## Test-Driven Verification
|
||||
|
||||
> Inspired by the test-driven development discipline: *if you didn't watch the test fail, you don't know it tests the right thing.* This matters even more for a coding agent than for a human reviewer. An agent's reading and reasoning are fallible — it can misread control flow, hallucinate a guarantee that isn't there, or rationalize a comfortable conclusion — so a prose verdict like "this looks safe" carries little weight on its own. An executable test is the one piece of **objective ground truth** the agent fully controls: it either passes or it doesn't, regardless of how confident the reasoning felt. That is what makes tests the agent's anchor of confidence. Reviewing the same way the discipline writes code — reproduce, don't assert — turns a hunch into proof.
|
||||
|
||||
A natural-language review comment ("this might let users delete each other's data") is exactly that kind of fallible hypothesis. FastAPI makes the ground truth cheap to obtain: an in-process client (`httpx.AsyncClient` over `ASGITransport`) runs the whole app, and `app.dependency_overrides` swaps out auth and the database without patching internals. So instead of trusting its own read of the code, the agent settles the question by reproduction.
|
||||
|
||||
### Reproduce a suspected bug with a failing test (Verify RED)
|
||||
|
||||
Suppose the reviewer suspects the `DELETE /documents/{doc_id}` route above never checks ownership. Write the test that asserts the *secure* behavior, then run it and **watch it fail** — the failure is the proof.
|
||||
|
||||
```python
|
||||
# test_document_authorization.py
|
||||
import pytest
|
||||
from httpx import AsyncClient, ASGITransport
|
||||
from fastapi import Header
|
||||
from app.main import app
|
||||
from app.deps import get_current_user, get_session
|
||||
|
||||
# Two users; the override picks one based on a test header.
|
||||
USERS = {"alice": User(id=1, email="alice@example.com"),
|
||||
"bob": User(id=2, email="bob@example.com")}
|
||||
|
||||
def fake_current_user(x_test_user: str = Header(default="alice")) -> User:
|
||||
return USERS[x_test_user]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_cannot_delete_another_users_document(session): # async fixture
|
||||
# Arrange: a document owned by Alice (id=1)
|
||||
session.add(Document(id=10, owner_id=1, title="Alice's doc"))
|
||||
await session.commit()
|
||||
|
||||
app.dependency_overrides[get_current_user] = fake_current_user
|
||||
app.dependency_overrides[get_session] = lambda: session
|
||||
|
||||
# Act: Bob tries to delete Alice's document
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
resp = await client.delete("/documents/10", headers={"X-Test-User": "bob"})
|
||||
|
||||
# Assert the SECURE behavior we expect
|
||||
assert resp.status_code == 403
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
```
|
||||
|
||||
Run it against the unfixed code and confirm the failure is the bug, not a typo:
|
||||
|
||||
```bash
|
||||
$ pytest test_document_authorization.py
|
||||
FAILED assert 204 == 403
|
||||
# ^ the endpoint deleted Alice's document for Bob — vulnerability confirmed
|
||||
```
|
||||
|
||||
A failure of `204 == 403` (not an import error, not a 404) is what makes the finding credible: the route returned success for an action that should have been forbidden. Now the fix from the [Security](#security) section turns it green:
|
||||
|
||||
```bash
|
||||
$ pytest test_document_authorization.py
|
||||
PASSED
|
||||
```
|
||||
|
||||
Attach this test to the review. It documents the vulnerability, proves the fix, and guards against regression — far stronger than "consider checking ownership here."
|
||||
|
||||
### Prefer `dependency_overrides` over `patch`/`mock`
|
||||
|
||||
FastAPI's DI is the seam the TDD discipline asks for: when something is hard to test without mocking everything, that usually signals coupling — and `Depends` already gives you the injection point, so you rarely need `unittest.mock.patch`.
|
||||
|
||||
```python
|
||||
# ❌ Bad — patching internals: brittle, couples the test to import paths
|
||||
@patch("app.routes.orders.asyncpg.connect")
|
||||
def test_get_order(mock_connect): ...
|
||||
|
||||
# ✅ Good — override the dependency with a real in-memory fake
|
||||
app.dependency_overrides[get_session] = lambda: in_memory_session
|
||||
app.dependency_overrides[get_current_user] = lambda: test_user
|
||||
```
|
||||
|
||||
Always reset overrides between tests (`app.dependency_overrides.clear()` in a fixture teardown) so state doesn't leak across tests.
|
||||
|
||||
The reproduction above uses `httpx.AsyncClient` over `ASGITransport` with `@pytest.mark.asyncio` — the community convention for an async app, so the suite shares the app's event loop and you avoid loop-mismatch errors later. The synchronous `TestClient` is simpler and fine for a fully sync app, but standardizing on the async client from the start saves a painful migration once any route or fixture becomes async.
|
||||
|
||||
### Critique the PR's own tests, not just its source
|
||||
|
||||
A PR that ships tests is not automatically safe. Apply these checks to the *tests* in the diff:
|
||||
|
||||
```python
|
||||
# ❌ Bad — happy-path only. Proves the route works when everything is correct,
|
||||
# says nothing about the validation and authorization paths.
|
||||
def test_create_item():
|
||||
resp = client.post("/items", json={"name": "x", "price": 5})
|
||||
assert resp.status_code == 201
|
||||
|
||||
# ✅ Good — the boundary and failure paths are where bugs live
|
||||
def test_create_item_rejects_negative_price():
|
||||
resp = client.post("/items", json={"name": "x", "price": -5})
|
||||
assert resp.status_code == 422
|
||||
|
||||
def test_create_item_requires_authentication():
|
||||
resp = client_without_auth.post("/items", json={"name": "x", "price": 5})
|
||||
assert resp.status_code == 401
|
||||
```
|
||||
|
||||
Review questions for the test suite:
|
||||
|
||||
- **Does it test behavior, or the mock?** An assertion that only confirms a mock was called proves the test's own setup, not the endpoint.
|
||||
- **Are the failure paths covered?** 401/403/404/422 — not just 200/201. Bugs cluster at the boundaries.
|
||||
- **Is the mock complete?** A partial mock of an external API response that omits fields the handler reads passes in the test and fails in production.
|
||||
- **Were the tests written after the fact?** Tests added alongside an implementation and passing on the first run never demonstrated that they can fail — and so prove little. A test that reproduces the bug (fails first, then passes) is worth more than one that was green from birth.
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### Dependency Injection
|
||||
|
||||
- [ ] Routes stay thin — DB access and business rules live behind `Depends`/services
|
||||
- [ ] `yield` dependencies release resources via context manager or `try/finally`
|
||||
- [ ] Singletons (HTTP clients, pools) created once in `lifespan`, not per request
|
||||
- [ ] `Annotated[T, Depends(...)]` form used; dependencies are `async def` unless they do blocking I/O
|
||||
- [ ] Existence/permission checks live in (cached) dependencies, not copy-pasted into routes
|
||||
- [ ] Dependencies are overridable in tests (no resources created inline in the route)
|
||||
|
||||
### Validation
|
||||
|
||||
- [ ] Input and output use distinct Pydantic models; ORM objects are not the `response_model`
|
||||
- [ ] `response_model` set so sensitive fields can't leak
|
||||
- [ ] Separate Create vs Update schemas (update is partial)
|
||||
- [ ] Constraints (`gt`, `le`, `EmailStr`, ...) enforced at the boundary, before the DB write
|
||||
|
||||
### Async
|
||||
|
||||
- [ ] No blocking calls (`requests`, `time.sleep`, blocking DB drivers) inside `async def`
|
||||
- [ ] Native-async SDKs preferred (`httpx`, `asyncpg`, `redis.asyncio`, ...) over sync ones
|
||||
- [ ] No `asyncio.run`/manual event loops/manual threads inside routes
|
||||
- [ ] `run_in_threadpool`/`def` routes used only as a last resort, not on hot paths
|
||||
- [ ] CPU-bound work offloaded to a worker process (Celery/Arq/RQ), not the loop or threadpool
|
||||
- [ ] No unawaited coroutines; `BackgroundTasks` only for short fire-and-forget work
|
||||
|
||||
### Database
|
||||
|
||||
- [ ] One request-scoped session via dependency; no module-level shared session
|
||||
- [ ] Relationships eager-loaded (`selectinload`/`joinedload`) where accessed in a loop
|
||||
- [ ] Joins/aggregations done in SQL, not by looping in Python
|
||||
- [ ] List endpoints are paginated with a capped `limit`
|
||||
|
||||
### Security
|
||||
|
||||
- [ ] Authentication dependency is backed by an explicit **authorization** check (ownership/role)
|
||||
- [ ] All SQL parameterized; no f-string interpolation of user input
|
||||
- [ ] CORS does not combine `allow_origins=["*"]` with `allow_credentials=True`
|
||||
- [ ] Secrets come from config/env; error responses don't leak internals
|
||||
|
||||
### Tests
|
||||
|
||||
- [ ] Suspected bugs reproduced with a failing test (`TestClient`/`AsyncClient`) before being claimed
|
||||
- [ ] `dependency_overrides` used instead of patching internals; overrides reset between tests
|
||||
- [ ] Failure paths covered (401/403/404/422), not just the happy path
|
||||
- [ ] Mocks of external responses are complete, not partial
|
||||
- [ ] New tests demonstrate they can fail (reproduce-then-fix), not green from birth
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [FastAPI official documentation](https://fastapi.tiangolo.com/) — async, dependencies, testing
|
||||
- [zhanymkanov/fastapi-best-practices](https://github.com/zhanymkanov/fastapi-best-practices) — production conventions (async routes, dependency caching, project structure)
|
||||
@@ -0,0 +1,989 @@
|
||||
# Go 代码审查指南
|
||||
|
||||
基于 Go 官方指南、Effective Go 和社区最佳实践的代码审查清单。
|
||||
|
||||
## 快速审查清单
|
||||
|
||||
### 必查项
|
||||
- [ ] 错误是否正确处理(不忽略、有上下文)
|
||||
- [ ] goroutine 是否有退出机制(避免泄漏)
|
||||
- [ ] context 是否正确传递和取消
|
||||
- [ ] 接收器类型选择是否合理(值/指针)
|
||||
- [ ] 是否使用 `gofmt` 格式化代码
|
||||
|
||||
### 高频问题
|
||||
- [ ] 循环变量捕获问题(Go < 1.22)
|
||||
- [ ] nil 检查是否完整
|
||||
- [ ] map 是否初始化后使用
|
||||
- [ ] defer 在循环中的使用
|
||||
- [ ] 变量遮蔽(shadowing)
|
||||
|
||||
---
|
||||
|
||||
## 1. 错误处理
|
||||
|
||||
### 1.1 永远不要忽略错误
|
||||
|
||||
```go
|
||||
// ❌ 错误:忽略错误
|
||||
result, _ := SomeFunction()
|
||||
|
||||
// ✅ 正确:处理错误
|
||||
result, err := SomeFunction()
|
||||
if err != nil {
|
||||
return fmt.Errorf("some function failed: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 错误包装与上下文
|
||||
|
||||
```go
|
||||
// ❌ 错误:丢失上下文
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ❌ 错误:使用 %v 丢失错误链
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed: %v", err)
|
||||
}
|
||||
|
||||
// ✅ 正确:使用 %w 保留错误链
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to process user %d: %w", userID, err)
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 使用 errors.Is 和 errors.As
|
||||
|
||||
```go
|
||||
// ❌ 错误:直接比较(无法处理包装错误)
|
||||
if err == sql.ErrNoRows {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ✅ 正确:使用 errors.Is(支持错误链)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
// ✅ 正确:使用 errors.As 提取特定类型
|
||||
var pathErr *os.PathError
|
||||
if errors.As(err, &pathErr) {
|
||||
log.Printf("path error: %s", pathErr.Path)
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 自定义错误类型
|
||||
|
||||
```go
|
||||
// ✅ 推荐:定义 sentinel 错误
|
||||
var (
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
)
|
||||
|
||||
// ✅ 推荐:带上下文的自定义错误
|
||||
type ValidationError struct {
|
||||
Field string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *ValidationError) Error() string {
|
||||
return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message)
|
||||
}
|
||||
```
|
||||
|
||||
### 1.5 错误处理只做一次
|
||||
|
||||
```go
|
||||
// ❌ 错误:既记录又返回(重复处理)
|
||||
if err != nil {
|
||||
log.Printf("error: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// ✅ 正确:只返回,让调用者决定
|
||||
if err != nil {
|
||||
return fmt.Errorf("operation failed: %w", err)
|
||||
}
|
||||
|
||||
// ✅ 或者:只记录并处理(不返回)
|
||||
if err != nil {
|
||||
log.Printf("non-critical error: %v", err)
|
||||
// 继续执行备用逻辑
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 并发与 Goroutine
|
||||
|
||||
### 2.1 避免 Goroutine 泄漏
|
||||
|
||||
```go
|
||||
// ❌ 错误:goroutine 永远无法退出
|
||||
func bad() {
|
||||
ch := make(chan int)
|
||||
go func() {
|
||||
val := <-ch // 永远阻塞,无人发送
|
||||
fmt.Println(val)
|
||||
}()
|
||||
// 函数返回,goroutine 泄漏
|
||||
}
|
||||
|
||||
// ✅ 正确:使用 context 或 done channel
|
||||
func good(ctx context.Context) {
|
||||
ch := make(chan int)
|
||||
go func() {
|
||||
select {
|
||||
case val := <-ch:
|
||||
fmt.Println(val)
|
||||
case <-ctx.Done():
|
||||
return // 优雅退出
|
||||
}
|
||||
}()
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Channel 使用规范
|
||||
|
||||
```go
|
||||
// ❌ 错误:向 nil channel 发送(永久阻塞)
|
||||
var ch chan int
|
||||
ch <- 1 // 永久阻塞
|
||||
|
||||
// ❌ 错误:向已关闭的 channel 发送(panic)
|
||||
close(ch)
|
||||
ch <- 1 // panic!
|
||||
|
||||
// ✅ 正确:发送方关闭 channel
|
||||
func producer(ch chan<- int) {
|
||||
defer close(ch) // 发送方负责关闭
|
||||
for i := 0; i < 10; i++ {
|
||||
ch <- i
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 正确:接收方检测关闭
|
||||
for val := range ch {
|
||||
process(val)
|
||||
}
|
||||
// 或者
|
||||
val, ok := <-ch
|
||||
if !ok {
|
||||
// channel 已关闭
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 使用 sync.WaitGroup
|
||||
|
||||
```go
|
||||
// ❌ 错误:Add 在 goroutine 内部
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 10; i++ {
|
||||
go func() {
|
||||
wg.Add(1) // 竞态条件!
|
||||
defer wg.Done()
|
||||
work()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// ✅ 正确:Add 在 goroutine 启动前
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
work()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
```
|
||||
|
||||
### 2.4 避免在循环中捕获变量(Go < 1.22)
|
||||
|
||||
```go
|
||||
// ❌ 错误(Go < 1.22):捕获循环变量
|
||||
for _, item := range items {
|
||||
go func() {
|
||||
process(item) // 所有 goroutine 可能使用同一个 item
|
||||
}()
|
||||
}
|
||||
|
||||
// ✅ 正确:传递参数
|
||||
for _, item := range items {
|
||||
go func(it Item) {
|
||||
process(it)
|
||||
}(item)
|
||||
}
|
||||
|
||||
// ✅ Go 1.22+:默认行为已修复,每次迭代创建新变量
|
||||
```
|
||||
|
||||
### 2.5 Worker Pool 模式
|
||||
|
||||
```go
|
||||
// ✅ 推荐:限制并发数量
|
||||
func processWithWorkerPool(ctx context.Context, items []Item, workers int) error {
|
||||
jobs := make(chan Item, len(items))
|
||||
results := make(chan error, len(items))
|
||||
|
||||
// 启动 worker
|
||||
for w := 0; w < workers; w++ {
|
||||
go func() {
|
||||
for item := range jobs {
|
||||
results <- process(item)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送任务
|
||||
for _, item := range items {
|
||||
jobs <- item
|
||||
}
|
||||
close(jobs)
|
||||
|
||||
// 收集结果
|
||||
for range items {
|
||||
if err := <-results; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Context 使用
|
||||
|
||||
### 3.1 Context 作为第一个参数
|
||||
|
||||
```go
|
||||
// ❌ 错误:context 不是第一个参数
|
||||
func Process(data []byte, ctx context.Context) error
|
||||
|
||||
// ❌ 错误:context 存储在 struct 中
|
||||
type Service struct {
|
||||
ctx context.Context // 不要这样做!
|
||||
}
|
||||
|
||||
// ✅ 正确:context 作为第一个参数,命名为 ctx
|
||||
func Process(ctx context.Context, data []byte) error
|
||||
```
|
||||
|
||||
### 3.2 传播而非创建新的根 Context
|
||||
|
||||
```go
|
||||
// ❌ 错误:在调用链中创建新的根 context
|
||||
func middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.Background() // 丢失了请求的 context!
|
||||
process(ctx)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// ✅ 正确:从请求中获取并传播
|
||||
func middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, key, value)
|
||||
process(ctx)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 始终调用 cancel 函数
|
||||
|
||||
```go
|
||||
// ❌ 错误:未调用 cancel
|
||||
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
|
||||
// 缺少 cancel() 调用,可能资源泄漏
|
||||
|
||||
// ✅ 正确:使用 defer 确保调用
|
||||
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
|
||||
defer cancel() // 即使超时也要调用
|
||||
```
|
||||
|
||||
### 3.4 响应 Context 取消
|
||||
|
||||
```go
|
||||
// ✅ 推荐:在长时间操作中检查 context
|
||||
func LongRunningTask(ctx context.Context) error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err() // 返回 context.Canceled 或 context.DeadlineExceeded
|
||||
default:
|
||||
// 执行一小部分工作
|
||||
if err := doChunk(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 区分取消原因
|
||||
|
||||
```go
|
||||
// ✅ 根据 ctx.Err() 区分取消原因
|
||||
if err := ctx.Err(); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, context.Canceled):
|
||||
log.Println("operation was canceled")
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
log.Println("operation timed out")
|
||||
}
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 接口设计
|
||||
|
||||
### 4.1 接受接口,返回结构体
|
||||
|
||||
```go
|
||||
// ❌ 不推荐:接受具体类型
|
||||
func SaveUser(db *sql.DB, user User) error
|
||||
|
||||
// ✅ 推荐:接受接口(解耦、易测试)
|
||||
type UserStore interface {
|
||||
Save(ctx context.Context, user User) error
|
||||
}
|
||||
|
||||
func SaveUser(store UserStore, user User) error
|
||||
|
||||
// ❌ 不推荐:返回接口
|
||||
func NewUserService() UserServiceInterface
|
||||
|
||||
// ✅ 推荐:返回具体类型
|
||||
func NewUserService(store UserStore) *UserService
|
||||
```
|
||||
|
||||
### 4.2 在消费者处定义接口
|
||||
|
||||
```go
|
||||
// ❌ 不推荐:在实现包中定义接口
|
||||
// package database
|
||||
type Database interface {
|
||||
Query(ctx context.Context, query string) ([]Row, error)
|
||||
// ... 20 个方法
|
||||
}
|
||||
|
||||
// ✅ 推荐:在消费者包中定义所需的最小接口
|
||||
// package userservice
|
||||
type UserQuerier interface {
|
||||
QueryUsers(ctx context.Context, filter Filter) ([]User, error)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 保持接口小而专注
|
||||
|
||||
```go
|
||||
// ❌ 不推荐:大而全的接口
|
||||
type Repository interface {
|
||||
GetUser(id int) (*User, error)
|
||||
CreateUser(u *User) error
|
||||
UpdateUser(u *User) error
|
||||
DeleteUser(id int) error
|
||||
GetOrder(id int) (*Order, error)
|
||||
CreateOrder(o *Order) error
|
||||
// ... 更多方法
|
||||
}
|
||||
|
||||
// ✅ 推荐:小而专注的接口
|
||||
type UserReader interface {
|
||||
GetUser(ctx context.Context, id int) (*User, error)
|
||||
}
|
||||
|
||||
type UserWriter interface {
|
||||
CreateUser(ctx context.Context, u *User) error
|
||||
UpdateUser(ctx context.Context, u *User) error
|
||||
}
|
||||
|
||||
// 组合接口
|
||||
type UserRepository interface {
|
||||
UserReader
|
||||
UserWriter
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 避免空接口滥用
|
||||
|
||||
```go
|
||||
// ❌ 不推荐:过度使用 interface{}
|
||||
func Process(data interface{}) interface{}
|
||||
|
||||
// ✅ 推荐:使用泛型(Go 1.18+)
|
||||
func Process[T any](data T) T
|
||||
|
||||
// ✅ 推荐:定义具体接口
|
||||
type Processor interface {
|
||||
Process() Result
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 接收器类型选择
|
||||
|
||||
### 5.1 使用指针接收器的情况
|
||||
|
||||
```go
|
||||
// ✅ 需要修改接收器时
|
||||
func (u *User) SetName(name string) {
|
||||
u.Name = name
|
||||
}
|
||||
|
||||
// ✅ 接收器包含 sync.Mutex 等同步原语
|
||||
type SafeCounter struct {
|
||||
mu sync.Mutex
|
||||
count int
|
||||
}
|
||||
|
||||
func (c *SafeCounter) Inc() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.count++
|
||||
}
|
||||
|
||||
// ✅ 接收器是大型结构体(避免复制开销)
|
||||
type LargeStruct struct {
|
||||
Data [1024]byte
|
||||
// ...
|
||||
}
|
||||
|
||||
func (l *LargeStruct) Process() { /* ... */ }
|
||||
```
|
||||
|
||||
### 5.2 使用值接收器的情况
|
||||
|
||||
```go
|
||||
// ✅ 接收器是小型不可变结构体
|
||||
type Point struct {
|
||||
X, Y float64
|
||||
}
|
||||
|
||||
func (p Point) Distance(other Point) float64 {
|
||||
return math.Sqrt(math.Pow(p.X-other.X, 2) + math.Pow(p.Y-other.Y, 2))
|
||||
}
|
||||
|
||||
// ✅ 接收器是基本类型的别名
|
||||
type Counter int
|
||||
|
||||
func (c Counter) String() string {
|
||||
return fmt.Sprintf("%d", c)
|
||||
}
|
||||
|
||||
// ✅ 接收器是 map、func、chan(本身是引用类型)
|
||||
type StringSet map[string]struct{}
|
||||
|
||||
func (s StringSet) Contains(key string) bool {
|
||||
_, ok := s[key]
|
||||
return ok
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 一致性原则
|
||||
|
||||
```go
|
||||
// ❌ 不推荐:混合使用接收器类型
|
||||
func (u User) GetName() string // 值接收器
|
||||
func (u *User) SetName(n string) // 指针接收器
|
||||
|
||||
// ✅ 推荐:如果有任何方法需要指针接收器,全部使用指针
|
||||
func (u *User) GetName() string { return u.Name }
|
||||
func (u *User) SetName(n string) { u.Name = n }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 性能优化
|
||||
|
||||
### 6.1 预分配 Slice
|
||||
|
||||
```go
|
||||
// ❌ 不推荐:动态增长
|
||||
var result []int
|
||||
for i := 0; i < 10000; i++ {
|
||||
result = append(result, i) // 多次分配和复制
|
||||
}
|
||||
|
||||
// ✅ 推荐:预分配已知大小
|
||||
result := make([]int, 0, 10000)
|
||||
for i := 0; i < 10000; i++ {
|
||||
result = append(result, i)
|
||||
}
|
||||
|
||||
// ✅ 或者直接初始化
|
||||
result := make([]int, 10000)
|
||||
for i := 0; i < 10000; i++ {
|
||||
result[i] = i
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 避免不必要的堆分配
|
||||
|
||||
```go
|
||||
// ❌ 可能逃逸到堆
|
||||
func NewUser() *User {
|
||||
return &User{} // 逃逸到堆
|
||||
}
|
||||
|
||||
// ✅ 考虑返回值(如果适用)
|
||||
func NewUser() User {
|
||||
return User{} // 可能在栈上分配
|
||||
}
|
||||
|
||||
// 检查逃逸分析
|
||||
// go build -gcflags '-m -m' ./...
|
||||
```
|
||||
|
||||
### 6.3 使用 sync.Pool 复用对象
|
||||
|
||||
```go
|
||||
// ✅ 推荐:高频创建/销毁的对象使用 sync.Pool
|
||||
var bufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
|
||||
func ProcessData(data []byte) string {
|
||||
buf := bufferPool.Get().(*bytes.Buffer)
|
||||
defer func() {
|
||||
buf.Reset()
|
||||
bufferPool.Put(buf)
|
||||
}()
|
||||
|
||||
buf.Write(data)
|
||||
return buf.String()
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 字符串拼接优化
|
||||
|
||||
```go
|
||||
// ❌ 不推荐:循环中使用 + 拼接
|
||||
var result string
|
||||
for _, s := range strings {
|
||||
result += s // 每次创建新字符串
|
||||
}
|
||||
|
||||
// ✅ 推荐:使用 strings.Builder
|
||||
var builder strings.Builder
|
||||
for _, s := range strings {
|
||||
builder.WriteString(s)
|
||||
}
|
||||
result := builder.String()
|
||||
|
||||
// ✅ 或者使用 strings.Join
|
||||
result := strings.Join(strings, "")
|
||||
```
|
||||
|
||||
### 6.5 避免 interface{} 转换开销
|
||||
|
||||
```go
|
||||
// ❌ 热路径中使用 interface{}
|
||||
func process(data interface{}) {
|
||||
switch v := data.(type) { // 类型断言有开销
|
||||
case int:
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 热路径中使用泛型或具体类型
|
||||
func process[T int | int64 | float64](data T) {
|
||||
// 编译时确定类型,无运行时开销
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 测试
|
||||
|
||||
### 7.1 表驱动测试
|
||||
|
||||
```go
|
||||
// ✅ 推荐:表驱动测试
|
||||
func TestAdd(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
a, b int
|
||||
expected int
|
||||
}{
|
||||
{"positive numbers", 1, 2, 3},
|
||||
{"with zero", 0, 5, 5},
|
||||
{"negative numbers", -1, -2, -3},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := Add(tt.a, tt.b)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Add(%d, %d) = %d; want %d",
|
||||
tt.a, tt.b, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 并行测试
|
||||
|
||||
```go
|
||||
// ✅ 推荐:独立测试用例并行执行
|
||||
func TestParallel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
}{
|
||||
{"test1", "input1"},
|
||||
{"test2", "input2"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt // Go < 1.22 需要复制
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel() // 标记为可并行
|
||||
result := Process(tt.input)
|
||||
// assertions...
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 使用接口进行 Mock
|
||||
|
||||
```go
|
||||
// ✅ 定义接口以便测试
|
||||
type EmailSender interface {
|
||||
Send(to, subject, body string) error
|
||||
}
|
||||
|
||||
// 生产实现
|
||||
type SMTPSender struct { /* ... */ }
|
||||
|
||||
// 测试 Mock
|
||||
type MockEmailSender struct {
|
||||
SendFunc func(to, subject, body string) error
|
||||
}
|
||||
|
||||
func (m *MockEmailSender) Send(to, subject, body string) error {
|
||||
return m.SendFunc(to, subject, body)
|
||||
}
|
||||
|
||||
func TestUserRegistration(t *testing.T) {
|
||||
mock := &MockEmailSender{
|
||||
SendFunc: func(to, subject, body string) error {
|
||||
if to != "test@example.com" {
|
||||
t.Errorf("unexpected recipient: %s", to)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
service := NewUserService(mock)
|
||||
// test...
|
||||
}
|
||||
```
|
||||
|
||||
### 7.4 测试辅助函数
|
||||
|
||||
```go
|
||||
// ✅ 使用 t.Helper() 标记辅助函数
|
||||
func assertEqual(t *testing.T, got, want interface{}) {
|
||||
t.Helper() // 错误报告时显示调用者位置
|
||||
if got != want {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 使用 t.Cleanup() 清理资源
|
||||
func TestWithTempFile(t *testing.T) {
|
||||
f, err := os.CreateTemp("", "test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
os.Remove(f.Name())
|
||||
})
|
||||
// test...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 常见陷阱
|
||||
|
||||
### 8.1 Nil Slice vs Empty Slice
|
||||
|
||||
```go
|
||||
var nilSlice []int // nil, len=0, cap=0
|
||||
emptySlice := []int{} // not nil, len=0, cap=0
|
||||
made := make([]int, 0) // not nil, len=0, cap=0
|
||||
|
||||
// ✅ JSON 编码差异
|
||||
json.Marshal(nilSlice) // null
|
||||
json.Marshal(emptySlice) // []
|
||||
|
||||
// ✅ 推荐:需要空数组 JSON 时显式初始化
|
||||
if slice == nil {
|
||||
slice = []int{}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 Map 初始化
|
||||
|
||||
```go
|
||||
// ❌ 错误:未初始化的 map
|
||||
var m map[string]int
|
||||
m["key"] = 1 // panic: assignment to entry in nil map
|
||||
|
||||
// ✅ 正确:使用 make 初始化
|
||||
m := make(map[string]int)
|
||||
m["key"] = 1
|
||||
|
||||
// ✅ 或者使用字面量
|
||||
m := map[string]int{}
|
||||
```
|
||||
|
||||
### 8.3 Defer 在循环中
|
||||
|
||||
```go
|
||||
// ❌ 潜在问题:defer 在函数结束时才执行
|
||||
func processFiles(files []string) error {
|
||||
for _, file := range files {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close() // 所有文件在函数结束时才关闭!
|
||||
// process...
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ✅ 正确:使用闭包或提取函数
|
||||
func processFiles(files []string) error {
|
||||
for _, file := range files {
|
||||
if err := processFile(file); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func processFile(file string) error {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
// process...
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 8.4 Slice 底层数组共享
|
||||
|
||||
```go
|
||||
// ❌ 潜在问题:切片共享底层数组
|
||||
original := []int{1, 2, 3, 4, 5}
|
||||
slice := original[1:3] // [2, 3]
|
||||
slice[0] = 100 // 修改了 original!
|
||||
// original 变成 [1, 100, 3, 4, 5]
|
||||
|
||||
// ✅ 正确:需要独立副本时显式复制
|
||||
slice := make([]int, 2)
|
||||
copy(slice, original[1:3])
|
||||
slice[0] = 100 // 不影响 original
|
||||
```
|
||||
|
||||
### 8.5 字符串子串内存泄漏
|
||||
|
||||
```go
|
||||
// ❌ 潜在问题:子串持有整个底层数组
|
||||
func getPrefix(s string) string {
|
||||
return s[:10] // 仍引用整个 s 的底层数组
|
||||
}
|
||||
|
||||
// ✅ 正确:创建独立副本(Go 1.18+)
|
||||
func getPrefix(s string) string {
|
||||
return strings.Clone(s[:10])
|
||||
}
|
||||
|
||||
// ✅ Go 1.18 之前
|
||||
func getPrefix(s string) string {
|
||||
return string([]byte(s[:10]))
|
||||
}
|
||||
```
|
||||
|
||||
### 8.6 Interface Nil 陷阱
|
||||
|
||||
```go
|
||||
// ❌ 陷阱:interface 的 nil 判断
|
||||
type MyError struct{}
|
||||
func (e *MyError) Error() string { return "error" }
|
||||
|
||||
func returnsError() error {
|
||||
var e *MyError = nil
|
||||
return e // 返回的 error 不是 nil!
|
||||
}
|
||||
|
||||
func main() {
|
||||
err := returnsError()
|
||||
if err != nil { // true! interface{type: *MyError, value: nil}
|
||||
fmt.Println("error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 正确:显式返回 nil
|
||||
func returnsError() error {
|
||||
var e *MyError = nil
|
||||
if e == nil {
|
||||
return nil // 显式返回 nil
|
||||
}
|
||||
return e
|
||||
}
|
||||
```
|
||||
|
||||
### 8.7 Time 比较
|
||||
|
||||
```go
|
||||
// ❌ 不推荐:直接使用 == 比较 time.Time
|
||||
if t1 == t2 { // 可能因为单调时钟差异而失败
|
||||
// ...
|
||||
}
|
||||
|
||||
// ✅ 推荐:使用 Equal 方法
|
||||
if t1.Equal(t2) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ✅ 比较时间范围
|
||||
if t1.Before(t2) || t1.After(t2) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 代码组织
|
||||
|
||||
### 9.1 包命名
|
||||
|
||||
```go
|
||||
// ❌ 不推荐
|
||||
package common // 过于宽泛
|
||||
package utils // 过于宽泛
|
||||
package helpers // 过于宽泛
|
||||
package models // 按类型分组
|
||||
|
||||
// ✅ 推荐:按功能命名
|
||||
package user // 用户相关功能
|
||||
package order // 订单相关功能
|
||||
package postgres // PostgreSQL 实现
|
||||
```
|
||||
|
||||
### 9.2 避免循环依赖
|
||||
|
||||
```go
|
||||
// ❌ 循环依赖
|
||||
// package a imports package b
|
||||
// package b imports package a
|
||||
|
||||
// ✅ 解决方案1:提取共享类型到独立包
|
||||
// package types (共享类型)
|
||||
// package a imports types
|
||||
// package b imports types
|
||||
|
||||
// ✅ 解决方案2:使用接口解耦
|
||||
// package a 定义接口
|
||||
// package b 实现接口
|
||||
```
|
||||
|
||||
### 9.3 导出标识符规范
|
||||
|
||||
```go
|
||||
// ✅ 只导出必要的标识符
|
||||
type UserService struct {
|
||||
db *sql.DB // 私有
|
||||
}
|
||||
|
||||
func (s *UserService) GetUser(id int) (*User, error) // 公开
|
||||
func (s *UserService) validate(u *User) error // 私有
|
||||
|
||||
// ✅ 内部包限制访问
|
||||
// internal/database/... 只能被同项目代码导入
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 工具与检查
|
||||
|
||||
### 10.1 必须使用的工具
|
||||
|
||||
```bash
|
||||
# 格式化(必须)
|
||||
gofmt -w .
|
||||
goimports -w .
|
||||
|
||||
# 静态分析
|
||||
go vet ./...
|
||||
|
||||
# 竞态检测
|
||||
go test -race ./...
|
||||
|
||||
# 逃逸分析
|
||||
go build -gcflags '-m -m' ./...
|
||||
```
|
||||
|
||||
### 10.2 推荐的 Linter
|
||||
|
||||
```bash
|
||||
# golangci-lint(集成多个 linter)
|
||||
golangci-lint run
|
||||
|
||||
# 常用检查项
|
||||
# - errcheck: 检查未处理的错误
|
||||
# - gosec: 安全检查
|
||||
# - ineffassign: 无效赋值
|
||||
# - staticcheck: 静态分析
|
||||
# - unused: 未使用的代码
|
||||
```
|
||||
|
||||
### 10.3 Benchmark 测试
|
||||
|
||||
```go
|
||||
// ✅ 性能基准测试
|
||||
func BenchmarkProcess(b *testing.B) {
|
||||
data := prepareData()
|
||||
b.ResetTimer() // 重置计时器
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
Process(data)
|
||||
}
|
||||
}
|
||||
|
||||
// 运行 benchmark
|
||||
// go test -bench=. -benchmem ./...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [Effective Go](https://go.dev/doc/effective_go)
|
||||
- [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments)
|
||||
- [Go Common Mistakes](https://go.dev/wiki/CommonMistakes)
|
||||
- [100 Go Mistakes](https://100go.co/)
|
||||
- [Go Proverbs](https://go-proverbs.github.io/)
|
||||
- [Uber Go Style Guide](https://github.com/uber-go/guide/blob/master/style.md)
|
||||
@@ -0,0 +1,405 @@
|
||||
# Java Code Review Guide
|
||||
|
||||
Java 审查重点:Java 17/21 新特性、Spring Boot 3 最佳实践、并发编程(虚拟线程)、JPA 性能优化以及代码可维护性。
|
||||
|
||||
## 目录
|
||||
|
||||
- [现代 Java 特性 (17/21+)](#现代-java-特性-1721)
|
||||
- [Stream API & Optional](#stream-api--optional)
|
||||
- [Spring Boot 最佳实践](#spring-boot-最佳实践)
|
||||
- [JPA 与 数据库性能](#jpa-与-数据库性能)
|
||||
- [并发与虚拟线程](#并发与虚拟线程)
|
||||
- [Lombok 使用规范](#lombok-使用规范)
|
||||
- [异常处理](#异常处理)
|
||||
- [测试规范](#测试规范)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## 现代 Java 特性 (17/21+)
|
||||
|
||||
### Record (记录类)
|
||||
|
||||
```java
|
||||
// ❌ 传统的 POJO/DTO:样板代码多
|
||||
public class UserDto {
|
||||
private final String name;
|
||||
private final int age;
|
||||
|
||||
public UserDto(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
// getters, equals, hashCode, toString...
|
||||
}
|
||||
|
||||
// ✅ 使用 Record:简洁、不可变、语义清晰
|
||||
public record UserDto(String name, int age) {
|
||||
// 紧凑构造函数进行验证
|
||||
public UserDto {
|
||||
if (age < 0) throw new IllegalArgumentException("Age cannot be negative");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Switch 表达式与模式匹配
|
||||
|
||||
```java
|
||||
// ❌ 传统的 Switch:容易漏掉 break,不仅冗长且易错
|
||||
String type = "";
|
||||
switch (obj) {
|
||||
case Integer i: // Java 16+
|
||||
type = String.format("int %d", i);
|
||||
break;
|
||||
case String s:
|
||||
type = String.format("string %s", s);
|
||||
break;
|
||||
default:
|
||||
type = "unknown";
|
||||
}
|
||||
|
||||
// ✅ Switch 表达式:无穿透风险,强制返回值
|
||||
String type = switch (obj) {
|
||||
case Integer i -> "int %d".formatted(i);
|
||||
case String s -> "string %s".formatted(s);
|
||||
case null -> "null value"; // Java 21 处理 null
|
||||
default -> "unknown";
|
||||
};
|
||||
```
|
||||
|
||||
### 文本块 (Text Blocks)
|
||||
|
||||
```java
|
||||
// ❌ 拼接 SQL/JSON 字符串
|
||||
String json = "{\n" +
|
||||
" \"name\": \"Alice\",\n" +
|
||||
" \"age\": 20\n" +
|
||||
"}";
|
||||
|
||||
// ✅ 使用文本块:所见即所得
|
||||
String json = """
|
||||
{
|
||||
"name": "Alice",
|
||||
"age": 20
|
||||
}
|
||||
""";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stream API & Optional
|
||||
|
||||
### 避免滥用 Stream
|
||||
|
||||
```java
|
||||
// ❌ 简单的循环不需要 Stream(性能开销 + 可读性差)
|
||||
items.stream().forEach(item -> {
|
||||
process(item);
|
||||
});
|
||||
|
||||
// ✅ 简单场景直接用 for-each
|
||||
for (var item : items) {
|
||||
process(item);
|
||||
}
|
||||
|
||||
// ❌ 极其复杂的 Stream 链
|
||||
List<Dto> result = list.stream()
|
||||
.filter(...)
|
||||
.map(...)
|
||||
.peek(...)
|
||||
.sorted(...)
|
||||
.collect(...); // 难以调试
|
||||
|
||||
// ✅ 拆分为有意义的步骤
|
||||
var filtered = list.stream().filter(...).toList();
|
||||
// ...
|
||||
```
|
||||
|
||||
### Optional 正确用法
|
||||
|
||||
```java
|
||||
// ❌ 将 Optional 用作参数或字段(序列化问题,增加调用复杂度)
|
||||
public void process(Optional<String> name) { ... }
|
||||
public class User {
|
||||
private Optional<String> email; // 不推荐
|
||||
}
|
||||
|
||||
// ✅ Optional 仅用于返回值
|
||||
public Optional<User> findUser(String id) { ... }
|
||||
|
||||
// ❌ 既然用了 Optional 还在用 isPresent() + get()
|
||||
Optional<User> userOpt = findUser(id);
|
||||
if (userOpt.isPresent()) {
|
||||
return userOpt.get().getName();
|
||||
} else {
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
// ✅ 使用函数式 API
|
||||
return findUser(id)
|
||||
.map(User::getName)
|
||||
.orElse("Unknown");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Spring Boot 最佳实践
|
||||
|
||||
### 依赖注入 (DI)
|
||||
|
||||
```java
|
||||
// ❌ 字段注入 (@Autowired)
|
||||
// 缺点:难以测试(需要反射注入),掩盖了依赖过多的问题,且不可变性差
|
||||
@Service
|
||||
public class UserService {
|
||||
@Autowired
|
||||
private UserRepository userRepo;
|
||||
}
|
||||
|
||||
// ✅ 构造器注入 (Constructor Injection)
|
||||
// 优点:依赖明确,易于单元测试 (Mock),字段可为 final
|
||||
@Service
|
||||
public class UserService {
|
||||
private final UserRepository userRepo;
|
||||
|
||||
public UserService(UserRepository userRepo) {
|
||||
this.userRepo = userRepo;
|
||||
}
|
||||
}
|
||||
// 💡 提示:结合 Lombok @RequiredArgsConstructor 可简化代码,但要小心循环依赖
|
||||
```
|
||||
|
||||
### 配置管理
|
||||
|
||||
```java
|
||||
// ❌ 硬编码配置值
|
||||
@Service
|
||||
public class PaymentService {
|
||||
private String apiKey = "sk_live_12345";
|
||||
}
|
||||
|
||||
// ❌ 直接使用 @Value 散落在代码中
|
||||
@Value("${app.payment.api-key}")
|
||||
private String apiKey;
|
||||
|
||||
// ✅ 使用 @ConfigurationProperties 类型安全配置
|
||||
@ConfigurationProperties(prefix = "app.payment")
|
||||
public record PaymentProperties(String apiKey, int timeout, String url) {}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## JPA 与 数据库性能
|
||||
|
||||
### N+1 查询问题
|
||||
|
||||
```java
|
||||
// ❌ FetchType.EAGER 或 循环中触发懒加载
|
||||
// Entity 定义
|
||||
@Entity
|
||||
public class User {
|
||||
@OneToMany(fetch = FetchType.EAGER) // 危险!
|
||||
private List<Order> orders;
|
||||
}
|
||||
|
||||
// 业务代码
|
||||
List<User> users = userRepo.findAll(); // 1 条 SQL
|
||||
for (User user : users) {
|
||||
// 如果是 Lazy,这里会触发 N 条 SQL
|
||||
System.out.println(user.getOrders().size());
|
||||
}
|
||||
|
||||
// ✅ 使用 @EntityGraph 或 JOIN FETCH
|
||||
@Query("SELECT u FROM User u JOIN FETCH u.orders")
|
||||
List<User> findAllWithOrders();
|
||||
```
|
||||
|
||||
### 事务管理
|
||||
|
||||
```java
|
||||
// ❌ 在 Controller 层开启事务(数据库连接占用时间过长)
|
||||
// ❌ 在 private 方法上加 @Transactional(AOP 不生效)
|
||||
@Transactional
|
||||
private void saveInternal() { ... }
|
||||
|
||||
// ✅ 在 Service 层公共方法加 @Transactional
|
||||
// ✅ 读操作显式标记 readOnly = true (性能优化)
|
||||
@Service
|
||||
public class UserService {
|
||||
@Transactional(readOnly = true)
|
||||
public User getUser(Long id) { ... }
|
||||
|
||||
@Transactional
|
||||
public void createUser(UserDto dto) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Entity 设计
|
||||
|
||||
```java
|
||||
// ❌ 在 Entity 中使用 Lombok @Data
|
||||
// @Data 生成的 equals/hashCode 包含所有字段,可能触发懒加载导致性能问题或异常
|
||||
@Entity
|
||||
@Data
|
||||
public class User { ... }
|
||||
|
||||
// ✅ 仅使用 @Getter, @Setter
|
||||
// ✅ 自定义 equals/hashCode (通常基于 ID)
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
public class User {
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof User)) return false;
|
||||
return id != null && id.equals(((User) o).id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 并发与虚拟线程
|
||||
|
||||
### 虚拟线程 (Java 21+)
|
||||
|
||||
```java
|
||||
// ❌ 传统线程池处理大量 I/O 阻塞任务(资源耗尽)
|
||||
ExecutorService executor = Executors.newFixedThreadPool(100);
|
||||
|
||||
// ✅ 使用虚拟线程处理 I/O 密集型任务(高吞吐量)
|
||||
// Spring Boot 3.2+ 开启:spring.threads.virtual.enabled=true
|
||||
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
|
||||
|
||||
// 在虚拟线程中,阻塞操作(如 DB 查询、HTTP 请求)几乎不消耗 OS 线程资源
|
||||
```
|
||||
|
||||
### 线程安全
|
||||
|
||||
```java
|
||||
// ❌ SimpleDateFormat 是线程不安全的
|
||||
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||
|
||||
// ✅ 使用 DateTimeFormatter (Java 8+)
|
||||
private static final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
// ❌ HashMap 在多线程环境会数据丢失(Java 7 及之前 resize 还可能死循环,Java 8 修复了死循环但仍非线程安全)
|
||||
// ✅ 使用 ConcurrentHashMap
|
||||
Map<String, String> cache = new ConcurrentHashMap<>();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Lombok 使用规范
|
||||
|
||||
```java
|
||||
// ❌ 滥用 @Builder 导致无法强制校验必填字段
|
||||
@Builder
|
||||
public class Order {
|
||||
private String id; // 必填
|
||||
private String note; // 选填
|
||||
}
|
||||
// 调用者可能漏掉 id: Order.builder().note("hi").build();
|
||||
|
||||
// ✅ 关键业务对象建议手动编写 Builder 或构造函数以确保不变量
|
||||
// 或者在 build() 方法中添加校验逻辑 (Lombok @Builder.Default 等)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 异常处理
|
||||
|
||||
### 全局异常处理
|
||||
|
||||
```java
|
||||
// ❌ 到处 try-catch 吞掉异常或只打印日志
|
||||
try {
|
||||
userService.create(user);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(); // 不应该在生产环境使用
|
||||
// return null; // 吞掉异常,上层不知道发生了什么
|
||||
}
|
||||
|
||||
// ✅ 自定义异常 + @ControllerAdvice (Spring Boot 3 ProblemDetail)
|
||||
public class UserNotFoundException extends RuntimeException { ... }
|
||||
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(UserNotFoundException.class)
|
||||
public ProblemDetail handleNotFound(UserNotFoundException e) {
|
||||
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试规范
|
||||
|
||||
### 单元测试 vs 集成测试
|
||||
|
||||
```java
|
||||
// ❌ 单元测试依赖真实数据库或外部服务
|
||||
@SpringBootTest // 启动整个 Context,慢
|
||||
public class UserServiceTest { ... }
|
||||
|
||||
// ✅ 单元测试使用 Mockito
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class UserServiceTest {
|
||||
@Mock UserRepository repo;
|
||||
@InjectMocks UserService service;
|
||||
|
||||
@Test
|
||||
void shouldCreateUser() { ... }
|
||||
}
|
||||
|
||||
// ✅ 集成测试使用 Testcontainers
|
||||
@Testcontainers
|
||||
@SpringBootTest
|
||||
class UserRepositoryTest {
|
||||
@Container
|
||||
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### 基础与规范
|
||||
- [ ] 遵循 Java 17/21 新特性(Switch 表达式, Records, 文本块)
|
||||
- [ ] 避免使用已过时的类(Date, Calendar, SimpleDateFormat)
|
||||
- [ ] 集合操作是否优先使用了 Stream API 或 Collections 方法?
|
||||
- [ ] Optional 仅用于返回值,未用于字段或参数
|
||||
|
||||
### Spring Boot
|
||||
- [ ] 使用构造器注入而非 @Autowired 字段注入
|
||||
- [ ] 配置属性使用了 @ConfigurationProperties
|
||||
- [ ] Controller 职责单一,业务逻辑下沉到 Service
|
||||
- [ ] 全局异常处理使用了 @ControllerAdvice / ProblemDetail
|
||||
|
||||
### 数据库 & 事务
|
||||
- [ ] 读操作事务标记了 `@Transactional(readOnly = true)`
|
||||
- [ ] 检查是否存在 N+1 查询(EAGER fetch 或循环调用)
|
||||
- [ ] Entity 类未使用 @Data,正确实现了 equals/hashCode
|
||||
- [ ] 数据库索引是否覆盖了查询条件
|
||||
|
||||
### 并发与性能
|
||||
- [ ] I/O 密集型任务是否考虑了虚拟线程?
|
||||
- [ ] 线程安全类是否使用正确(ConcurrentHashMap vs HashMap)
|
||||
- [ ] 锁的粒度是否合理?避免在锁内进行 I/O 操作
|
||||
|
||||
### 可维护性
|
||||
- [ ] 关键业务逻辑有充分的单元测试
|
||||
- [ ] 日志记录恰当(使用 Slf4j,避免 System.out)
|
||||
- [ ] 魔法值提取为常量或枚举
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,593 @@
|
||||
# NestJS Code Review Guide
|
||||
|
||||
> NestJS 代码审查指南,覆盖依赖注入与分层架构、模块组织、Guard/Interceptor/Pipe、DTO 验证、错误处理、循环依赖及测试模式等核心主题。
|
||||
|
||||
## 目录
|
||||
|
||||
- [依赖注入与分层架构](#依赖注入与分层架构)
|
||||
- [模块组织](#模块组织)
|
||||
- [Guard / Interceptor / Pipe](#guard--interceptor--pipe)
|
||||
- [验证模式 (DTO)](#验证模式-dto)
|
||||
- [错误处理](#错误处理)
|
||||
- [循环依赖](#循环依赖)
|
||||
- [测试模式](#测试模式)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## 依赖注入与分层架构
|
||||
|
||||
### 三层架构:Controller → Service → Repository
|
||||
|
||||
```typescript
|
||||
// ❌ ORM 直接注入 Controller,跳过 Service 层
|
||||
@Controller('users')
|
||||
export class UsersController {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.prisma.user.findMany();
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Controller → Service → Repository
|
||||
@Controller('users')
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.usersService.findAll();
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(private readonly usersRepo: UsersRepository) {}
|
||||
|
||||
findAll() {
|
||||
return this.usersRepo.findAll();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Repository 之间不应互相注入
|
||||
|
||||
```typescript
|
||||
// ❌ Repository 导入另一个 Repository——编排逻辑属于 Service
|
||||
@Injectable()
|
||||
export class OrdersRepository {
|
||||
constructor(private readonly usersRepository: UsersRepository) {}
|
||||
}
|
||||
|
||||
// ✅ 跨 Repository 编排在 Service 中完成
|
||||
@Injectable()
|
||||
export class OrdersService {
|
||||
constructor(
|
||||
private readonly ordersRepo: OrdersRepository,
|
||||
private readonly usersRepo: UsersRepository,
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
### God Service:依赖超过 8 个时拆分
|
||||
|
||||
```typescript
|
||||
// ❌ 9 个依赖的巨型 Service
|
||||
@Injectable()
|
||||
export class OrdersService {
|
||||
constructor(
|
||||
private readonly ordersRepo: OrdersRepository,
|
||||
private readonly usersRepo: UsersRepository,
|
||||
private readonly productsRepo: ProductsRepository,
|
||||
private readonly paymentsService: PaymentsService,
|
||||
private readonly mailerService: MailerService,
|
||||
private readonly inventoryService: InventoryService,
|
||||
private readonly discountService: DiscountService,
|
||||
private readonly taxService: TaxService,
|
||||
private readonly auditService: AuditService,
|
||||
) {}
|
||||
}
|
||||
|
||||
// ✅ 拆分为 Use-Case Service(一个文件一个操作)
|
||||
@Injectable()
|
||||
export class CreateOrderService {
|
||||
constructor(
|
||||
private readonly ordersRepo: OrdersRepository,
|
||||
private readonly paymentsService: PaymentsService,
|
||||
) {}
|
||||
|
||||
async execute(dto: CreateOrderDto) { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### Symbol Token 实现依赖反转
|
||||
|
||||
```typescript
|
||||
// ❌ 直接依赖具体实现——测试时无法替换
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(private readonly repo: TypeOrmUserRepository) {}
|
||||
}
|
||||
|
||||
// ✅ 接口 + Symbol Token——可替换为内存实现
|
||||
export const USER_REPOSITORY = Symbol('USER_REPOSITORY');
|
||||
|
||||
export interface UserRepository {
|
||||
findAll(): Promise<User[]>;
|
||||
findById(id: string): Promise<User | null>;
|
||||
}
|
||||
|
||||
// module:
|
||||
{
|
||||
provide: USER_REPOSITORY,
|
||||
useClass: TypeOrmUserRepository,
|
||||
}
|
||||
|
||||
// service:
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(@Inject(USER_REPOSITORY) private readonly repo: UserRepository) {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 模块组织
|
||||
|
||||
### 推荐四层结构
|
||||
|
||||
```
|
||||
src/
|
||||
common/ ← 全局技术基础设施(Guards、Filters、Interceptors、Decorators)
|
||||
core/ ← 内部基础设施(Config、Database、Queue 配置)
|
||||
integrations/ ← 外部服务封装(Mailer、Storage、Stripe、SMS)
|
||||
modules/ ← 按领域组织的业务逻辑
|
||||
[feature]/
|
||||
dtos/
|
||||
repositories/
|
||||
services/
|
||||
internal/ ← 模块内共享 Service
|
||||
use-cases/ ← 一个文件 = 一个操作
|
||||
types/
|
||||
[feature].controller.ts
|
||||
[feature].module.ts
|
||||
```
|
||||
|
||||
### Domain 必须框架无关
|
||||
|
||||
```typescript
|
||||
// ❌ Domain Entity 依赖 NestJS——不可独立测试
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class User {
|
||||
constructor(private readonly email: string) {}
|
||||
}
|
||||
|
||||
// ✅ Domain 是纯类,无框架装饰器
|
||||
export class User {
|
||||
private constructor(private readonly email: string) {}
|
||||
|
||||
static create(email: string): User {
|
||||
return new User(email);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 关键规则
|
||||
|
||||
- `common/` 必须 **不涉及业务**——如果需要知道"订单",它不属于这里
|
||||
- `integrations/` 封装每个外部服务;换 SendGrid → AWS SES 只改一个目录
|
||||
- 使用 **Use-Case Service**(一个文件一个操作)而非 15 个方法的巨型 `XxxService`
|
||||
|
||||
---
|
||||
|
||||
## Guard / Interceptor / Pipe
|
||||
|
||||
### 业务逻辑不应放在 Guard 中
|
||||
|
||||
```typescript
|
||||
// ❌ Guard 中查询数据库 + 业务判断
|
||||
@Injectable()
|
||||
export class OrderOwnershipGuard implements CanActivate {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const order = await this.prisma.order.findUnique({
|
||||
where: { id: req.params.id },
|
||||
});
|
||||
if (order.userId !== req.user.id) {
|
||||
return false; // 数据获取 + 业务规则判断都在 Guard 里
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Guard 只做授权检查(角色/权限)
|
||||
@Injectable()
|
||||
export class RolesGuard implements CanActivate {
|
||||
constructor(private readonly reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
if (!requiredRoles) return true;
|
||||
const { user } = context.switchToHttp().getRequest();
|
||||
return requiredRoles.some((role) => user.roles?.includes(role));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Interceptor 只用于横切关注点
|
||||
|
||||
```typescript
|
||||
// ❌ Interceptor 中执行业务逻辑
|
||||
@Injectable()
|
||||
export class PricingInterceptor implements NestInterceptor {
|
||||
intercept(context: ExecutionContext, next: CallHandler) {
|
||||
// 计算折扣——这不是横切关注点!
|
||||
return next.handle().pipe(map(data => applyDiscount(data)));
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Interceptor 用于日志、缓存、响应转换、计时
|
||||
@Injectable()
|
||||
export class LoggingInterceptor implements NestInterceptor {
|
||||
intercept(context: ExecutionContext, next: CallHandler) {
|
||||
const now = Date.now();
|
||||
const req = context.switchToHttp().getRequest();
|
||||
return next.handle().pipe(
|
||||
tap(() => console.log(`${req.method} ${req.url} - ${Date.now() - now}ms`)),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 全局 ValidationPipe 必须配置 whitelist
|
||||
|
||||
```typescript
|
||||
// ❌ 没有 whitelist——请求体中的额外属性直接传入
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
await app.listen(3000);
|
||||
}
|
||||
|
||||
// ✅ 全局 ValidationPipe + whitelist 过滤未知属性
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
transform: true,
|
||||
}),
|
||||
);
|
||||
await app.listen(3000);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验证模式 (DTO)
|
||||
|
||||
### @ValidateNested() 必须搭配 @Type()
|
||||
|
||||
```typescript
|
||||
// ❌ 只有 @ValidateNested——嵌套对象验证被静默跳过!
|
||||
export class CreateOrderDto {
|
||||
@ValidateNested()
|
||||
shipping: AddressDto;
|
||||
}
|
||||
|
||||
// ✅ @ValidateNested + @Type 配对使用
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class CreateOrderDto {
|
||||
@ValidateNested()
|
||||
@Type(() => AddressDto)
|
||||
shipping: AddressDto;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => OrderItemDto)
|
||||
items: OrderItemDto[];
|
||||
}
|
||||
```
|
||||
|
||||
### 禁止裸 any Body
|
||||
|
||||
```typescript
|
||||
// ❌ 没有 DTO——无验证、无类型安全、无 Swagger 文档
|
||||
@Post()
|
||||
create(@Body() body: any) {
|
||||
return this.service.create(body);
|
||||
}
|
||||
|
||||
// ✅ 为每个操作创建 DTO
|
||||
export class CreateUserDto {
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(100)
|
||||
name: string;
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() dto: CreateUserDto) {
|
||||
return this.service.create(dto);
|
||||
}
|
||||
```
|
||||
|
||||
### Create 和 Update 应使用不同 DTO
|
||||
|
||||
```typescript
|
||||
// ❌ PATCH 也要求所有字段——不合理的 API 设计
|
||||
@Patch(':id')
|
||||
update(@Body() dto: CreateUserDto) { /* all fields required */ }
|
||||
|
||||
// ✅ Update 使用 PartialType
|
||||
export class UpdateUserDto extends PartialType(CreateUserDto) {}
|
||||
|
||||
@Patch(':id')
|
||||
update(@Body() dto: UpdateUserDto) { /* all fields optional */ }
|
||||
```
|
||||
|
||||
### 可选嵌套对象
|
||||
|
||||
```typescript
|
||||
// ❌ 可选嵌套对象缺少 @IsOptional
|
||||
export class UpdateOrderDto {
|
||||
@ValidateNested()
|
||||
@Type(() => AddressDto)
|
||||
shipping?: AddressDto; // undefined 时仍尝试验证
|
||||
}
|
||||
|
||||
// ✅ @IsOptional + @ValidateNested + @Type
|
||||
export class UpdateOrderDto {
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => AddressDto)
|
||||
shipping?: AddressDto;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 禁止吞掉错误
|
||||
|
||||
```typescript
|
||||
// ❌ catch { return null }——隐藏了问题,调用者无法区分"不存在"和"出错了"
|
||||
async findOne(id: string) {
|
||||
try {
|
||||
return await this.repo.findById(id);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 抛出有意义的异常
|
||||
async findOne(id: string): Promise<User> {
|
||||
const user = await this.repo.findById(id);
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User ${id} not found`);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
```
|
||||
|
||||
### 使用内置异常类
|
||||
|
||||
```typescript
|
||||
// ❌ 手动构造 HTTP 响应
|
||||
throw new HttpException('Bad request', 400);
|
||||
|
||||
// ✅ 使用语义化的内置异常
|
||||
throw new BadRequestException('Invalid email format');
|
||||
throw new NotFoundException('User not found');
|
||||
throw new ConflictException('Email already taken');
|
||||
throw new ForbiddenException('Insufficient permissions');
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
```
|
||||
|
||||
### 自定义异常过滤器
|
||||
|
||||
```typescript
|
||||
// ✅ 全局异常过滤器——统一响应格式
|
||||
@Catch()
|
||||
export class AllExceptionsFilter implements ExceptionFilter {
|
||||
private readonly logger = new Logger(AllExceptionsFilter.name);
|
||||
|
||||
catch(exception: unknown, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse();
|
||||
const request = ctx.getRequest();
|
||||
|
||||
const status =
|
||||
exception instanceof HttpException
|
||||
? exception.getStatus()
|
||||
: HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
this.logger.error(`${request.method} ${request.url} - ${status}`, exception instanceof Error ? exception.stack : '');
|
||||
|
||||
response.status(status).json({
|
||||
statusCode: status,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 循环依赖
|
||||
|
||||
### 模块间循环引用
|
||||
|
||||
```typescript
|
||||
// ❌ Module A ↔ Module B
|
||||
@Module({ imports: [UsersModule] })
|
||||
export class OrdersModule {}
|
||||
|
||||
@Module({ imports: [OrdersModule] })
|
||||
export class UsersModule {}
|
||||
|
||||
// ✅ 提取共享逻辑到第三个模块
|
||||
@Module({
|
||||
providers: [SharedService],
|
||||
exports: [SharedService],
|
||||
})
|
||||
export class SharedModule {}
|
||||
|
||||
@Module({ imports: [SharedModule] })
|
||||
export class OrdersModule {}
|
||||
|
||||
@Module({ imports: [SharedModule] })
|
||||
export class UsersModule {}
|
||||
```
|
||||
|
||||
### forwardRef 是最后手段
|
||||
|
||||
```typescript
|
||||
// ⚠️ forwardRef 表示设计有问题——优先重新设计
|
||||
@Module({
|
||||
imports: [forwardRef(() => UsersModule)],
|
||||
})
|
||||
export class OrdersModule {}
|
||||
|
||||
// ✅ 重新设计消除循环:
|
||||
// 1. 提取共享模块
|
||||
// 2. 使用事件驱动(EventEmitter)代替直接调用
|
||||
// 3. 将共享逻辑提升到上层 Service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试模式
|
||||
|
||||
### Use-Case 可脱离 NestJS 测试
|
||||
|
||||
```typescript
|
||||
// ✅ 无需 NestFactory——直接 new
|
||||
describe('CreateUserHandler', () => {
|
||||
let handler: CreateUserHandler;
|
||||
let repo: InMemoryUserRepository;
|
||||
|
||||
beforeEach(() => {
|
||||
repo = new InMemoryUserRepository();
|
||||
handler = new CreateUserHandler(repo);
|
||||
});
|
||||
|
||||
it('creates a user', async () => {
|
||||
const id = await handler.execute(
|
||||
new CreateUserCommand('user@example.com', 'Alice'),
|
||||
);
|
||||
expect(id).toBeDefined();
|
||||
});
|
||||
|
||||
it('rejects duplicate email', async () => {
|
||||
await handler.execute(new CreateUserCommand('user@example.com', 'Alice'));
|
||||
await expect(
|
||||
handler.execute(new CreateUserCommand('user@example.com', 'Bob')),
|
||||
).rejects.toThrow('already exists');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### E2E 测试应配置与生产一致的 Pipes
|
||||
|
||||
```typescript
|
||||
describe('UsersController (e2e)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleFixture = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
// 必须与 main.ts 中相同的全局配置
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
transform: true,
|
||||
}),
|
||||
);
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/POST users - valid', () => {
|
||||
return request(app.getHttpServer())
|
||||
.post('/users')
|
||||
.send({ email: 'test@test.com', name: 'Test' })
|
||||
.expect(201);
|
||||
});
|
||||
|
||||
it('/POST users - extra fields rejected', () => {
|
||||
return request(app.getHttpServer())
|
||||
.post('/users')
|
||||
.send({ email: 'test@test.com', name: 'Test', role: 'admin' })
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### 分层架构
|
||||
|
||||
- [ ] ORM/Prisma 未直接注入 Controller
|
||||
- [ ] 业务逻辑不在 Controller 中
|
||||
- [ ] Repository 之间无互相注入
|
||||
- [ ] Service 依赖数 ≤ 8(超出则拆分为 Use-Case)
|
||||
|
||||
### 依赖注入
|
||||
|
||||
- [ ] 接口 + Symbol Token 用于可替换的依赖
|
||||
- [ ] 无 `forwardRef()`(如有,需设计文档说明原因)
|
||||
- [ ] Scoped 服务未注入到 Singleton 中
|
||||
|
||||
### 验证
|
||||
|
||||
- [ ] 每个 `@ValidateNested()` 都有对应的 `@Type()`
|
||||
- [ ] 全局 `ValidationPipe({ whitelist: true, forbidNonWhitelisted: true })` 已配置
|
||||
- [ ] 无 `@Body() body: any`——必须使用 DTO
|
||||
- [ ] Create 和 Update 使用不同 DTO(`PartialType`)
|
||||
- [ ] 数组验证使用 `{ each: true }`
|
||||
- [ ] 可选嵌套对象使用 `@IsOptional()` + `@ValidateNested()` + `@Type()`
|
||||
|
||||
### Guard / Interceptor / Pipe
|
||||
|
||||
- [ ] Guard 只做授权检查,不查询数据库
|
||||
- [ ] Interceptor 只用于横切关注点(日志、缓存、响应转换)
|
||||
- [ ] 业务规则在 Service 中
|
||||
|
||||
### 错误处理
|
||||
|
||||
- [ ] 无 `catch { return null }`——抛出有意义的异常
|
||||
- [ ] 使用 NestJS 内置异常类
|
||||
- [ ] 自定义异常过滤器在 `common/filters/` 中
|
||||
|
||||
### 模块
|
||||
|
||||
- [ ] 无循环模块引用
|
||||
- [ ] Domain Entity 无框架装饰器(`@Injectable` 等)
|
||||
- [ ] 外部服务调用在 `integrations/` 中
|
||||
|
||||
### 测试
|
||||
|
||||
- [ ] Use-Case Service 可脱离 NestJS 测试
|
||||
- [ ] E2E 测试配置与生产一致的全局 Pipes/Guards
|
||||
- [ ] Domain Entity 零框架依赖
|
||||
@@ -0,0 +1,816 @@
|
||||
# Performance Review Guide
|
||||
|
||||
性能审查指南,覆盖前端、后端、数据库、算法复杂度和 API 性能。
|
||||
|
||||
## 目录
|
||||
|
||||
- [前端性能 (Core Web Vitals)](#前端性能-core-web-vitals)
|
||||
- [JavaScript 性能](#javascript-性能)
|
||||
- [内存管理](#内存管理)
|
||||
- [数据库性能](#数据库性能)
|
||||
- [API 性能](#api-性能)
|
||||
- [算法复杂度](#算法复杂度)
|
||||
- [性能审查清单](#性能审查清单)
|
||||
|
||||
---
|
||||
|
||||
## 前端性能 (Core Web Vitals)
|
||||
|
||||
### 2024 核心指标
|
||||
|
||||
| 指标 | 全称 | 目标值 | 含义 |
|
||||
|------|------|--------|------|
|
||||
| **LCP** | Largest Contentful Paint | ≤ 2.5s | 最大内容绘制时间 |
|
||||
| **INP** | Interaction to Next Paint | ≤ 200ms | 交互响应时间(2024 年替代 FID)|
|
||||
| **CLS** | Cumulative Layout Shift | ≤ 0.1 | 累积布局偏移 |
|
||||
| **FCP** | First Contentful Paint | ≤ 1.8s | 首次内容绘制 |
|
||||
| **TBT** | Total Blocking Time | ≤ 200ms | 主线程阻塞时间 |
|
||||
|
||||
### LCP 优化检查
|
||||
|
||||
```javascript
|
||||
// ❌ LCP 图片懒加载 - 延迟关键内容
|
||||
<img src="hero.jpg" loading="lazy" />
|
||||
|
||||
// ✅ LCP 图片立即加载
|
||||
<img src="hero.jpg" fetchpriority="high" />
|
||||
|
||||
// ❌ 未优化的图片格式
|
||||
<img src="hero.png" /> // PNG 文件过大
|
||||
|
||||
// ✅ 现代图片格式 + 响应式
|
||||
<picture>
|
||||
<source srcset="hero.avif" type="image/avif" />
|
||||
<source srcset="hero.webp" type="image/webp" />
|
||||
<img src="hero.jpg" alt="Hero" />
|
||||
</picture>
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- [ ] LCP 元素是否设置 `fetchpriority="high"`?
|
||||
- [ ] 是否使用 WebP/AVIF 格式?
|
||||
- [ ] 是否有服务端渲染或静态生成?
|
||||
- [ ] CDN 是否配置正确?
|
||||
|
||||
### FCP 优化检查
|
||||
|
||||
```html
|
||||
<!-- ❌ 阻塞渲染的 CSS -->
|
||||
<link rel="stylesheet" href="all-styles.css" />
|
||||
|
||||
<!-- ✅ 关键 CSS 内联 + 异步加载其余 -->
|
||||
<style>/* 首屏关键样式 */</style>
|
||||
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'" />
|
||||
|
||||
<!-- ❌ 阻塞渲染的字体 -->
|
||||
@font-face {
|
||||
font-family: 'CustomFont';
|
||||
src: url('font.woff2');
|
||||
}
|
||||
|
||||
<!-- ✅ 字体显示优化 -->
|
||||
@font-face {
|
||||
font-family: 'CustomFont';
|
||||
src: url('font.woff2');
|
||||
font-display: swap; /* 先用系统字体,加载后切换 */
|
||||
}
|
||||
```
|
||||
|
||||
### INP 优化检查
|
||||
|
||||
```javascript
|
||||
// ❌ 长任务阻塞主线程
|
||||
button.addEventListener('click', () => {
|
||||
// 耗时 500ms 的同步操作
|
||||
processLargeData(data);
|
||||
updateUI();
|
||||
});
|
||||
|
||||
// ✅ 拆分长任务
|
||||
button.addEventListener('click', async () => {
|
||||
// 让出主线程
|
||||
await scheduler.yield?.() ?? new Promise(r => setTimeout(r, 0));
|
||||
|
||||
// 分批处理
|
||||
for (const chunk of chunks) {
|
||||
processChunk(chunk);
|
||||
await scheduler.yield?.();
|
||||
}
|
||||
updateUI();
|
||||
});
|
||||
|
||||
// ✅ 使用 Web Worker 处理复杂计算
|
||||
const worker = new Worker('heavy-computation.js');
|
||||
worker.postMessage(data);
|
||||
worker.onmessage = (e) => updateUI(e.data);
|
||||
```
|
||||
|
||||
### CLS 优化检查
|
||||
|
||||
```css
|
||||
/* ❌ 未指定尺寸的媒体 */
|
||||
img { width: 100%; }
|
||||
|
||||
/* ✅ 预留空间 */
|
||||
img {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
/* ❌ 动态插入内容导致布局偏移 */
|
||||
.ad-container { }
|
||||
|
||||
/* ✅ 预留固定高度 */
|
||||
.ad-container {
|
||||
min-height: 250px;
|
||||
}
|
||||
```
|
||||
|
||||
**CLS 审查清单:**
|
||||
- [ ] 图片/视频是否有 width/height 或 aspect-ratio?
|
||||
- [ ] 字体加载是否使用 `font-display: swap`?
|
||||
- [ ] 动态内容是否预留空间?
|
||||
- [ ] 是否避免在现有内容上方插入内容?
|
||||
|
||||
---
|
||||
|
||||
## JavaScript 性能
|
||||
|
||||
### 代码分割与懒加载
|
||||
|
||||
```javascript
|
||||
// ❌ 一次性加载所有代码
|
||||
import { HeavyChart } from './charts';
|
||||
import { PDFExporter } from './pdf';
|
||||
import { AdminPanel } from './admin';
|
||||
|
||||
// ✅ 按需加载
|
||||
const HeavyChart = lazy(() => import('./charts'));
|
||||
const PDFExporter = lazy(() => import('./pdf'));
|
||||
|
||||
// ✅ 路由级代码分割
|
||||
const routes = [
|
||||
{
|
||||
path: '/dashboard',
|
||||
component: lazy(() => import('./pages/Dashboard')),
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
component: lazy(() => import('./pages/Admin')),
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### Bundle 体积优化
|
||||
|
||||
```javascript
|
||||
// ❌ 导入整个库
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
// ✅ 按需导入
|
||||
import debounce from 'lodash/debounce';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
// ❌ 未使用 Tree Shaking
|
||||
export default {
|
||||
fn1() {},
|
||||
fn2() {}, // 未使用但被打包
|
||||
};
|
||||
|
||||
// ✅ 命名导出支持 Tree Shaking
|
||||
export function fn1() {}
|
||||
export function fn2() {}
|
||||
```
|
||||
|
||||
**Bundle 审查清单:**
|
||||
- [ ] 是否使用动态 import() 进行代码分割?
|
||||
- [ ] 大型库是否按需导入?
|
||||
- [ ] 是否分析过 bundle 大小?(webpack-bundle-analyzer)
|
||||
- [ ] 是否有未使用的依赖?
|
||||
|
||||
### 列表渲染优化
|
||||
|
||||
```javascript
|
||||
// ❌ 渲染大列表
|
||||
function List({ items }) {
|
||||
return (
|
||||
<ul>
|
||||
{items.map(item => <li key={item.id}>{item.name}</li>)}
|
||||
</ul>
|
||||
); // 10000 条数据 = 10000 个 DOM 节点
|
||||
}
|
||||
|
||||
// ✅ 虚拟列表 - 只渲染可见项
|
||||
import { FixedSizeList } from 'react-window';
|
||||
|
||||
function VirtualList({ items }) {
|
||||
return (
|
||||
<FixedSizeList
|
||||
height={400}
|
||||
itemCount={items.length}
|
||||
itemSize={35}
|
||||
>
|
||||
{({ index, style }) => (
|
||||
<div style={style}>{items[index].name}</div>
|
||||
)}
|
||||
</FixedSizeList>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**大数据审查要点:**
|
||||
- [ ] 列表超过 100 项是否使用虚拟滚动?
|
||||
- [ ] 表格是否支持分页或虚拟化?
|
||||
- [ ] 是否有不必要的全量渲染?
|
||||
|
||||
---
|
||||
|
||||
## 内存管理
|
||||
|
||||
### 常见内存泄漏
|
||||
|
||||
#### 1. 未清理的事件监听
|
||||
|
||||
```javascript
|
||||
// ❌ 组件卸载后事件仍在监听
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
// ✅ 清理事件监听
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
```
|
||||
|
||||
#### 2. 未清理的定时器
|
||||
|
||||
```javascript
|
||||
// ❌ 定时器未清理
|
||||
useEffect(() => {
|
||||
setInterval(fetchData, 5000);
|
||||
}, []);
|
||||
|
||||
// ✅ 清理定时器
|
||||
useEffect(() => {
|
||||
const timer = setInterval(fetchData, 5000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
```
|
||||
|
||||
#### 3. 闭包引用
|
||||
|
||||
```javascript
|
||||
// ❌ 闭包持有大对象引用
|
||||
function createHandler() {
|
||||
const largeData = new Array(1000000).fill('x');
|
||||
|
||||
return function handler() {
|
||||
// largeData 被闭包引用,无法被回收
|
||||
console.log(largeData.length);
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ 只保留必要数据
|
||||
function createHandler() {
|
||||
const largeData = new Array(1000000).fill('x');
|
||||
const length = largeData.length; // 只保留需要的值
|
||||
|
||||
return function handler() {
|
||||
console.log(length);
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. 未清理的订阅
|
||||
|
||||
```javascript
|
||||
// ❌ WebSocket/EventSource 未关闭
|
||||
useEffect(() => {
|
||||
const ws = new WebSocket('wss://...');
|
||||
ws.onmessage = handleMessage;
|
||||
}, []);
|
||||
|
||||
// ✅ 清理连接
|
||||
useEffect(() => {
|
||||
const ws = new WebSocket('wss://...');
|
||||
ws.onmessage = handleMessage;
|
||||
return () => ws.close();
|
||||
}, []);
|
||||
```
|
||||
|
||||
### 内存审查清单
|
||||
|
||||
```markdown
|
||||
- [ ] useEffect 是否都有清理函数?
|
||||
- [ ] 事件监听是否在组件卸载时移除?
|
||||
- [ ] 定时器是否被清理?
|
||||
- [ ] WebSocket/SSE 连接是否关闭?
|
||||
- [ ] 大对象是否及时释放?
|
||||
- [ ] 是否有全局变量累积数据?
|
||||
```
|
||||
|
||||
### 检测工具
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| Chrome DevTools Memory | 堆快照分析 |
|
||||
| MemLab (Meta) | 自动化内存泄漏检测 |
|
||||
| Performance Monitor | 实时内存监控 |
|
||||
|
||||
---
|
||||
|
||||
## 数据库性能
|
||||
|
||||
### N+1 查询问题
|
||||
|
||||
```python
|
||||
# ❌ N+1 问题 - 1 + N 次查询
|
||||
users = User.objects.all() # 1 次查询
|
||||
for user in users:
|
||||
print(user.profile.bio) # N 次查询(每个用户一次)
|
||||
|
||||
# ✅ Eager Loading - 2 次查询
|
||||
users = User.objects.select_related('profile').all()
|
||||
for user in users:
|
||||
print(user.profile.bio) # 无额外查询
|
||||
|
||||
# ✅ 多对多关系用 prefetch_related
|
||||
posts = Post.objects.prefetch_related('tags').all()
|
||||
```
|
||||
|
||||
```javascript
|
||||
// TypeORM 示例
|
||||
// ❌ N+1 问题
|
||||
const users = await userRepository.find();
|
||||
for (const user of users) {
|
||||
const posts = await user.posts; // 每次循环都查询
|
||||
}
|
||||
|
||||
// ✅ Eager Loading
|
||||
const users = await userRepository.find({
|
||||
relations: ['posts'],
|
||||
});
|
||||
```
|
||||
|
||||
### 索引优化
|
||||
|
||||
```sql
|
||||
-- ❌ 全表扫描
|
||||
SELECT * FROM orders WHERE status = 'pending';
|
||||
|
||||
-- ✅ 添加索引
|
||||
CREATE INDEX idx_orders_status ON orders(status);
|
||||
|
||||
-- ❌ 索引失效:函数操作
|
||||
SELECT * FROM users WHERE YEAR(created_at) = 2024;
|
||||
|
||||
-- ✅ 范围查询可用索引
|
||||
SELECT * FROM users
|
||||
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';
|
||||
|
||||
-- ❌ 索引失效:LIKE 前缀通配符
|
||||
SELECT * FROM products WHERE name LIKE '%phone%';
|
||||
|
||||
-- ✅ 前缀匹配可用索引
|
||||
SELECT * FROM products WHERE name LIKE 'phone%';
|
||||
```
|
||||
|
||||
### 查询优化
|
||||
|
||||
```sql
|
||||
-- ❌ SELECT * 获取不需要的列
|
||||
SELECT * FROM users WHERE id = 1;
|
||||
|
||||
-- ✅ 只查询需要的列
|
||||
SELECT id, name, email FROM users WHERE id = 1;
|
||||
|
||||
-- ❌ 大表无 LIMIT
|
||||
SELECT * FROM logs WHERE type = 'error';
|
||||
|
||||
-- ✅ 分页查询
|
||||
SELECT * FROM logs WHERE type = 'error' LIMIT 100 OFFSET 0;
|
||||
|
||||
-- ❌ 在循环中执行查询
|
||||
for id in user_ids:
|
||||
cursor.execute("SELECT * FROM users WHERE id = %s", (id,))
|
||||
|
||||
-- ✅ 批量查询
|
||||
cursor.execute("SELECT * FROM users WHERE id IN %s", (tuple(user_ids),))
|
||||
```
|
||||
|
||||
### 数据库审查清单
|
||||
|
||||
```markdown
|
||||
🔴 必须检查:
|
||||
- [ ] 是否存在 N+1 查询?
|
||||
- [ ] WHERE 子句列是否有索引?
|
||||
- [ ] 是否避免了 SELECT *?
|
||||
- [ ] 大表查询是否有 LIMIT?
|
||||
|
||||
🟡 建议检查:
|
||||
- [ ] 是否使用了 EXPLAIN 分析查询计划?
|
||||
- [ ] 复合索引列顺序是否正确?
|
||||
- [ ] 是否有未使用的索引?
|
||||
- [ ] 是否有慢查询日志监控?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 性能
|
||||
|
||||
### 分页实现
|
||||
|
||||
```javascript
|
||||
// ❌ 返回全部数据
|
||||
app.get('/users', async (req, res) => {
|
||||
const users = await User.findAll(); // 可能返回 100000 条
|
||||
res.json(users);
|
||||
});
|
||||
|
||||
// ✅ 分页 + 限制最大数量
|
||||
app.get('/users', async (req, res) => {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = Math.min(parseInt(req.query.limit) || 20, 100); // 最大 100
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const { rows, count } = await User.findAndCountAll({
|
||||
limit,
|
||||
offset,
|
||||
order: [['id', 'ASC']],
|
||||
});
|
||||
|
||||
res.json({
|
||||
data: rows,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total: count,
|
||||
totalPages: Math.ceil(count / limit),
|
||||
},
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 缓存策略
|
||||
|
||||
```javascript
|
||||
// ✅ Redis 缓存示例
|
||||
async function getUser(id) {
|
||||
const cacheKey = `user:${id}`;
|
||||
|
||||
// 1. 检查缓存
|
||||
const cached = await redis.get(cacheKey);
|
||||
if (cached) {
|
||||
return JSON.parse(cached);
|
||||
}
|
||||
|
||||
// 2. 查询数据库
|
||||
const user = await db.users.findById(id);
|
||||
|
||||
// 3. 写入缓存(设置过期时间)
|
||||
await redis.setex(cacheKey, 3600, JSON.stringify(user));
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
// ✅ HTTP 缓存头
|
||||
app.get('/static-data', (req, res) => {
|
||||
res.set({
|
||||
'Cache-Control': 'public, max-age=86400', // 24 小时
|
||||
'ETag': 'abc123',
|
||||
});
|
||||
res.json(data);
|
||||
});
|
||||
```
|
||||
|
||||
### 响应压缩
|
||||
|
||||
```javascript
|
||||
// ✅ 启用 Gzip/Brotli 压缩
|
||||
const compression = require('compression');
|
||||
app.use(compression());
|
||||
|
||||
// ✅ 只返回必要字段
|
||||
// 请求: GET /users?fields=id,name,email
|
||||
app.get('/users', async (req, res) => {
|
||||
const fields = req.query.fields?.split(',') || ['id', 'name'];
|
||||
const users = await User.findAll({
|
||||
attributes: fields,
|
||||
});
|
||||
res.json(users);
|
||||
});
|
||||
```
|
||||
|
||||
### 限流保护
|
||||
|
||||
```javascript
|
||||
// ✅ 速率限制
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
const limiter = rateLimit({
|
||||
windowMs: 60 * 1000, // 1 分钟
|
||||
max: 100, // 最多 100 次请求
|
||||
message: { error: 'Too many requests, please try again later.' },
|
||||
});
|
||||
|
||||
app.use('/api/', limiter);
|
||||
```
|
||||
|
||||
### API 审查清单
|
||||
|
||||
```markdown
|
||||
- [ ] 列表接口是否有分页?
|
||||
- [ ] 是否限制了每页最大数量?
|
||||
- [ ] 热点数据是否有缓存?
|
||||
- [ ] 是否启用了响应压缩?
|
||||
- [ ] 是否有速率限制?
|
||||
- [ ] 是否只返回必要字段?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 算法复杂度
|
||||
|
||||
### 常见复杂度对比
|
||||
|
||||
| 复杂度 | 名称 | 10 条 | 1000 条 | 100 万条 | 示例 |
|
||||
|--------|------|-------|---------|----------|------|
|
||||
| O(1) | 常数 | 1 | 1 | 1 | 哈希查找 |
|
||||
| O(log n) | 对数 | 3 | 10 | 20 | 二分查找 |
|
||||
| O(n) | 线性 | 10 | 1000 | 100 万 | 遍历数组 |
|
||||
| O(n log n) | 线性对数 | 33 | 10000 | 2000 万 | 快速排序 |
|
||||
| O(n²) | 平方 | 100 | 100 万 | 1 万亿 | 嵌套循环 |
|
||||
| O(2ⁿ) | 指数 | 1024 | ∞ | ∞ | 递归斐波那契 |
|
||||
|
||||
### 代码审查中的识别
|
||||
|
||||
```javascript
|
||||
// ❌ O(n²) - 嵌套循环
|
||||
function findDuplicates(arr) {
|
||||
const duplicates = [];
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
for (let j = i + 1; j < arr.length; j++) {
|
||||
if (arr[i] === arr[j]) {
|
||||
duplicates.push(arr[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return duplicates;
|
||||
}
|
||||
|
||||
// ✅ O(n) - 使用 Set
|
||||
function findDuplicates(arr) {
|
||||
const seen = new Set();
|
||||
const duplicates = new Set();
|
||||
for (const item of arr) {
|
||||
if (seen.has(item)) {
|
||||
duplicates.add(item);
|
||||
}
|
||||
seen.add(item);
|
||||
}
|
||||
return [...duplicates];
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
// ❌ O(n²) - 每次循环都调用 includes
|
||||
function removeDuplicates(arr) {
|
||||
const result = [];
|
||||
for (const item of arr) {
|
||||
if (!result.includes(item)) { // includes 是 O(n)
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ✅ O(n) - 使用 Set
|
||||
function removeDuplicates(arr) {
|
||||
return [...new Set(arr)];
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
// ❌ O(n) 查找 - 每次都遍历
|
||||
const users = [{ id: 1, name: 'A' }, { id: 2, name: 'B' }, ...];
|
||||
|
||||
function getUser(id) {
|
||||
return users.find(u => u.id === id); // O(n)
|
||||
}
|
||||
|
||||
// ✅ O(1) 查找 - 使用 Map
|
||||
const userMap = new Map(users.map(u => [u.id, u]));
|
||||
|
||||
function getUser(id) {
|
||||
return userMap.get(id); // O(1)
|
||||
}
|
||||
```
|
||||
|
||||
### 空间复杂度考虑
|
||||
|
||||
```javascript
|
||||
// ⚠️ O(n) 空间 - 创建新数组
|
||||
const doubled = arr.map(x => x * 2);
|
||||
|
||||
// ✅ O(1) 空间 - 原地修改(如果允许)
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
arr[i] *= 2;
|
||||
}
|
||||
|
||||
// ⚠️ 递归深度过大可能栈溢出
|
||||
function factorial(n) {
|
||||
if (n <= 1) return 1;
|
||||
return n * factorial(n - 1); // O(n) 栈空间
|
||||
}
|
||||
|
||||
// ✅ 迭代版本 O(1) 空间
|
||||
function factorial(n) {
|
||||
let result = 1;
|
||||
for (let i = 2; i <= n; i++) {
|
||||
result *= i;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
### 复杂度审查问题
|
||||
|
||||
```markdown
|
||||
💡 "这个嵌套循环的复杂度是 O(n²),数据量大时会有性能问题"
|
||||
🔴 "这里用 Array.includes() 在循环中,整体是 O(n²),建议用 Set"
|
||||
🟡 "这个递归深度可能导致栈溢出,建议改为迭代或尾递归"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能审查清单
|
||||
|
||||
### 🔴 必须检查(阻塞级)
|
||||
|
||||
**前端:**
|
||||
- [ ] LCP 图片是否懒加载?(不应该)
|
||||
- [ ] 是否有 `transition: all`?
|
||||
- [ ] 是否动画 width/height/top/left?
|
||||
- [ ] 列表 >100 项是否虚拟化?
|
||||
|
||||
**后端:**
|
||||
- [ ] 是否存在 N+1 查询?
|
||||
- [ ] 列表接口是否有分页?
|
||||
- [ ] 是否有 SELECT * 查大表?
|
||||
|
||||
**通用:**
|
||||
- [ ] 是否有 O(n²) 或更差的嵌套循环?
|
||||
- [ ] useEffect/事件监听是否有清理?
|
||||
|
||||
### 🟡 建议检查(重要级)
|
||||
|
||||
**前端:**
|
||||
- [ ] 是否使用代码分割?
|
||||
- [ ] 大型库是否按需导入?
|
||||
- [ ] 图片是否使用 WebP/AVIF?
|
||||
- [ ] 是否有未使用的依赖?
|
||||
|
||||
**后端:**
|
||||
- [ ] 热点数据是否有缓存?
|
||||
- [ ] WHERE 列是否有索引?
|
||||
- [ ] 是否有慢查询监控?
|
||||
|
||||
**API:**
|
||||
- [ ] 是否启用响应压缩?
|
||||
- [ ] 是否有速率限制?
|
||||
- [ ] 是否只返回必要字段?
|
||||
|
||||
### 🟢 优化建议(建议级)
|
||||
|
||||
- [ ] 是否分析过 bundle 大小?
|
||||
- [ ] 是否使用 CDN?
|
||||
- [ ] 是否有性能监控?
|
||||
- [ ] 是否做过性能基准测试?
|
||||
|
||||
---
|
||||
|
||||
## 性能度量阈值
|
||||
|
||||
### 前端指标
|
||||
|
||||
| 指标 | 好 | 需改进 | 差 |
|
||||
|------|-----|--------|-----|
|
||||
| LCP | ≤ 2.5s | 2.5-4s | > 4s |
|
||||
| INP | ≤ 200ms | 200-500ms | > 500ms |
|
||||
| CLS | ≤ 0.1 | 0.1-0.25 | > 0.25 |
|
||||
| FCP | ≤ 1.8s | 1.8-3s | > 3s |
|
||||
| Bundle Size (JS) | < 200KB | 200-500KB | > 500KB |
|
||||
|
||||
### 后端指标
|
||||
|
||||
| 指标 | 好 | 需改进 | 差 |
|
||||
|------|-----|--------|-----|
|
||||
| API 响应时间 | < 100ms | 100-500ms | > 500ms |
|
||||
| 数据库查询 | < 50ms | 50-200ms | > 200ms |
|
||||
| 页面加载 | < 3s | 3-5s | > 5s |
|
||||
|
||||
---
|
||||
|
||||
## 工具推荐
|
||||
|
||||
### 前端性能
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| [Lighthouse](https://developer.chrome.com/docs/lighthouse/) | Core Web Vitals 测试 |
|
||||
| [WebPageTest](https://www.webpagetest.org/) | 详细性能分析 |
|
||||
| [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) | Bundle 分析 |
|
||||
| [Chrome DevTools Performance](https://developer.chrome.com/docs/devtools/performance/) | 运行时性能分析 |
|
||||
|
||||
### 内存检测
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| [MemLab](https://github.com/facebookincubator/memlab) | 自动化内存泄漏检测 |
|
||||
| Chrome Memory Tab | 堆快照分析 |
|
||||
|
||||
### 后端性能
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| EXPLAIN | 数据库查询计划分析 |
|
||||
| [pganalyze](https://pganalyze.com/) | PostgreSQL 性能监控 |
|
||||
| [New Relic](https://newrelic.com/) / [Datadog](https://www.datadoghq.com/) | APM 监控 |
|
||||
|
||||
---
|
||||
|
||||
## 低级别效率反模式
|
||||
|
||||
代码层面的效率失误,独立于架构层面的性能问题。补充 [common-bugs-checklist.md](common-bugs-checklist.md) 中已涵盖的资源管理与并发缺陷。
|
||||
|
||||
### 不必要的重复工作
|
||||
|
||||
- [ ] 同一函数 / 查询是否在同一 request/render 中被重复调用?
|
||||
- [ ] 文件 / 配置是否在循环内重复读取(loop-invariant)?
|
||||
- [ ] 计算结果是否可以被缓存或向下游传递?
|
||||
|
||||
```typescript
|
||||
// ❌ loop-invariant 在循环内反复执行
|
||||
for (const path of paths) {
|
||||
const config = JSON.parse(fs.readFileSync("config.json", "utf-8"));
|
||||
processFile(path, config);
|
||||
}
|
||||
|
||||
// ✅ 提到循环外
|
||||
const config = JSON.parse(fs.readFileSync("config.json", "utf-8"));
|
||||
for (const path of paths) processFile(path, config);
|
||||
```
|
||||
|
||||
### 错失的并发机会
|
||||
|
||||
- [ ] 独立的 async 操作是否顺序 `await`?
|
||||
- [ ] 是否可以用 `Promise.all` / `asyncio.gather` / `tokio::join!` 并发?
|
||||
|
||||
```typescript
|
||||
// ❌ 顺序 await
|
||||
const a = await fetchA();
|
||||
const b = await fetchB();
|
||||
|
||||
// ✅ 并发
|
||||
const [a, b] = await Promise.all([fetchA(), fetchB()]);
|
||||
```
|
||||
|
||||
### 热路径膨胀
|
||||
|
||||
- [ ] 模块级 / import 时代码是否执行重操作(文件 I/O、网络、大对象构造)?
|
||||
- [ ] per-request 路径是否有可延迟的初始化?
|
||||
- [ ] 启动时代码是否阻塞首次请求?
|
||||
|
||||
### 无界数据结构
|
||||
|
||||
> 资源生命周期相关缺陷(未关闭的连接、未移除的监听器、未清除的定时器)见 [common-bugs-checklist.md → Resource Management](common-bugs-checklist.md#resource-management)。本节聚焦 *容量边界*。
|
||||
|
||||
- [ ] 全局 dict / list / 缓存是否有 `max-size` 或 TTL?
|
||||
- [ ] 累积型数据结构(队列、日志、metrics buffer)是否有上限?
|
||||
- [ ] 每请求分配的对象是否会被持久引用而无法 GC?
|
||||
|
||||
```python
|
||||
# ❌ 无界缓存
|
||||
_cache: dict[str, Any] = {}
|
||||
|
||||
# ✅ 有界 LRU
|
||||
from functools import lru_cache
|
||||
|
||||
@lru_cache(maxsize=256)
|
||||
def get_cached(key: str) -> Any:
|
||||
return expensive_computation(key)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [Core Web Vitals - web.dev](https://web.dev/articles/vitals)
|
||||
- [Optimizing Core Web Vitals - Vercel](https://vercel.com/guides/optimizing-core-web-vitals-in-2024)
|
||||
- [MemLab - Meta Engineering](https://engineering.fb.com/2022/09/12/open-source/memlab/)
|
||||
- [Big O Cheat Sheet](https://www.bigocheatsheet.com/)
|
||||
- [N+1 Query Problem - Stack Overflow](https://stackoverflow.com/questions/97197/what-is-the-n1-selects-problem-in-orm-object-relational-mapping)
|
||||
- [API Performance Optimization](https://algorithmsin60days.com/blog/optimizing-api-performance/)
|
||||
@@ -0,0 +1,704 @@
|
||||
# PHP Code Review Guide
|
||||
|
||||
> PHP 8.x code review guide covering the type system, modern language features, OOP modeling, PDO data access, security, error handling, Composer dependencies, performance, and testing.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Quick Review Checklist](#quick-review-checklist)
|
||||
- [Type System & Modern PHP](#type-system--modern-php)
|
||||
- [Object Modeling](#object-modeling)
|
||||
- [Input, Output & Security](#input-output--security)
|
||||
- [Database Access](#database-access)
|
||||
- [Error Handling](#error-handling)
|
||||
- [Composer & Dependencies](#composer--dependencies)
|
||||
- [Performance & Resource Management](#performance--resource-management)
|
||||
- [Testing & Static Analysis](#testing--static-analysis)
|
||||
- [Review Checklist](#review-checklist)
|
||||
- [References](#references)
|
||||
|
||||
---
|
||||
|
||||
## Quick Review Checklist
|
||||
|
||||
### Must-check
|
||||
|
||||
- [ ] New files enable `declare(strict_types=1);`
|
||||
- [ ] Public APIs have parameter, return, and property types
|
||||
- [ ] User input is validated; output is escaped per context
|
||||
- [ ] SQL uses parameterized queries or ORM binding
|
||||
- [ ] Passwords use `password_hash()` / `password_verify()`
|
||||
- [ ] File uploads validate MIME, size, extension, and storage path
|
||||
- [ ] `composer.lock` is committed; dependency ranges are reasonable
|
||||
- [ ] PHPUnit/Pest tests and PHPStan/Psalm static analysis are present
|
||||
|
||||
### Common issues
|
||||
|
||||
- [ ] Loose comparison `==` / `!=` causing type-juggling vulnerabilities
|
||||
- [ ] `md5()` / `sha1()` used to store passwords
|
||||
- [ ] Concatenating SQL, HTML, shell commands, or file paths
|
||||
- [ ] Using `@` to suppress errors
|
||||
- [ ] `unserialize()` on untrusted data
|
||||
- [ ] `$_GET` / `$_POST` / `$_FILES` flowing straight into business logic
|
||||
- [ ] PHP 8.2+ dynamic properties trigger a deprecation; PHP 9 may turn it into an error
|
||||
|
||||
---
|
||||
|
||||
## Type System & Modern PHP
|
||||
|
||||
### strict_types and explicit types
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ weak boundary: passing "42" gets silently coerced
|
||||
function findUser($id) {
|
||||
return User::find($id);
|
||||
}
|
||||
|
||||
// ✅ enable strict_types at the top of the file; type the public API
|
||||
declare(strict_types=1);
|
||||
|
||||
function findUser(int $id): ?User
|
||||
{
|
||||
return User::find($id);
|
||||
}
|
||||
```
|
||||
|
||||
Don't leave type checking entirely to runtime input validation. Type declarations express an internal contract; input validation expresses how much to trust the boundary. You need both.
|
||||
|
||||
### Avoid loose comparisons
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ strings like "0e12345" can be treated as 0 under loose comparison
|
||||
if ($providedHash == $storedHash) {
|
||||
grantAccess();
|
||||
}
|
||||
|
||||
// ✅ strict comparison; use hash_equals() for secrets or tokens
|
||||
if (hash_equals($storedHash, $providedHash)) {
|
||||
grantAccess();
|
||||
}
|
||||
|
||||
// ✅ match uses identity checks, so fewer type-juggling surprises than switch
|
||||
$status = match ($code) {
|
||||
200 => 'ok',
|
||||
404 => 'not_found',
|
||||
default => 'unknown',
|
||||
};
|
||||
```
|
||||
|
||||
Pay attention to `==`, `!=`, and `in_array($x, $list)` (loose by default) in auth, payment, state machine, and permission logic. Use `===`, `!==`, and `in_array($x, $list, true)` where it matters.
|
||||
|
||||
### Union / intersection / nullable types
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ mixed or untyped makes callers guess the return shape
|
||||
function loadConfig($source) {
|
||||
return parseConfig($source);
|
||||
}
|
||||
|
||||
// ✅ express the real contract with types
|
||||
function loadConfig(string|PathInfo $source): Config
|
||||
{
|
||||
return parseConfig($source);
|
||||
}
|
||||
|
||||
// ✅ make null explicit when it's a real business state
|
||||
function currentUser(): ?User
|
||||
{
|
||||
return Auth::user();
|
||||
}
|
||||
```
|
||||
|
||||
`mixed` can show up at the boundary or while migrating legacy code, but in core business services it usually signals missing modeling.
|
||||
|
||||
### The nullsafe operator shouldn't hide missing state
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ chained nullsafe blurs the reason for failure
|
||||
$country = $order?->customer?->profile?->country;
|
||||
|
||||
// ✅ branch explicitly on critical business state
|
||||
if ($order === null) {
|
||||
throw new OrderNotFound();
|
||||
}
|
||||
|
||||
$customer = $order->customer();
|
||||
if ($customer === null) {
|
||||
throw new MissingCustomer($order->id);
|
||||
}
|
||||
|
||||
$country = $customer->profile()?->country;
|
||||
```
|
||||
|
||||
Distinguish "optional display field" from "business invariant that must exist." The former is a good fit for `?->`; the latter should fail loudly.
|
||||
|
||||
---
|
||||
|
||||
## Object Modeling
|
||||
|
||||
### Use readonly properties and value objects
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ public mutable fields let callers change state at will
|
||||
class Money
|
||||
{
|
||||
public $amount;
|
||||
public $currency;
|
||||
}
|
||||
|
||||
// ✅ express an immutable value object with types and readonly
|
||||
final readonly class Money
|
||||
{
|
||||
public function __construct(
|
||||
public int $amount,
|
||||
public string $currency,
|
||||
) {
|
||||
if ($amount < 0) {
|
||||
throw new InvalidArgumentException('Amount must be non-negative');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For DTOs, config, and domain value objects, check first whether a `readonly class` or readonly properties can remove hidden side effects.
|
||||
|
||||
### Enums instead of string states
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ string states are easy to typo and can't enumerate the legal set
|
||||
if ($order->status === 'paied') {
|
||||
ship($order);
|
||||
}
|
||||
|
||||
// ✅ an enum surfaces illegal states earlier
|
||||
enum OrderStatus: string
|
||||
{
|
||||
case Pending = 'pending';
|
||||
case Paid = 'paid';
|
||||
case Cancelled = 'cancelled';
|
||||
}
|
||||
|
||||
if ($order->status === OrderStatus::Paid) {
|
||||
ship($order);
|
||||
}
|
||||
```
|
||||
|
||||
When reviewing state machines, permissions, or type fields, look for "magic string values." If the value set is stable, suggest an enum; if it comes from an external system, convert it to an internal enum before it enters the business layer.
|
||||
|
||||
### Don't rely on dynamic properties
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ PHP 8.2+ triggers a deprecation when creating a dynamic property
|
||||
$user = new User();
|
||||
$user->emali = 'a@example.com'; // a typo also silently creates a property
|
||||
|
||||
// ✅ declare properties or use a dedicated data structure
|
||||
final class User
|
||||
{
|
||||
public function __construct(
|
||||
public string $email,
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
`#[AllowDynamicProperties]` should be an exception for legacy compatibility, not the default for new code. Watch for serialization, ORM hydration, and test doubles that secretly rely on dynamic properties.
|
||||
|
||||
### Don't do heavy I/O in constructors
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ quietly connecting to the DB on construction makes testing and error handling hard
|
||||
final class ReportService
|
||||
{
|
||||
private PDO $pdo;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->pdo = new PDO($_ENV['DSN']);
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ inject dependencies from the outside
|
||||
final class ReportService
|
||||
{
|
||||
public function __construct(
|
||||
private PDO $pdo,
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
A constructor should establish the object's invariants — not send HTTP requests, open connections, read large files, or run complex queries.
|
||||
|
||||
---
|
||||
|
||||
## Input, Output & Security
|
||||
|
||||
### Validate input at the boundary
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ superglobals flow straight into business logic
|
||||
$user = $service->create($_POST['email'], $_POST['age']);
|
||||
|
||||
// ✅ validate and coerce types at the boundary first
|
||||
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
|
||||
$age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, [
|
||||
'options' => ['min_range' => 0, 'max_range' => 130],
|
||||
]);
|
||||
|
||||
if ($email === false || $email === null || $age === false || $age === null) {
|
||||
throw new InvalidInput();
|
||||
}
|
||||
|
||||
$user = $service->create($email, $age);
|
||||
```
|
||||
|
||||
`filter_input()` only handles a slice of basic validation. Complex rules, cross-field constraints, and business constraints still need a dedicated validator or request DTO.
|
||||
|
||||
### Escape output per context
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ user input goes straight into HTML
|
||||
echo "<h1>Hello {$_GET['name']}</h1>";
|
||||
|
||||
// ✅ use htmlspecialchars in an HTML text context
|
||||
$name = (string) ($_GET['name'] ?? '');
|
||||
echo '<h1>Hello ' . htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</h1>';
|
||||
```
|
||||
|
||||
Different contexts need different escaping: HTML text, HTML attributes, URLs, JavaScript strings, and CSS are all different. When a template engine's default escaping is turned off, treat it as a security risk.
|
||||
|
||||
### Passwords and randomness
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ md5/sha1 must not be used for password storage
|
||||
$hash = md5($password);
|
||||
|
||||
// ✅ use PHP's built-in password API
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
if (!password_verify($password, $hash)) {
|
||||
throw new InvalidCredentials();
|
||||
}
|
||||
|
||||
// ✅ use a CSPRNG for tokens
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$code = random_int(100000, 999999);
|
||||
```
|
||||
|
||||
Don't hand-roll salts, round migration, or password comparison. Use `password_needs_rehash()` when you need to upgrade the cost factor.
|
||||
|
||||
### Deserialization and object injection
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ untrusted input into unserialize can trigger object injection
|
||||
$payload = unserialize($_COOKIE['state']);
|
||||
|
||||
// ✅ prefer JSON for external data, and validate its schema/shape
|
||||
$payload = json_decode($_COOKIE['state'] ?? '{}', true, flags: JSON_THROW_ON_ERROR);
|
||||
```
|
||||
|
||||
If you must process historical serialized data, at least restrict `allowed_classes` and make sure the relevant classes' magic methods can't produce dangerous side effects.
|
||||
|
||||
### File uploads and paths
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ building the path from the raw filename
|
||||
$target = __DIR__ . '/uploads/' . $_FILES['avatar']['name'];
|
||||
move_uploaded_file($_FILES['avatar']['tmp_name'], $target);
|
||||
|
||||
// ✅ generate a server-side filename, check the upload error and MIME
|
||||
$file = $_FILES['avatar'];
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||
throw new UploadFailed();
|
||||
}
|
||||
|
||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||
$mime = $finfo->file($file['tmp_name']);
|
||||
if (!in_array($mime, ['image/png', 'image/jpeg'], true)) {
|
||||
throw new InvalidFileType();
|
||||
}
|
||||
|
||||
$target = __DIR__ . '/uploads/' . bin2hex(random_bytes(16)) . '.jpg';
|
||||
move_uploaded_file($file['tmp_name'], $target);
|
||||
```
|
||||
|
||||
When reviewing upload features, check size limits, MIME detection, extensions, a non-executable storage directory, path traversal, overwrite protection, and any virus-scan or async-processing requirements.
|
||||
|
||||
---
|
||||
|
||||
## Database Access
|
||||
|
||||
### Use parameterized queries
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ concatenated SQL is an injection risk
|
||||
$sql = "SELECT * FROM users WHERE email = '" . $_GET['email'] . "'";
|
||||
$user = $pdo->query($sql)->fetch();
|
||||
|
||||
// ✅ PDO prepared statement + bound value
|
||||
$stmt = $pdo->prepare('SELECT id, email FROM users WHERE email = :email');
|
||||
$stmt->execute(['email' => $email]);
|
||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
```
|
||||
|
||||
Parameters can only bind values — not table names, column names, or sort direction. Dynamic identifiers must go through a whitelist mapping.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ✅ whitelist the dynamic sort column
|
||||
$columns = [
|
||||
'created' => 'created_at',
|
||||
'email' => 'email',
|
||||
];
|
||||
|
||||
$column = $columns[$_GET['sort'] ?? 'created'] ?? $columns['created'];
|
||||
$stmt = $pdo->query("SELECT id, email FROM users ORDER BY {$column} DESC");
|
||||
```
|
||||
|
||||
### Wrap multi-step writes in transactions
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ multi-step writes with no transaction leave half-finished state on failure
|
||||
$orderId = $orders->create($cart);
|
||||
$inventory->reserve($cart);
|
||||
$payments->charge($orderId);
|
||||
|
||||
// ✅ explicit transaction boundary
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$orderId = $orders->create($cart);
|
||||
$inventory->reserve($cart);
|
||||
$payments->recordIntent($orderId);
|
||||
$pdo->commit();
|
||||
} catch (Throwable $e) {
|
||||
$pdo->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
```
|
||||
|
||||
Don't casually put external, non-rollbackable side effects (an actual charge, an email, a message dispatch) inside a database transaction. Common patterns are an outbox, an idempotency key, or triggering after the transaction commits.
|
||||
|
||||
### Avoid N+1 queries
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ querying inside a loop
|
||||
foreach ($orders as $order) {
|
||||
$customer = $customerRepo->find($order->customerId);
|
||||
render($order, $customer);
|
||||
}
|
||||
|
||||
// ✅ batch-load, then map
|
||||
$customerIds = array_unique(array_map(fn ($o) => $o->customerId, $orders));
|
||||
$customers = $customerRepo->findByIds($customerIds);
|
||||
|
||||
foreach ($orders as $order) {
|
||||
render($order, $customers[$order->customerId] ?? null);
|
||||
}
|
||||
```
|
||||
|
||||
In ORMs like Laravel/Doctrine, check eager loading, join fetch, selected columns, pagination, and indexes.
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Catch specific exceptions, keep context
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ swallowing the exception leaves callers unable to know it failed
|
||||
try {
|
||||
$mailer->send($message);
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
|
||||
// ✅ catch a specific exception, keep context, and rethrow
|
||||
try {
|
||||
$mailer->send($message);
|
||||
} catch (TransportException $e) {
|
||||
throw new NotificationFailed($userId, previous: $e);
|
||||
}
|
||||
```
|
||||
|
||||
Empty `catch` blocks, `error_log()`-and-continue without surfacing the error, and turning every exception into `RuntimeException('failed')` in production code all deserve a question.
|
||||
|
||||
### Don't suppress errors with @
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ hides the real error and makes debugging hard
|
||||
$content = @file_get_contents($path);
|
||||
|
||||
// ✅ handle failure explicitly
|
||||
$content = file_get_contents($path);
|
||||
if ($content === false) {
|
||||
throw new RuntimeException("Unable to read file: {$path}");
|
||||
}
|
||||
```
|
||||
|
||||
`@` is common around file, network, array access, and legacy library calls. Push for an explicit branch, or convert third-party errors into project exceptions.
|
||||
|
||||
### Don't leak sensitive data in logs
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ writing tokens, passwords, or the full request body to the log
|
||||
$logger->error('Login failed', ['request' => $_POST]);
|
||||
|
||||
// ✅ log non-sensitive context that still helps locate the problem
|
||||
$logger->warning('Login failed', [
|
||||
'email_hash' => hash('sha256', strtolower($email)),
|
||||
'ip' => $requestIp,
|
||||
]);
|
||||
```
|
||||
|
||||
Check logs, exception messages, the debug toolbar, error pages, and failed-queue records. Sensitive data includes passwords, tokens, sessions, PII, payment data, and full cookies.
|
||||
|
||||
---
|
||||
|
||||
## Composer & Dependencies
|
||||
|
||||
### Lock reproducible dependencies
|
||||
|
||||
```json
|
||||
{
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"monolog/monolog": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^11.0",
|
||||
"phpstan/phpstan": "^1.10"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When reviewing `composer.json` / `composer.lock`, watch for:
|
||||
|
||||
- Application repos commit `composer.lock`; library repos usually don't
|
||||
- `require-dev` shouldn't make it into the production image
|
||||
- The PHP platform version matches the CI version
|
||||
- Autoload rules aren't too broad (don't load test or script directories)
|
||||
- `scripts` commands don't depend on a developer's local secret config
|
||||
|
||||
### Dependency security and maintenance
|
||||
|
||||
```bash
|
||||
composer audit
|
||||
composer outdated --direct
|
||||
composer validate --strict
|
||||
```
|
||||
|
||||
When adding a package, look at its maintenance status — download count isn't the only signal. What matters is its security history, release cadence, minimal dependency footprint, and whether it duplicates the standard library or a framework built-in.
|
||||
|
||||
---
|
||||
|
||||
## Performance & Resource Management
|
||||
|
||||
### Stream large datasets with generators or pagination
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ loading every record at once
|
||||
$rows = $repo->all();
|
||||
foreach ($rows as $row) {
|
||||
exportRow($row);
|
||||
}
|
||||
|
||||
// ✅ paginate or use a generator to avoid a memory spike
|
||||
foreach ($repo->cursor() as $row) {
|
||||
exportRow($row);
|
||||
}
|
||||
```
|
||||
|
||||
A PHP request lifecycle is short, but CLI jobs, queue workers, and export tasks run for a long time. For that kind of code, watch memory growth, unclosed resources, and global-state pollution especially closely.
|
||||
|
||||
### Avoid expensive work inside loops
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ re-parsing config or opening a connection on every iteration
|
||||
foreach ($items as $item) {
|
||||
$client = new ApiClient($_ENV['API_KEY']);
|
||||
$client->send($item);
|
||||
}
|
||||
|
||||
// ✅ create reusable dependencies outside the loop
|
||||
$client = new ApiClient($_ENV['API_KEY']);
|
||||
foreach ($items as $item) {
|
||||
$client->send($item);
|
||||
}
|
||||
```
|
||||
|
||||
Watch for database queries, HTTP requests, regex compilation, large array copies, accumulating `array_merge()` appends, and repeatedly reading env vars or config files inside loops.
|
||||
|
||||
### Release or scope resources
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ✅ close file handles after use
|
||||
$handle = fopen($path, 'rb');
|
||||
if ($handle === false) {
|
||||
throw new RuntimeException('Unable to open file');
|
||||
}
|
||||
|
||||
try {
|
||||
while (($line = fgets($handle)) !== false) {
|
||||
process($line);
|
||||
}
|
||||
} finally {
|
||||
fclose($handle);
|
||||
}
|
||||
```
|
||||
|
||||
PDO connections are usually managed by the container, but file handles, curl handles, temp files, locks, and cached objects in queue workers still need an explicit lifecycle.
|
||||
|
||||
---
|
||||
|
||||
## Testing & Static Analysis
|
||||
|
||||
### Test behavior, not implementation details
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ asserting an internal method call makes refactoring expensive
|
||||
$mailer->expects($this->once())->method('buildTemplate');
|
||||
|
||||
// ✅ assert observable results
|
||||
$service->sendWelcomeEmail($user);
|
||||
|
||||
$this->assertTrue($mailbox->hasMessageFor($user->email));
|
||||
```
|
||||
|
||||
For business services, controllers, and queue jobs, prefer covering observable behavior: inputs/outputs, database state, published events, and dispatched messages.
|
||||
|
||||
### Static analysis and formatting
|
||||
|
||||
```bash
|
||||
vendor/bin/phpunit
|
||||
vendor/bin/phpstan analyse
|
||||
vendor/bin/psalm
|
||||
vendor/bin/php-cs-fixer fix --dry-run --diff
|
||||
vendor/bin/rector process --dry-run
|
||||
```
|
||||
|
||||
When reviewing a PR, check whether the new code lowers the PHPStan/Psalm level, leans heavily on baseline ignores, or uses `@phpstan-ignore-next-line` to paper over a real type problem.
|
||||
|
||||
### Isolate test data
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ the test depends on real time and external services
|
||||
$service->expireOldSessions();
|
||||
|
||||
// ✅ inject a clock and a fake gateway
|
||||
$clock->setNow(new DateTimeImmutable('2026-01-01T00:00:00Z'));
|
||||
$service->expireOldSessions();
|
||||
```
|
||||
|
||||
Watch for database transaction rollback, fixture cleanup, randomness, time, queues, caches, and external APIs. Slow PHP tests are usually not a language problem — it's that the boundaries aren't isolated.
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### Types & modeling
|
||||
|
||||
- [ ] `declare(strict_types=1);` at the top of the file
|
||||
- [ ] Parameters, return values, and properties have explicit types
|
||||
- [ ] `===` / `!==` used; collection lookups use strict mode
|
||||
- [ ] Stable state sets use an enum, not magic strings
|
||||
- [ ] New code doesn't rely on dynamic properties
|
||||
- [ ] Value objects are readonly or otherwise immutable
|
||||
|
||||
### Security
|
||||
|
||||
- [ ] Input is validated and type-coerced at the boundary
|
||||
- [ ] Output is escaped per HTML/URL/JS/CSS context
|
||||
- [ ] SQL uses prepared statements or ORM binding
|
||||
- [ ] Dynamic table/column/sort names go through a whitelist
|
||||
- [ ] Passwords use `password_hash()` / `password_verify()`
|
||||
- [ ] Tokens, codes, and filenames use `random_bytes()` / `random_int()`
|
||||
- [ ] Untrusted input never reaches `unserialize()`
|
||||
- [ ] File uploads check the error code, size, MIME, extension, and storage directory
|
||||
- [ ] No injection or leakage risk in shell commands, path building, or log output
|
||||
|
||||
### Data & transactions
|
||||
|
||||
- [ ] Multi-step writes have a transaction or compensation mechanism
|
||||
- [ ] External side effects are designed to be idempotent
|
||||
- [ ] N+1 queries avoided
|
||||
- [ ] Pagination, indexes, and selected columns are reasonable
|
||||
- [ ] Database errors aren't swallowed
|
||||
|
||||
### Maintainability
|
||||
|
||||
- [ ] Constructors don't do heavy I/O
|
||||
- [ ] Dependency injection is clear; no hidden global state
|
||||
- [ ] No `@` error suppression
|
||||
- [ ] Exceptions preserve context and `previous`
|
||||
- [ ] Composer dependency ranges, autoload, and scripts are reasonable
|
||||
- [ ] Application repos commit `composer.lock`
|
||||
|
||||
### Testing & tooling
|
||||
|
||||
- [ ] PHPUnit/Pest cover the critical and failure paths
|
||||
- [ ] PHPStan/Psalm config doesn't lower strictness
|
||||
- [ ] New ignores/baselines are explained
|
||||
- [ ] Formatting tools and CI commands are reproducible
|
||||
- [ ] Tests isolate time, randomness, the database, queues, and external APIs
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [PHP Manual: Type declarations](https://www.php.net/manual/en/language.types.declarations.php)
|
||||
- [PHP Manual: match](https://www.php.net/match)
|
||||
- [PHP Manual: Enumerations](https://www.php.net/manual/en/language.enumerations.overview.php)
|
||||
- [PHP Manual: Properties](https://www.php.net/manual/en/language.oop5.properties.php)
|
||||
- [PHP Manual: PDO](https://www.php.net/manual/en/class.pdo.php)
|
||||
- [PHP Manual: password_hash](https://www.php.net/manual/en/function.password-hash.php)
|
||||
- [PHP Manual: random_bytes](https://www.php.net/manual/en/function.random-bytes.php)
|
||||
- [Composer documentation](https://getcomposer.org/doc/)
|
||||
- [PHPUnit documentation](https://docs.phpunit.de/)
|
||||
- [PHPStan documentation](https://phpstan.org/user-guide/getting-started)
|
||||
- [Psalm documentation](https://psalm.dev/docs/)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,186 @@
|
||||
# Qt Code Review Guide
|
||||
|
||||
> Code review guidelines focusing on object model, signals/slots, event loop, and GUI performance. Examples based on Qt 5.15 / Qt 6.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Object Model & Memory Management](#object-model--memory-management)
|
||||
- [Signals & Slots](#signals--slots)
|
||||
- [Containers & Strings](#containers--strings)
|
||||
- [Threads & Concurrency](#threads--concurrency)
|
||||
- [GUI & Widgets](#gui--widgets)
|
||||
- [Meta-Object System](#meta-object-system)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## Object Model & Memory Management
|
||||
|
||||
### Use Parent-Child Ownership Mechanism
|
||||
Qt's `QObject` hierarchy automatically manages memory. For `QObject`, prefer setting a parent object over manual `delete` or smart pointers.
|
||||
|
||||
```cpp
|
||||
// ❌ Manual management prone to memory leaks
|
||||
QWidget* w = new QWidget();
|
||||
QLabel* l = new QLabel();
|
||||
l->setParent(w);
|
||||
// ... If w is deleted, l is automatically deleted. But if w leaks, l also leaks.
|
||||
|
||||
// ✅ Specify parent in constructor
|
||||
QWidget* w = new QWidget(this); // Owned by 'this'
|
||||
QLabel* l = new QLabel(w); // Owned by 'w'
|
||||
```
|
||||
|
||||
### Use Smart Pointers with QObject
|
||||
If a `QObject` has no parent, use `QScopedPointer` or `std::unique_ptr` with a custom deleter (use `deleteLater` if cross-thread). Avoid `std::shared_ptr` for `QObject` unless necessary, as it confuses the parent-child ownership system.
|
||||
|
||||
```cpp
|
||||
// ✅ Scoped pointer for local/member QObject without parent
|
||||
QScopedPointer<MyObject> obj(new MyObject());
|
||||
|
||||
// ✅ Safe pointer to prevent dangling pointers
|
||||
QPointer<MyObject> safePtr = obj.data();
|
||||
if (safePtr) {
|
||||
safePtr->doSomething();
|
||||
}
|
||||
```
|
||||
|
||||
### Use `deleteLater()`
|
||||
For asynchronous deletion, especially in slots or event handlers, use `deleteLater()` instead of `delete` to ensure pending events in the event loop are processed.
|
||||
|
||||
---
|
||||
|
||||
## Signals & Slots
|
||||
|
||||
### Prefer Function Pointer Syntax
|
||||
Use compile-time checked syntax (Qt 5+).
|
||||
|
||||
```cpp
|
||||
// ❌ String-based (runtime check only, slower)
|
||||
connect(sender, SIGNAL(valueChanged(int)), receiver, SLOT(updateValue(int)));
|
||||
|
||||
// ✅ Compile-time check
|
||||
connect(sender, &Sender::valueChanged, receiver, &Receiver::updateValue);
|
||||
```
|
||||
|
||||
### Connection Types
|
||||
Be explicit or aware of connection types when crossing threads.
|
||||
- `Qt::AutoConnection` (Default): Direct if same thread, Queued if different thread.
|
||||
- `Qt::QueuedConnection`: Always posts event (thread-safe across threads).
|
||||
- `Qt::DirectConnection`: Immediate call (dangerous if accessing non-thread-safe data across threads).
|
||||
|
||||
### Avoid Loops
|
||||
Check logic that might cause infinite signal loops (e.g., `valueChanged` -> `setValue` -> `valueChanged`). Block signals or check for equality before setting values.
|
||||
|
||||
```cpp
|
||||
void MyClass::setValue(int v) {
|
||||
if (m_value == v) return; // ✅ Good: Break loop
|
||||
m_value = v;
|
||||
emit valueChanged(v);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Containers & Strings
|
||||
|
||||
### QString Efficiency
|
||||
- Use `QStringLiteral("...")` for compile-time string creation to avoid runtime allocation.
|
||||
- Use `QLatin1String` for comparison with ASCII literals (in Qt 5).
|
||||
- Prefer `arg()` for formatting (or `QStringBuilder`'s `%` operator).
|
||||
|
||||
```cpp
|
||||
// ❌ Runtime conversion
|
||||
if (str == "test") ...
|
||||
|
||||
// ✅ Prefer QLatin1String for comparison with ASCII literals (in Qt 5)
|
||||
if (str == QLatin1String("test")) ... // Qt 5
|
||||
if (str == u"test"_s) ... // Qt 6
|
||||
```
|
||||
|
||||
### Container Selection
|
||||
- **Qt 6**: `QList` is now the default choice (unified with `QVector`).
|
||||
- **Qt 5**: Prefer `QVector` over `QList` for contiguous memory and cache performance, unless stable references are needed.
|
||||
- Be aware of Implicit Sharing (Copy-on-Write). Passing containers by value is cheap *until* modified. Use `const &` for read-only access.
|
||||
|
||||
```cpp
|
||||
// ❌ Forces deep copy if function modifies 'list'
|
||||
void process(QVector<int> list) {
|
||||
list[0] = 1;
|
||||
}
|
||||
|
||||
// ✅ Read-only reference
|
||||
void process(const QVector<int>& list) { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Threads & Concurrency
|
||||
|
||||
### Subclassing QThread vs Worker Object
|
||||
Prefer the "Worker Object" pattern over subclassing `QThread` implementation details.
|
||||
|
||||
```cpp
|
||||
// ❌ Business logic inside QThread::run()
|
||||
class MyThread : public QThread {
|
||||
void run() override { ... }
|
||||
};
|
||||
|
||||
// ✅ Worker object moved to thread
|
||||
QThread* thread = new QThread;
|
||||
Worker* worker = new Worker;
|
||||
worker->moveToThread(thread);
|
||||
connect(thread, &QThread::started, worker, &Worker::process);
|
||||
thread->start();
|
||||
```
|
||||
|
||||
### GUI Thread Safety
|
||||
**NEVER** access UI widgets (`QWidget` and subclasses) from a background thread. Use signals/slots to communicate updates to the main thread.
|
||||
|
||||
---
|
||||
|
||||
## GUI & Widgets
|
||||
|
||||
### Logic Separation
|
||||
Keep business logic out of UI classes (`MainWindow`, `Dialog`). UI classes should only handle display and user input forwarding.
|
||||
|
||||
### Layouts
|
||||
Avoid fixed sizes (`setGeometry`, `resize`). Use layouts (`QVBoxLayout`, `QGridLayout`) to handle different DPIs and window resizing gracefully.
|
||||
|
||||
### Blocking Event Loop
|
||||
Never execute long-running operations on the main thread (freezes GUI).
|
||||
- **Bad**: `Sleep()`, `while(busy)`, synchronous network calls.
|
||||
- **Good**: `QProcess`, `QThread`, `QtConcurrent`, or asynchronous APIs (`QNetworkAccessManager`).
|
||||
|
||||
---
|
||||
|
||||
## Meta-Object System
|
||||
|
||||
### Properties & Enums
|
||||
Use `Q_PROPERTY` for values exposed to QML or needing introspection.
|
||||
Use `Q_ENUM` to enable string conversion for enums.
|
||||
|
||||
```cpp
|
||||
class MyObject : public QObject {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged)
|
||||
public:
|
||||
enum State { Idle, Running };
|
||||
Q_ENUM(State)
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
### qobject_cast
|
||||
Use `qobject_cast<T*>` for QObjects instead of `dynamic_cast`. It is faster and doesn't require RTTI.
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
- [ ] **Memory**: Is parent-child relationship correct? Are dangling pointers avoided (using `QPointer`)?
|
||||
- [ ] **Signals**: Are connections checked? Do lambdas use safe captures (context object)?
|
||||
- [ ] **Threads**: Is UI accessed only from main thread? Are long tasks offloaded?
|
||||
- [ ] **Strings**: Are `QStringLiteral` or `tr()` used appropriately?
|
||||
- [ ] **Style**: Naming conventions (camelCase for methods, PascalCase for classes).
|
||||
- [ ] **Resources**: Are resources (images, styles) loaded from `.qrc`?
|
||||
@@ -0,0 +1,871 @@
|
||||
# React Code Review Guide
|
||||
|
||||
React 审查重点:Hooks 规则、性能优化的适度性、组件设计、以及现代 React 19/RSC 模式。
|
||||
|
||||
## 目录
|
||||
|
||||
- [基础 Hooks 规则](#基础-hooks-规则)
|
||||
- [useEffect 模式](#useeffect-模式)
|
||||
- [useMemo / useCallback](#usememo--usecallback)
|
||||
- [组件设计](#组件设计)
|
||||
- [Error Boundaries & Suspense](#error-boundaries--suspense)
|
||||
- [Server Components (RSC)](#server-components-rsc)
|
||||
- [React 19 Actions & Forms](#react-19-actions--forms)
|
||||
- [Suspense & Streaming SSR](#suspense--streaming-ssr)
|
||||
- [TanStack Query v5](#tanstack-query-v5)
|
||||
- [Review Checklists](#review-checklists)
|
||||
|
||||
---
|
||||
|
||||
## 基础 Hooks 规则
|
||||
|
||||
```tsx
|
||||
// ❌ 条件调用 Hooks — 违反 Hooks 规则
|
||||
function BadComponent({ isLoggedIn }) {
|
||||
if (isLoggedIn) {
|
||||
const [user, setUser] = useState(null); // Error!
|
||||
}
|
||||
return <div>...</div>;
|
||||
}
|
||||
|
||||
// ✅ Hooks 必须在组件顶层调用
|
||||
function GoodComponent({ isLoggedIn }) {
|
||||
const [user, setUser] = useState(null);
|
||||
if (!isLoggedIn) return <LoginPrompt />;
|
||||
return <div>{user?.name}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## useEffect 模式
|
||||
|
||||
```tsx
|
||||
// ❌ 依赖数组缺失或不完整
|
||||
function BadEffect({ userId }) {
|
||||
const [user, setUser] = useState(null);
|
||||
useEffect(() => {
|
||||
fetchUser(userId).then(setUser);
|
||||
}, []); // 缺少 userId 依赖!
|
||||
}
|
||||
|
||||
// ✅ 完整的依赖数组
|
||||
function GoodEffect({ userId }) {
|
||||
const [user, setUser] = useState(null);
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
fetchUser(userId).then(data => {
|
||||
if (!cancelled) setUser(data);
|
||||
});
|
||||
return () => { cancelled = true; }; // 清理函数
|
||||
}, [userId]);
|
||||
}
|
||||
|
||||
// ❌ useEffect 用于派生状态(反模式)
|
||||
function BadDerived({ items }) {
|
||||
const [filteredItems, setFilteredItems] = useState([]);
|
||||
useEffect(() => {
|
||||
setFilteredItems(items.filter(i => i.active));
|
||||
}, [items]); // 不必要的 effect + 额外渲染
|
||||
return <List items={filteredItems} />;
|
||||
}
|
||||
|
||||
// ✅ 直接在渲染时计算,或用 useMemo
|
||||
function GoodDerived({ items }) {
|
||||
const filteredItems = useMemo(
|
||||
() => items.filter(i => i.active),
|
||||
[items]
|
||||
);
|
||||
return <List items={filteredItems} />;
|
||||
}
|
||||
|
||||
// ❌ useEffect 用于事件响应
|
||||
function BadEventEffect() {
|
||||
const [query, setQuery] = useState('');
|
||||
useEffect(() => {
|
||||
if (query) {
|
||||
analytics.track('search', { query }); // 应该在事件处理器中
|
||||
}
|
||||
}, [query]);
|
||||
}
|
||||
|
||||
// ✅ 在事件处理器中执行副作用
|
||||
function GoodEvent() {
|
||||
const [query, setQuery] = useState('');
|
||||
const handleSearch = (q: string) => {
|
||||
setQuery(q);
|
||||
analytics.track('search', { query: q });
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## useMemo / useCallback
|
||||
|
||||
```tsx
|
||||
// ❌ 过度优化 — 常量不需要 useMemo
|
||||
function OverOptimized() {
|
||||
const config = useMemo(() => ({ timeout: 5000 }), []); // 无意义
|
||||
const handleClick = useCallback(() => {
|
||||
console.log('clicked');
|
||||
}, []); // 如果不传给 memo 组件,无意义
|
||||
}
|
||||
|
||||
// ✅ 只在需要时优化
|
||||
function ProperlyOptimized() {
|
||||
const config = { timeout: 5000 }; // 简单对象直接定义
|
||||
const handleClick = () => console.log('clicked');
|
||||
}
|
||||
|
||||
// ❌ useCallback 依赖总是变化
|
||||
function BadCallback({ data }) {
|
||||
// data 每次渲染都是新对象,useCallback 无效
|
||||
const process = useCallback(() => {
|
||||
return data.map(transform);
|
||||
}, [data]);
|
||||
}
|
||||
|
||||
// ✅ useMemo + useCallback 配合 React.memo 使用
|
||||
const MemoizedChild = React.memo(function Child({ onClick, items }) {
|
||||
return <div onClick={onClick}>{items.length}</div>;
|
||||
});
|
||||
|
||||
function Parent({ rawItems }) {
|
||||
const items = useMemo(() => processItems(rawItems), [rawItems]);
|
||||
const handleClick = useCallback(() => {
|
||||
console.log(items.length);
|
||||
}, [items]);
|
||||
return <MemoizedChild onClick={handleClick} items={items} />;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 组件设计
|
||||
|
||||
```tsx
|
||||
// ❌ 在组件内定义组件 — 每次渲染都创建新组件
|
||||
function BadParent() {
|
||||
function ChildComponent() { // 每次渲染都是新函数!
|
||||
return <div>child</div>;
|
||||
}
|
||||
return <ChildComponent />;
|
||||
}
|
||||
|
||||
// ✅ 组件定义在外部
|
||||
function ChildComponent() {
|
||||
return <div>child</div>;
|
||||
}
|
||||
function GoodParent() {
|
||||
return <ChildComponent />;
|
||||
}
|
||||
|
||||
// ❌ Props 总是新对象引用
|
||||
function BadProps() {
|
||||
return (
|
||||
<MemoizedComponent
|
||||
style={{ color: 'red' }} // 每次渲染新对象
|
||||
onClick={() => {}} // 每次渲染新函数
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ 稳定的引用
|
||||
const style = { color: 'red' };
|
||||
function GoodProps() {
|
||||
const handleClick = useCallback(() => {}, []);
|
||||
return <MemoizedComponent style={style} onClick={handleClick} />;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Boundaries & Suspense
|
||||
|
||||
```tsx
|
||||
// ❌ 没有错误边界
|
||||
function BadApp() {
|
||||
return (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<DataComponent /> {/* 错误会导致整个应用崩溃 */}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ Error Boundary 包裹 Suspense
|
||||
function GoodApp() {
|
||||
return (
|
||||
<ErrorBoundary fallback={<ErrorUI />}>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<DataComponent />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Server Components (RSC)
|
||||
|
||||
```tsx
|
||||
// ❌ 在 Server Component 中使用客户端特性
|
||||
// app/page.tsx (Server Component by default)
|
||||
function BadServerComponent() {
|
||||
const [count, setCount] = useState(0); // Error! No hooks in RSC
|
||||
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
|
||||
}
|
||||
|
||||
// ✅ 交互逻辑提取到 Client Component
|
||||
// app/counter.tsx
|
||||
'use client';
|
||||
function Counter() {
|
||||
const [count, setCount] = useState(0);
|
||||
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
|
||||
}
|
||||
|
||||
// app/page.tsx (Server Component)
|
||||
async function GoodServerComponent() {
|
||||
const data = await fetchData(); // 可以直接 await
|
||||
return (
|
||||
<div>
|
||||
<h1>{data.title}</h1>
|
||||
<Counter /> {/* 客户端组件 */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ❌ 'use client' 放置不当 — 整个树都变成客户端
|
||||
// layout.tsx
|
||||
'use client'; // 这会让所有子组件都成为客户端组件
|
||||
export default function Layout({ children }) { ... }
|
||||
|
||||
// ✅ 只在需要交互的组件使用 'use client'
|
||||
// 将客户端逻辑隔离到叶子组件
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## React 19 Actions & Forms
|
||||
|
||||
React 19 引入了 Actions 系统和新的表单处理 Hooks,简化异步操作和乐观更新。
|
||||
|
||||
### useActionState
|
||||
|
||||
```tsx
|
||||
// ❌ 传统方式:多个状态变量
|
||||
function OldForm() {
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
const handleSubmit = async (formData: FormData) => {
|
||||
setIsPending(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await submitForm(formData);
|
||||
setData(result);
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setIsPending(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ React 19: useActionState 统一管理
|
||||
import { useActionState } from 'react';
|
||||
|
||||
function NewForm() {
|
||||
const [state, formAction, isPending] = useActionState(
|
||||
async (prevState, formData: FormData) => {
|
||||
try {
|
||||
const result = await submitForm(formData);
|
||||
return { success: true, data: result };
|
||||
} catch (e) {
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
},
|
||||
{ success: false, data: null, error: null }
|
||||
);
|
||||
|
||||
return (
|
||||
<form action={formAction}>
|
||||
<input name="email" />
|
||||
<button disabled={isPending}>
|
||||
{isPending ? 'Submitting...' : 'Submit'}
|
||||
</button>
|
||||
{state.error && <p className="error">{state.error}</p>}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### useFormStatus
|
||||
|
||||
```tsx
|
||||
// ❌ Props 透传表单状态
|
||||
function BadSubmitButton({ isSubmitting }) {
|
||||
return <button disabled={isSubmitting}>Submit</button>;
|
||||
}
|
||||
|
||||
// ✅ useFormStatus 访问父 <form> 状态(无需 props)
|
||||
import { useFormStatus } from 'react-dom';
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending, data, method, action } = useFormStatus();
|
||||
// 注意:必须在 <form> 内部的子组件中使用
|
||||
return (
|
||||
<button disabled={pending}>
|
||||
{pending ? 'Submitting...' : 'Submit'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// ❌ useFormStatus 在 form 同级组件中调用——不工作
|
||||
function BadForm() {
|
||||
const { pending } = useFormStatus(); // 这里无法获取状态!
|
||||
return (
|
||||
<form action={action}>
|
||||
<button disabled={pending}>Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ useFormStatus 必须在 form 的子组件中
|
||||
function GoodForm() {
|
||||
return (
|
||||
<form action={action}>
|
||||
<SubmitButton /> {/* useFormStatus 在这里面调用 */}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### useOptimistic
|
||||
|
||||
```tsx
|
||||
// ❌ 等待服务器响应再更新 UI
|
||||
function SlowLike({ postId, likes }) {
|
||||
const [likeCount, setLikeCount] = useState(likes);
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
|
||||
const handleLike = async () => {
|
||||
setIsPending(true);
|
||||
const newCount = await likePost(postId); // 等待...
|
||||
setLikeCount(newCount);
|
||||
setIsPending(false);
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ useOptimistic 即时反馈,失败自动回滚
|
||||
import { useOptimistic } from 'react';
|
||||
|
||||
function FastLike({ postId, likes }) {
|
||||
const [optimisticLikes, addOptimisticLike] = useOptimistic(
|
||||
likes,
|
||||
(currentLikes, increment: number) => currentLikes + increment
|
||||
);
|
||||
|
||||
const handleLike = async () => {
|
||||
addOptimisticLike(1); // 立即更新 UI
|
||||
try {
|
||||
await likePost(postId); // 后台同步
|
||||
} catch {
|
||||
// React 自动回滚到 likes 原值
|
||||
}
|
||||
};
|
||||
|
||||
return <button onClick={handleLike}>{optimisticLikes} likes</button>;
|
||||
}
|
||||
```
|
||||
|
||||
### Server Actions (Next.js 15+)
|
||||
|
||||
```tsx
|
||||
// ❌ 客户端调用 API
|
||||
'use client';
|
||||
function ClientForm() {
|
||||
const handleSubmit = async (formData: FormData) => {
|
||||
const res = await fetch('/api/submit', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
// ...
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ Server Action + useActionState
|
||||
// actions.ts
|
||||
'use server';
|
||||
export async function createPost(prevState: any, formData: FormData) {
|
||||
const title = formData.get('title');
|
||||
await db.posts.create({ title });
|
||||
revalidatePath('/posts');
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// form.tsx
|
||||
'use client';
|
||||
import { createPost } from './actions';
|
||||
|
||||
function PostForm() {
|
||||
const [state, formAction, isPending] = useActionState(createPost, null);
|
||||
return (
|
||||
<form action={formAction}>
|
||||
<input name="title" />
|
||||
<SubmitButton />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Suspense & Streaming SSR
|
||||
|
||||
Suspense 和 Streaming 是 React 18+ 的核心特性,在 2025 年的 Next.js 15 等框架中广泛使用。
|
||||
|
||||
### 基础 Suspense
|
||||
|
||||
```tsx
|
||||
// ❌ 传统加载状态管理
|
||||
function OldComponent() {
|
||||
const [data, setData] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData().then(setData).finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
if (isLoading) return <Spinner />;
|
||||
return <DataView data={data} />;
|
||||
}
|
||||
|
||||
// ✅ Suspense 声明式加载状态
|
||||
function NewComponent() {
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<DataView /> {/* 内部使用 use() 或支持 Suspense 的数据获取 */}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 多个独立 Suspense 边界
|
||||
|
||||
```tsx
|
||||
// ❌ 单一边界——所有内容一起加载
|
||||
function BadLayout() {
|
||||
return (
|
||||
<Suspense fallback={<FullPageSpinner />}>
|
||||
<Header />
|
||||
<MainContent /> {/* 慢 */}
|
||||
<Sidebar /> {/* 快 */}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ 独立边界——各部分独立流式传输
|
||||
function GoodLayout() {
|
||||
return (
|
||||
<>
|
||||
<Header /> {/* 立即显示 */}
|
||||
<div className="flex">
|
||||
<Suspense fallback={<ContentSkeleton />}>
|
||||
<MainContent /> {/* 独立加载 */}
|
||||
</Suspense>
|
||||
<Suspense fallback={<SidebarSkeleton />}>
|
||||
<Sidebar /> {/* 独立加载 */}
|
||||
</Suspense>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Next.js 15 Streaming
|
||||
|
||||
```tsx
|
||||
// app/page.tsx - 自动 Streaming
|
||||
export default async function Page() {
|
||||
// 这个 await 不会阻塞整个页面
|
||||
const data = await fetchSlowData();
|
||||
return <div>{data}</div>;
|
||||
}
|
||||
|
||||
// app/loading.tsx - 自动 Suspense 边界
|
||||
export default function Loading() {
|
||||
return <Skeleton />;
|
||||
}
|
||||
```
|
||||
|
||||
### use() Hook (React 19)
|
||||
|
||||
```tsx
|
||||
// ✅ 在组件中读取 Promise
|
||||
import { use } from 'react';
|
||||
|
||||
function Comments({ commentsPromise }) {
|
||||
const comments = use(commentsPromise); // 自动触发 Suspense
|
||||
return (
|
||||
<ul>
|
||||
{comments.map(c => <li key={c.id}>{c.text}</li>)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
// 父组件创建 Promise,子组件消费
|
||||
function Post({ postId }) {
|
||||
const commentsPromise = fetchComments(postId); // 不 await
|
||||
return (
|
||||
<article>
|
||||
<PostContent id={postId} />
|
||||
<Suspense fallback={<CommentsSkeleton />}>
|
||||
<Comments commentsPromise={commentsPromise} />
|
||||
</Suspense>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TanStack Query v5
|
||||
|
||||
TanStack Query 是 React 生态中最流行的数据获取库,v5 是当前稳定版本。
|
||||
|
||||
### 基础配置
|
||||
|
||||
```tsx
|
||||
// ❌ 不正确的默认配置
|
||||
const queryClient = new QueryClient(); // 默认配置可能不适合
|
||||
|
||||
// ✅ 生产环境推荐配置
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 5, // 5 分钟内数据视为新鲜
|
||||
gcTime: 1000 * 60 * 30, // 30 分钟后垃圾回收(v5 重命名)
|
||||
retry: 3,
|
||||
refetchOnWindowFocus: false, // 根据需求决定
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### queryOptions (v5 新增)
|
||||
|
||||
```tsx
|
||||
// ❌ 重复定义 queryKey 和 queryFn
|
||||
function Component1() {
|
||||
const { data } = useQuery({
|
||||
queryKey: ['users', userId],
|
||||
queryFn: () => fetchUser(userId),
|
||||
});
|
||||
}
|
||||
|
||||
function prefetchUser(queryClient, userId) {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['users', userId], // 重复!
|
||||
queryFn: () => fetchUser(userId), // 重复!
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ queryOptions 统一定义,类型安全
|
||||
import { queryOptions } from '@tanstack/react-query';
|
||||
|
||||
const userQueryOptions = (userId: string) =>
|
||||
queryOptions({
|
||||
queryKey: ['users', userId],
|
||||
queryFn: () => fetchUser(userId),
|
||||
});
|
||||
|
||||
function Component1({ userId }) {
|
||||
const { data } = useQuery(userQueryOptions(userId));
|
||||
}
|
||||
|
||||
function prefetchUser(queryClient, userId) {
|
||||
queryClient.prefetchQuery(userQueryOptions(userId));
|
||||
}
|
||||
|
||||
// getQueryData 也是类型安全的
|
||||
const user = queryClient.getQueryData(userQueryOptions(userId).queryKey);
|
||||
```
|
||||
|
||||
### 常见陷阱
|
||||
|
||||
```tsx
|
||||
// ❌ staleTime 为 0 导致过度请求
|
||||
useQuery({
|
||||
queryKey: ['data'],
|
||||
queryFn: fetchData,
|
||||
// staleTime 默认为 0,每次组件挂载都会 refetch
|
||||
});
|
||||
|
||||
// ✅ 设置合理的 staleTime
|
||||
useQuery({
|
||||
queryKey: ['data'],
|
||||
queryFn: fetchData,
|
||||
staleTime: 1000 * 60, // 1 分钟内不会重新请求
|
||||
});
|
||||
|
||||
// ❌ 在 queryFn 中使用不稳定的引用
|
||||
function BadQuery({ filters }) {
|
||||
useQuery({
|
||||
queryKey: ['items'], // queryKey 没有包含 filters!
|
||||
queryFn: () => fetchItems(filters), // filters 变化不会触发重新请求
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ queryKey 包含所有影响数据的参数
|
||||
function GoodQuery({ filters }) {
|
||||
useQuery({
|
||||
queryKey: ['items', filters], // filters 是 queryKey 的一部分
|
||||
queryFn: () => fetchItems(filters),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### useSuspenseQuery
|
||||
|
||||
> **重要限制**:useSuspenseQuery 与 useQuery 有显著差异,选择前需了解其限制。
|
||||
|
||||
#### useSuspenseQuery 的限制
|
||||
|
||||
| 特性 | useQuery | useSuspenseQuery |
|
||||
|------|----------|------------------|
|
||||
| `enabled` 选项 | ✅ 支持 | ❌ 不支持 |
|
||||
| `placeholderData` | ✅ 支持 | ❌ 不支持 |
|
||||
| `data` 类型 | `T \| undefined` | `T`(保证有值)|
|
||||
| 错误处理 | `error` 属性 | 抛出到 Error Boundary |
|
||||
| 加载状态 | `isLoading` 属性 | 挂起到 Suspense |
|
||||
|
||||
#### 不支持 enabled 的替代方案
|
||||
|
||||
```tsx
|
||||
// ❌ 使用 useQuery + enabled 实现条件查询
|
||||
function BadSuspenseQuery({ userId }) {
|
||||
const { data } = useSuspenseQuery({
|
||||
queryKey: ['user', userId],
|
||||
queryFn: () => fetchUser(userId),
|
||||
enabled: !!userId, // useSuspenseQuery 不支持 enabled!
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 组件组合实现条件渲染
|
||||
function GoodSuspenseQuery({ userId }) {
|
||||
// useSuspenseQuery 保证 data 是 T 不是 T | undefined
|
||||
const { data } = useSuspenseQuery({
|
||||
queryKey: ['user', userId],
|
||||
queryFn: () => fetchUser(userId),
|
||||
});
|
||||
return <UserProfile user={data} />;
|
||||
}
|
||||
|
||||
function Parent({ userId }) {
|
||||
if (!userId) return <NoUserSelected />;
|
||||
return (
|
||||
<Suspense fallback={<UserSkeleton />}>
|
||||
<GoodSuspenseQuery userId={userId} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误处理差异
|
||||
|
||||
```tsx
|
||||
// ❌ useSuspenseQuery 没有 error 属性
|
||||
function BadErrorHandling() {
|
||||
const { data, error } = useSuspenseQuery({...});
|
||||
if (error) return <Error />; // error 总是 null!
|
||||
}
|
||||
|
||||
// ✅ 使用 Error Boundary 处理错误
|
||||
function GoodErrorHandling() {
|
||||
return (
|
||||
<ErrorBoundary fallback={<ErrorMessage />}>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<DataComponent />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
function DataComponent() {
|
||||
// 错误会抛出到 Error Boundary
|
||||
const { data } = useSuspenseQuery({
|
||||
queryKey: ['data'],
|
||||
queryFn: fetchData,
|
||||
});
|
||||
return <Display data={data} />;
|
||||
}
|
||||
```
|
||||
|
||||
#### 何时选择 useSuspenseQuery
|
||||
|
||||
```tsx
|
||||
// ✅ 适合场景:
|
||||
// 1. 数据总是需要的(无条件查询)
|
||||
// 2. 组件必须有数据才能渲染
|
||||
// 3. 使用 React 19 的 Suspense 模式
|
||||
// 4. 服务端组件 + 客户端 hydration
|
||||
|
||||
// ❌ 不适合场景:
|
||||
// 1. 条件查询(根据用户操作触发)
|
||||
// 2. 需要 placeholderData 或初始数据
|
||||
// 3. 需要在组件内处理 loading/error 状态
|
||||
// 4. 多个查询有依赖关系
|
||||
|
||||
// ✅ 多个独立查询用 useSuspenseQueries
|
||||
function MultipleQueries({ userId }) {
|
||||
const [userQuery, postsQuery] = useSuspenseQueries({
|
||||
queries: [
|
||||
{ queryKey: ['user', userId], queryFn: () => fetchUser(userId) },
|
||||
{ queryKey: ['posts', userId], queryFn: () => fetchPosts(userId) },
|
||||
],
|
||||
});
|
||||
// 两个查询并行执行,都完成后组件渲染
|
||||
return <Profile user={userQuery.data} posts={postsQuery.data} />;
|
||||
}
|
||||
```
|
||||
|
||||
### 乐观更新 (v5 简化)
|
||||
|
||||
```tsx
|
||||
// ❌ 手动管理缓存的乐观更新(复杂)
|
||||
const mutation = useMutation({
|
||||
mutationFn: updateTodo,
|
||||
onMutate: async (newTodo) => {
|
||||
await queryClient.cancelQueries({ queryKey: ['todos'] });
|
||||
const previousTodos = queryClient.getQueryData(['todos']);
|
||||
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
|
||||
return { previousTodos };
|
||||
},
|
||||
onError: (err, newTodo, context) => {
|
||||
queryClient.setQueryData(['todos'], context.previousTodos);
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['todos'] });
|
||||
},
|
||||
});
|
||||
|
||||
// ✅ v5 简化:使用 variables 进行乐观 UI
|
||||
function TodoList() {
|
||||
const { data: todos } = useQuery(todosQueryOptions);
|
||||
const { mutate, variables, isPending } = useMutation({
|
||||
mutationFn: addTodo,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['todos'] });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{todos?.map(todo => <TodoItem key={todo.id} todo={todo} />)}
|
||||
{/* 乐观显示正在添加的 todo */}
|
||||
{isPending && <TodoItem todo={variables} isOptimistic />}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### v5 状态字段变化
|
||||
|
||||
```tsx
|
||||
// v4: isLoading 表示首次加载或后续获取
|
||||
// v5: isPending 表示没有数据,isLoading = isPending && isFetching
|
||||
|
||||
const { data, isPending, isFetching, isLoading } = useQuery({...});
|
||||
|
||||
// isPending: 缓存中没有数据(首次加载)
|
||||
// isFetching: 正在请求中(包括后台刷新)
|
||||
// isLoading: isPending && isFetching(首次加载中)
|
||||
|
||||
// ❌ v4 代码直接迁移
|
||||
if (isLoading) return <Spinner />; // v5 中行为可能不同
|
||||
|
||||
// ✅ 明确意图
|
||||
if (isPending) return <Spinner />; // 没有数据时显示加载
|
||||
// 或
|
||||
if (isLoading) return <Spinner />; // 首次加载中
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklists
|
||||
|
||||
### Hooks 规则
|
||||
|
||||
- [ ] Hooks 在组件/自定义 Hook 顶层调用
|
||||
- [ ] 没有条件/循环中调用 Hooks
|
||||
- [ ] useEffect 依赖数组完整
|
||||
- [ ] useEffect 有清理函数(订阅/定时器/请求)
|
||||
- [ ] 没有用 useEffect 计算派生状态
|
||||
|
||||
### 性能优化(适度原则)
|
||||
|
||||
- [ ] useMemo/useCallback 只用于真正需要的场景
|
||||
- [ ] React.memo 配合稳定的 props 引用
|
||||
- [ ] 没有在组件内定义子组件
|
||||
- [ ] 没有在 JSX 中创建新对象/函数(除非传给非 memo 组件)
|
||||
- [ ] 长列表使用虚拟化(react-window/react-virtual)
|
||||
|
||||
### 组件设计
|
||||
|
||||
- [ ] 组件职责单一,不超过 200 行
|
||||
- [ ] 逻辑与展示分离(Custom Hooks)
|
||||
- [ ] Props 接口清晰,使用 TypeScript
|
||||
- [ ] 避免 Props Drilling(考虑 Context 或组合)
|
||||
|
||||
### 状态管理
|
||||
|
||||
- [ ] 状态就近原则(最小必要范围)
|
||||
- [ ] 复杂状态用 useReducer
|
||||
- [ ] 全局状态用 Context 或状态库
|
||||
- [ ] 避免不必要的状态(派生 > 存储)
|
||||
|
||||
### 错误处理
|
||||
|
||||
- [ ] 关键区域有 Error Boundary
|
||||
- [ ] Suspense 配合 Error Boundary 使用
|
||||
- [ ] 异步操作有错误处理
|
||||
|
||||
### Server Components (RSC)
|
||||
|
||||
- [ ] 'use client' 只用于需要交互的组件
|
||||
- [ ] Server Component 不使用 Hooks/事件处理
|
||||
- [ ] 客户端组件尽量放在叶子节点
|
||||
- [ ] 数据获取在 Server Component 中进行
|
||||
|
||||
### React 19 Forms
|
||||
|
||||
- [ ] 使用 useActionState 替代多个 useState
|
||||
- [ ] useFormStatus 在 form 子组件中调用
|
||||
- [ ] useOptimistic 不用于关键业务(支付等)
|
||||
- [ ] Server Action 正确标记 'use server'
|
||||
|
||||
### Suspense & Streaming
|
||||
|
||||
- [ ] 按用户体验需求划分 Suspense 边界
|
||||
- [ ] 每个 Suspense 有对应的 Error Boundary
|
||||
- [ ] 提供有意义的 fallback(骨架屏 > Spinner)
|
||||
- [ ] 避免在 layout 层级 await 慢数据
|
||||
|
||||
### TanStack Query
|
||||
|
||||
- [ ] queryKey 包含所有影响数据的参数
|
||||
- [ ] 设置合理的 staleTime(不是默认 0)
|
||||
- [ ] useSuspenseQuery 不使用 enabled
|
||||
- [ ] Mutation 成功后 invalidate 相关查询
|
||||
- [ ] 理解 isPending vs isLoading 区别
|
||||
|
||||
### 测试
|
||||
|
||||
- [ ] 使用 @testing-library/react
|
||||
- [ ] 用 screen 查询元素
|
||||
- [ ] 用 userEvent 代替 fireEvent
|
||||
- [ ] 优先使用 *ByRole 查询
|
||||
- [ ] 测试行为而非实现细节
|
||||
@@ -0,0 +1,842 @@
|
||||
# Rust Code Review Guide
|
||||
|
||||
> Rust 代码审查指南。编译器能捕获内存安全问题,但审查者需要关注编译器无法检测的问题——业务逻辑、API 设计、性能、取消安全性和可维护性。
|
||||
|
||||
## 目录
|
||||
|
||||
- [所有权与借用](#所有权与借用)
|
||||
- [Unsafe 代码审查](#unsafe-代码审查最关键)
|
||||
- [异步代码](#异步代码)
|
||||
- [取消安全性](#取消安全性)
|
||||
- [spawn vs await](#spawn-vs-await)
|
||||
- [错误处理](#错误处理)
|
||||
- [性能](#性能)
|
||||
- [Trait 设计](#trait-设计)
|
||||
- [Review Checklist](#rust-review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## 所有权与借用
|
||||
|
||||
### 避免不必要的 clone()
|
||||
|
||||
```rust
|
||||
// ❌ clone() 是"Rust 的胶带"——用于绕过借用检查器
|
||||
fn bad_process(data: &Data) -> Result<()> {
|
||||
let owned = data.clone(); // 为什么需要 clone?
|
||||
expensive_operation(owned)
|
||||
}
|
||||
|
||||
// ✅ 审查时问:clone 是否必要?能否用借用?
|
||||
fn good_process(data: &Data) -> Result<()> {
|
||||
expensive_operation(data) // 传递引用
|
||||
}
|
||||
|
||||
// ✅ 如果确实需要 clone,添加注释说明原因
|
||||
fn justified_clone(data: &Data) -> Result<()> {
|
||||
// Clone needed: data will be moved to spawned task
|
||||
let owned = data.clone();
|
||||
tokio::spawn(async move {
|
||||
process(owned).await
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Arc<Mutex<T>> 的使用
|
||||
|
||||
```rust
|
||||
// ❌ Arc<Mutex<T>> 可能隐藏不必要的共享状态
|
||||
struct BadService {
|
||||
cache: Arc<Mutex<HashMap<String, Data>>>, // 真的需要共享?
|
||||
}
|
||||
|
||||
// ✅ 考虑是否需要共享,或者设计可以避免
|
||||
struct GoodService {
|
||||
cache: HashMap<String, Data>, // 单一所有者
|
||||
}
|
||||
|
||||
// ✅ 如果确实需要并发访问,考虑更好的数据结构
|
||||
use dashmap::DashMap;
|
||||
|
||||
struct ConcurrentService {
|
||||
cache: DashMap<String, Data>, // 更细粒度的锁
|
||||
}
|
||||
```
|
||||
|
||||
### Cow (Copy-on-Write) 模式
|
||||
|
||||
```rust
|
||||
use std::borrow::Cow;
|
||||
|
||||
// ❌ 总是分配新字符串
|
||||
fn bad_process_name(name: &str) -> String {
|
||||
if name.is_empty() {
|
||||
"Unknown".to_string() // 分配
|
||||
} else {
|
||||
name.to_string() // 不必要的分配
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 使用 Cow 避免不必要的分配
|
||||
fn good_process_name(name: &str) -> Cow<'_, str> {
|
||||
if name.is_empty() {
|
||||
Cow::Borrowed("Unknown") // 静态字符串,无分配
|
||||
} else {
|
||||
Cow::Borrowed(name) // 借用原始数据
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 只在需要修改时才分配
|
||||
fn normalize_name(name: &str) -> Cow<'_, str> {
|
||||
if name.chars().any(|c| c.is_uppercase()) {
|
||||
Cow::Owned(name.to_lowercase()) // 需要修改,分配
|
||||
} else {
|
||||
Cow::Borrowed(name) // 无需修改,借用
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Unsafe 代码审查(最关键!)
|
||||
|
||||
### 基本要求
|
||||
|
||||
```rust
|
||||
// ❌ unsafe 没有安全文档——这是红旗
|
||||
unsafe fn bad_transmute<T, U>(t: T) -> U {
|
||||
std::mem::transmute(t)
|
||||
}
|
||||
|
||||
// ✅ 每个 unsafe 必须解释:为什么安全?什么不变量?
|
||||
/// Transmutes `T` to `U`.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// - `T` and `U` must have the same size and alignment
|
||||
/// - `T` must be a valid bit pattern for `U`
|
||||
/// - The caller ensures no references to `t` exist after this call
|
||||
unsafe fn documented_transmute<T, U>(t: T) -> U {
|
||||
// SAFETY: Caller guarantees size/alignment match and bit validity
|
||||
std::mem::transmute(t)
|
||||
}
|
||||
```
|
||||
|
||||
### Unsafe 块注释
|
||||
|
||||
```rust
|
||||
// ❌ 没有解释的 unsafe 块
|
||||
fn bad_get_unchecked(slice: &[u8], index: usize) -> u8 {
|
||||
unsafe { *slice.get_unchecked(index) }
|
||||
}
|
||||
|
||||
// ✅ 每个 unsafe 块必须有 SAFETY 注释
|
||||
fn good_get_unchecked(slice: &[u8], index: usize) -> u8 {
|
||||
debug_assert!(index < slice.len(), "index out of bounds");
|
||||
// SAFETY: We verified index < slice.len() via debug_assert.
|
||||
// In release builds, callers must ensure valid index.
|
||||
unsafe { *slice.get_unchecked(index) }
|
||||
}
|
||||
|
||||
// ✅ 封装 unsafe 提供安全 API
|
||||
pub fn checked_get(slice: &[u8], index: usize) -> Option<u8> {
|
||||
if index < slice.len() {
|
||||
// SAFETY: bounds check performed above
|
||||
Some(unsafe { *slice.get_unchecked(index) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 常见 unsafe 模式
|
||||
|
||||
```rust
|
||||
// ✅ FFI 边界
|
||||
extern "C" {
|
||||
fn external_function(ptr: *const u8, len: usize) -> i32;
|
||||
}
|
||||
|
||||
pub fn safe_wrapper(data: &[u8]) -> Result<i32, Error> {
|
||||
// SAFETY: data.as_ptr() is valid for data.len() bytes,
|
||||
// and external_function only reads from the buffer.
|
||||
let result = unsafe {
|
||||
external_function(data.as_ptr(), data.len())
|
||||
};
|
||||
if result < 0 {
|
||||
Err(Error::from_code(result))
|
||||
} else {
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 性能关键路径的 unsafe
|
||||
pub fn fast_copy(src: &[u8], dst: &mut [u8]) {
|
||||
assert_eq!(src.len(), dst.len(), "slices must be equal length");
|
||||
// SAFETY: src and dst are valid slices of equal length,
|
||||
// and dst is mutable so no aliasing.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
src.as_ptr(),
|
||||
dst.as_mut_ptr(),
|
||||
src.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 异步代码
|
||||
|
||||
### 避免阻塞操作
|
||||
|
||||
```rust
|
||||
// ❌ 在 async 上下文中阻塞——会饿死其他任务
|
||||
async fn bad_async() {
|
||||
let data = std::fs::read_to_string("file.txt").unwrap(); // 阻塞!
|
||||
std::thread::sleep(Duration::from_secs(1)); // 阻塞!
|
||||
}
|
||||
|
||||
// ✅ 使用异步 API
|
||||
async fn good_async() -> Result<String> {
|
||||
let data = tokio::fs::read_to_string("file.txt").await?;
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
// ✅ 如果必须使用阻塞操作,用 spawn_blocking
|
||||
async fn with_blocking() -> Result<Data> {
|
||||
let result = tokio::task::spawn_blocking(|| {
|
||||
// 这里可以安全地进行阻塞操作
|
||||
expensive_cpu_computation()
|
||||
}).await?;
|
||||
Ok(result)
|
||||
}
|
||||
```
|
||||
|
||||
### Mutex 和 .await
|
||||
|
||||
```rust
|
||||
// ❌ 跨 .await 持有 std::sync::Mutex——可能死锁
|
||||
async fn bad_lock(mutex: &std::sync::Mutex<Data>) {
|
||||
let guard = mutex.lock().unwrap();
|
||||
async_operation().await; // 持锁等待!
|
||||
process(&guard);
|
||||
}
|
||||
|
||||
// ✅ 方案1:最小化锁范围
|
||||
async fn good_lock_scoped(mutex: &std::sync::Mutex<Data>) {
|
||||
let data = {
|
||||
let guard = mutex.lock().unwrap();
|
||||
guard.clone() // 立即释放锁
|
||||
};
|
||||
async_operation().await;
|
||||
process(&data);
|
||||
}
|
||||
|
||||
// ✅ 方案2:使用 tokio::sync::Mutex(可跨 await)
|
||||
async fn good_lock_tokio(mutex: &tokio::sync::Mutex<Data>) {
|
||||
let guard = mutex.lock().await;
|
||||
async_operation().await; // OK: tokio Mutex 设计为可跨 await
|
||||
process(&guard);
|
||||
}
|
||||
|
||||
// 💡 选择指南:
|
||||
// - std::sync::Mutex:低竞争、短临界区、不跨 await
|
||||
// - tokio::sync::Mutex:需要跨 await、高竞争场景
|
||||
```
|
||||
|
||||
### 异步 trait 方法
|
||||
|
||||
```rust
|
||||
// ❌ async trait 方法的陷阱(旧版本)
|
||||
#[async_trait]
|
||||
trait BadRepository {
|
||||
async fn find(&self, id: i64) -> Option<Entity>; // 隐式 Box
|
||||
}
|
||||
|
||||
// ✅ Rust 1.75+:原生 async trait 方法
|
||||
trait Repository {
|
||||
async fn find(&self, id: i64) -> Option<Entity>;
|
||||
|
||||
// 返回具体 Future 类型以避免 allocation
|
||||
fn find_many(&self, ids: &[i64]) -> impl Future<Output = Vec<Entity>> + Send;
|
||||
}
|
||||
|
||||
// ✅ 对于需要 dyn 的场景
|
||||
trait DynRepository: Send + Sync {
|
||||
fn find(&self, id: i64) -> Pin<Box<dyn Future<Output = Option<Entity>> + Send + '_>>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 取消安全性
|
||||
|
||||
### 什么是取消安全
|
||||
|
||||
```rust
|
||||
// 当一个 Future 在 .await 点被 drop 时,它处于什么状态?
|
||||
// 取消安全的 Future:可以在任何 await 点安全取消
|
||||
// 取消不安全的 Future:取消可能导致数据丢失或不一致状态
|
||||
|
||||
// ❌ 取消不安全的例子
|
||||
async fn cancel_unsafe(conn: &mut Connection) -> Result<()> {
|
||||
let data = receive_data().await; // 如果这里被取消...
|
||||
conn.send_ack().await; // ...确认永远不会发送,数据可能丢失
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ✅ 取消安全的版本
|
||||
async fn cancel_safe(conn: &mut Connection) -> Result<()> {
|
||||
// 使用事务或原子操作确保一致性
|
||||
let transaction = conn.begin_transaction().await?;
|
||||
let data = receive_data().await;
|
||||
transaction.commit_with_ack(data).await?; // 原子操作
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### select! 中的取消安全
|
||||
|
||||
```rust
|
||||
use tokio::select;
|
||||
|
||||
// ❌ 在 select! 中使用取消不安全的 Future
|
||||
async fn bad_select(stream: &mut TcpStream) {
|
||||
let mut buffer = vec![0u8; 1024];
|
||||
loop {
|
||||
select! {
|
||||
// read_exact 不是取消安全的:timeout 先完成时,
|
||||
// 已经读进 buffer 的部分字节会随 Future 一起丢弃
|
||||
result = stream.read_exact(&mut buffer) => {
|
||||
result?;
|
||||
handle_data(&buffer);
|
||||
}
|
||||
_ = tokio::time::sleep(Duration::from_secs(5)) => {
|
||||
println!("Timeout");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 使用取消安全的 API
|
||||
async fn good_select(stream: &mut TcpStream) {
|
||||
let mut buffer = vec![0u8; 1024];
|
||||
loop {
|
||||
select! {
|
||||
// read 是取消安全的:被取消时未读取的数据仍留在流中
|
||||
// 真的需要按定长读取时,把 read_exact 丢到单独的 task 里,
|
||||
// 这里 select! 它的 JoinHandle,取消就不会丢字节
|
||||
result = stream.read(&mut buffer) => {
|
||||
match result {
|
||||
Ok(0) => break, // EOF
|
||||
Ok(n) => handle_data(&buffer[..n]),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
_ = tokio::time::sleep(Duration::from_secs(5)) => {
|
||||
println!("Timeout, retrying...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 使用 tokio::pin! 确保 Future 可以安全重用
|
||||
async fn pinned_select() {
|
||||
let sleep = tokio::time::sleep(Duration::from_secs(10));
|
||||
tokio::pin!(sleep);
|
||||
|
||||
loop {
|
||||
select! {
|
||||
_ = &mut sleep => {
|
||||
println!("Timer elapsed");
|
||||
break;
|
||||
}
|
||||
data = receive_data() => {
|
||||
process(data).await;
|
||||
// sleep 继续倒计时,不会重置
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 文档化取消安全性
|
||||
|
||||
```rust
|
||||
/// Reads a complete message from the stream.
|
||||
///
|
||||
/// # Cancel Safety
|
||||
///
|
||||
/// This method is **not** cancel safe. If cancelled while reading,
|
||||
/// partial data may be lost and the stream state becomes undefined.
|
||||
/// Use `read_message_cancel_safe` if cancellation is expected.
|
||||
async fn read_message(stream: &mut TcpStream) -> Result<Message> {
|
||||
let len = stream.read_u32().await?;
|
||||
let mut buffer = vec![0u8; len as usize];
|
||||
stream.read_exact(&mut buffer).await?;
|
||||
Ok(Message::from_bytes(&buffer))
|
||||
}
|
||||
|
||||
/// Reads a message with cancel safety.
|
||||
///
|
||||
/// # Cancel Safety
|
||||
///
|
||||
/// This method is cancel safe. If cancelled, any partial data
|
||||
/// is preserved in the internal buffer for the next call.
|
||||
async fn read_message_cancel_safe(reader: &mut BufferedReader) -> Result<Message> {
|
||||
reader.read_message_buffered().await
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## spawn vs await
|
||||
|
||||
### 何时使用 spawn
|
||||
|
||||
```rust
|
||||
// ❌ 不必要的 spawn——增加开销,失去结构化并发
|
||||
async fn bad_unnecessary_spawn() {
|
||||
let handle = tokio::spawn(async {
|
||||
simple_operation().await
|
||||
});
|
||||
handle.await.unwrap(); // 为什么不直接 await?
|
||||
}
|
||||
|
||||
// ✅ 直接 await 简单操作
|
||||
async fn good_direct_await() {
|
||||
simple_operation().await;
|
||||
}
|
||||
|
||||
// ✅ spawn 用于真正的并行执行
|
||||
async fn good_parallel_spawn() {
|
||||
let task1 = tokio::spawn(fetch_from_service_a());
|
||||
let task2 = tokio::spawn(fetch_from_service_b());
|
||||
|
||||
// 两个请求并行执行
|
||||
let (result1, result2) = tokio::try_join!(task1, task2)?;
|
||||
}
|
||||
|
||||
// ✅ spawn 用于后台任务(fire-and-forget)
|
||||
async fn good_background_spawn() {
|
||||
// 启动后台任务,不等待完成
|
||||
tokio::spawn(async {
|
||||
cleanup_old_sessions().await;
|
||||
log_metrics().await;
|
||||
});
|
||||
|
||||
// 继续执行其他工作
|
||||
handle_request().await;
|
||||
}
|
||||
```
|
||||
|
||||
### spawn 的 'static 要求
|
||||
|
||||
```rust
|
||||
// ❌ spawn 的 Future 必须是 'static
|
||||
async fn bad_spawn_borrow(data: &Data) {
|
||||
tokio::spawn(async {
|
||||
process(data).await; // Error: `data` 不是 'static
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 方案1:克隆数据
|
||||
async fn good_spawn_clone(data: &Data) {
|
||||
let owned = data.clone();
|
||||
tokio::spawn(async move {
|
||||
process(&owned).await;
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 方案2:使用 Arc 共享
|
||||
async fn good_spawn_arc(data: Arc<Data>) {
|
||||
let data = Arc::clone(&data);
|
||||
tokio::spawn(async move {
|
||||
process(&data).await;
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 方案3:使用作用域任务(tokio-scoped 或 async-scoped)
|
||||
async fn good_scoped_spawn(data: &Data) {
|
||||
// 假设使用 async-scoped crate
|
||||
async_scoped::scope(|s| async {
|
||||
s.spawn(async {
|
||||
process(data).await; // 可以借用
|
||||
});
|
||||
}).await;
|
||||
}
|
||||
```
|
||||
|
||||
### JoinHandle 错误处理
|
||||
|
||||
```rust
|
||||
// ❌ 忽略 spawn 的错误
|
||||
async fn bad_ignore_spawn_error() {
|
||||
let handle = tokio::spawn(async {
|
||||
risky_operation().await
|
||||
});
|
||||
let _ = handle.await; // 忽略了 panic 和错误
|
||||
}
|
||||
|
||||
// ✅ 正确处理 JoinHandle 结果
|
||||
async fn good_handle_spawn_error() -> Result<()> {
|
||||
let handle = tokio::spawn(async {
|
||||
risky_operation().await
|
||||
});
|
||||
|
||||
match handle.await {
|
||||
Ok(Ok(result)) => {
|
||||
// 任务成功完成
|
||||
process_result(result);
|
||||
Ok(())
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
// 任务内部错误
|
||||
Err(e.into())
|
||||
}
|
||||
Err(join_err) => {
|
||||
// 任务 panic 或被取消
|
||||
if join_err.is_panic() {
|
||||
error!("Task panicked: {:?}", join_err);
|
||||
}
|
||||
Err(anyhow!("Task failed: {}", join_err))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 结构化并发 vs spawn
|
||||
|
||||
```rust
|
||||
// ✅ 优先使用 join!(结构化并发)
|
||||
async fn structured_concurrency() -> Result<(A, B, C)> {
|
||||
// 所有任务在同一个作用域内
|
||||
// 如果任何一个失败,其他的会被取消
|
||||
tokio::try_join!(
|
||||
fetch_a(),
|
||||
fetch_b(),
|
||||
fetch_c()
|
||||
)
|
||||
}
|
||||
|
||||
// ✅ 使用 spawn 时考虑任务生命周期
|
||||
struct TaskManager {
|
||||
handles: Vec<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl TaskManager {
|
||||
async fn shutdown(self) {
|
||||
// 优雅关闭:等待所有任务完成
|
||||
for handle in self.handles {
|
||||
if let Err(e) = handle.await {
|
||||
error!("Task failed during shutdown: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn abort_all(self) {
|
||||
// 强制关闭:取消所有任务
|
||||
for handle in self.handles {
|
||||
handle.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 库 vs 应用的错误类型
|
||||
|
||||
```rust
|
||||
// ❌ 库代码用 anyhow——调用者无法 match 错误
|
||||
pub fn parse_config(s: &str) -> anyhow::Result<Config> { ... }
|
||||
|
||||
// ✅ 库用 thiserror,应用用 anyhow
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ConfigError {
|
||||
#[error("invalid syntax at line {line}: {message}")]
|
||||
Syntax { line: usize, message: String },
|
||||
#[error("missing required field: {0}")]
|
||||
MissingField(String),
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
pub fn parse_config(s: &str) -> Result<Config, ConfigError> { ... }
|
||||
```
|
||||
|
||||
### 保留错误上下文
|
||||
|
||||
```rust
|
||||
// ❌ 吞掉错误上下文
|
||||
fn bad_error() -> Result<()> {
|
||||
operation().map_err(|_| anyhow!("failed"))?; // 原始错误丢失
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ✅ 使用 context 保留错误链
|
||||
fn good_error() -> Result<()> {
|
||||
operation().context("failed to perform operation")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ✅ 使用 with_context 进行懒计算
|
||||
fn good_error_lazy() -> Result<()> {
|
||||
operation()
|
||||
.with_context(|| format!("failed to process file: {}", filename))?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 错误类型设计
|
||||
|
||||
```rust
|
||||
// ✅ 使用 #[source] 保留错误链
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ServiceError {
|
||||
#[error("database error")]
|
||||
Database(#[source] sqlx::Error),
|
||||
|
||||
#[error("network error: {message}")]
|
||||
Network {
|
||||
message: String,
|
||||
#[source]
|
||||
source: reqwest::Error,
|
||||
},
|
||||
|
||||
#[error("validation failed: {0}")]
|
||||
Validation(String),
|
||||
}
|
||||
|
||||
// ✅ 为常见转换实现 From
|
||||
impl From<sqlx::Error> for ServiceError {
|
||||
fn from(err: sqlx::Error) -> Self {
|
||||
ServiceError::Database(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能
|
||||
|
||||
### 避免不必要的 collect()
|
||||
|
||||
```rust
|
||||
// ❌ 不必要的 collect——中间分配
|
||||
fn bad_sum(items: &[i32]) -> i32 {
|
||||
items.iter()
|
||||
.filter(|x| **x > 0)
|
||||
.collect::<Vec<_>>() // 不必要!
|
||||
.iter()
|
||||
.sum()
|
||||
}
|
||||
|
||||
// ✅ 惰性迭代
|
||||
fn good_sum(items: &[i32]) -> i32 {
|
||||
items.iter().filter(|x| **x > 0).copied().sum()
|
||||
}
|
||||
```
|
||||
|
||||
### 字符串拼接
|
||||
|
||||
```rust
|
||||
// ❌ 字符串拼接在循环中重复分配
|
||||
fn bad_concat(items: &[&str]) -> String {
|
||||
let mut s = String::new();
|
||||
for item in items {
|
||||
s = s + item; // 每次都重新分配!
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
// ✅ 预分配或用 join
|
||||
fn good_concat(items: &[&str]) -> String {
|
||||
items.join("")
|
||||
}
|
||||
|
||||
// ✅ 使用 with_capacity 预分配
|
||||
fn good_concat_capacity(items: &[&str]) -> String {
|
||||
let total_len: usize = items.iter().map(|s| s.len()).sum();
|
||||
let mut result = String::with_capacity(total_len);
|
||||
for item in items {
|
||||
result.push_str(item);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// ✅ 使用 write! 宏
|
||||
use std::fmt::Write;
|
||||
|
||||
fn good_concat_write(items: &[&str]) -> String {
|
||||
let mut result = String::new();
|
||||
for item in items {
|
||||
write!(result, "{}", item).unwrap();
|
||||
}
|
||||
result
|
||||
}
|
||||
```
|
||||
|
||||
### 避免不必要的分配
|
||||
|
||||
```rust
|
||||
// ❌ 不必要的 Vec 分配
|
||||
fn bad_check_any(items: &[Item]) -> bool {
|
||||
let filtered: Vec<_> = items.iter()
|
||||
.filter(|i| i.is_valid())
|
||||
.collect();
|
||||
!filtered.is_empty()
|
||||
}
|
||||
|
||||
// ✅ 使用迭代器方法
|
||||
fn good_check_any(items: &[Item]) -> bool {
|
||||
items.iter().any(|i| i.is_valid())
|
||||
}
|
||||
|
||||
// ❌ String::from 用于静态字符串
|
||||
fn bad_static() -> String {
|
||||
String::from("error message") // 运行时分配
|
||||
}
|
||||
|
||||
// ✅ 返回 &'static str
|
||||
fn good_static() -> &'static str {
|
||||
"error message" // 无分配
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Trait 设计
|
||||
|
||||
### 避免过度抽象
|
||||
|
||||
```rust
|
||||
// ❌ 过度抽象——不是 Java,不需要 Interface 一切
|
||||
trait Processor { fn process(&self); }
|
||||
trait Handler { fn handle(&self); }
|
||||
trait Manager { fn manage(&self); } // Trait 过多
|
||||
|
||||
// ✅ 只在需要多态时创建 trait
|
||||
// 具体类型通常更简单、更快
|
||||
struct DataProcessor {
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl DataProcessor {
|
||||
fn process(&self, data: &Data) -> Result<Output> {
|
||||
// 直接实现
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Trait 对象 vs 泛型
|
||||
|
||||
```rust
|
||||
// ❌ 不必要的 trait 对象(动态分发)
|
||||
fn bad_process(handler: &dyn Handler) {
|
||||
handler.handle(); // 虚表调用
|
||||
}
|
||||
|
||||
// ✅ 使用泛型(静态分发,可内联)
|
||||
fn good_process<H: Handler>(handler: &H) {
|
||||
handler.handle(); // 可能被内联
|
||||
}
|
||||
|
||||
// ✅ trait 对象适用场景:异构集合
|
||||
fn store_handlers(handlers: Vec<Box<dyn Handler>>) {
|
||||
// 需要存储不同类型的 handlers
|
||||
}
|
||||
|
||||
// ✅ 使用 impl Trait 返回类型
|
||||
fn create_handler() -> impl Handler {
|
||||
ConcreteHandler::new()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rust Review Checklist
|
||||
|
||||
### 编译器不能捕获的问题
|
||||
|
||||
**业务逻辑正确性**
|
||||
- [ ] 边界条件处理正确
|
||||
- [ ] 状态机转换完整
|
||||
- [ ] 并发场景下的竞态条件
|
||||
|
||||
**API 设计**
|
||||
- [ ] 公共 API 难以误用
|
||||
- [ ] 类型签名清晰表达意图
|
||||
- [ ] 错误类型粒度合适
|
||||
|
||||
### 所有权与借用
|
||||
|
||||
- [ ] clone() 是有意为之,文档说明了原因
|
||||
- [ ] Arc<Mutex<T>> 真的需要共享状态吗?
|
||||
- [ ] RefCell 的使用有正当理由
|
||||
- [ ] 生命周期不过度复杂
|
||||
- [ ] 考虑使用 Cow 避免不必要的分配
|
||||
|
||||
### Unsafe 代码(最重要)
|
||||
|
||||
- [ ] 每个 unsafe 块有 SAFETY 注释
|
||||
- [ ] unsafe fn 有 # Safety 文档节
|
||||
- [ ] 解释了为什么是安全的,不只是做什么
|
||||
- [ ] 列出了必须维护的不变量
|
||||
- [ ] unsafe 边界尽可能小
|
||||
- [ ] 考虑过是否有 safe 替代方案
|
||||
|
||||
### 异步/并发
|
||||
|
||||
- [ ] 没有在 async 中阻塞(std::fs、thread::sleep)
|
||||
- [ ] 没有跨 .await 持有 std::sync 锁
|
||||
- [ ] spawn 的任务满足 'static
|
||||
- [ ] 锁的获取顺序一致
|
||||
- [ ] Channel 缓冲区大小合理
|
||||
|
||||
### 取消安全性
|
||||
|
||||
- [ ] select! 中的 Future 是取消安全的
|
||||
- [ ] 文档化了 async 函数的取消安全性
|
||||
- [ ] 取消不会导致数据丢失或不一致状态
|
||||
- [ ] 使用 tokio::pin! 正确处理需要重用的 Future
|
||||
|
||||
### spawn vs await
|
||||
|
||||
- [ ] spawn 只用于真正需要并行的场景
|
||||
- [ ] 简单操作直接 await,不要 spawn
|
||||
- [ ] spawn 的 JoinHandle 结果被正确处理
|
||||
- [ ] 考虑任务的生命周期和关闭策略
|
||||
- [ ] 优先使用 join!/try_join! 进行结构化并发
|
||||
|
||||
### 错误处理
|
||||
|
||||
- [ ] 库:thiserror 定义结构化错误
|
||||
- [ ] 应用:anyhow + context
|
||||
- [ ] 没有生产代码 unwrap/expect
|
||||
- [ ] 错误消息对调试有帮助
|
||||
- [ ] must_use 返回值被处理
|
||||
- [ ] 使用 #[source] 保留错误链
|
||||
|
||||
### 性能
|
||||
|
||||
- [ ] 避免不必要的 collect()
|
||||
- [ ] 大数据传引用
|
||||
- [ ] 字符串用 with_capacity 或 write!
|
||||
- [ ] impl Trait vs Box<dyn Trait> 选择合理
|
||||
- [ ] 热路径避免分配
|
||||
- [ ] 考虑使用 Cow 减少克隆
|
||||
|
||||
### 代码质量
|
||||
|
||||
- [ ] cargo clippy 零警告
|
||||
- [ ] cargo fmt 格式化
|
||||
- [ ] 文档注释完整
|
||||
- [ ] 测试覆盖边界条件
|
||||
- [ ] 公共 API 有文档示例
|
||||
@@ -0,0 +1,266 @@
|
||||
# Security Review Guide
|
||||
|
||||
Security-focused code review checklist based on OWASP Top 10 and best practices.
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
### Authentication
|
||||
- [ ] Passwords hashed with strong algorithm (bcrypt, argon2)
|
||||
- [ ] Password complexity requirements enforced
|
||||
- [ ] Account lockout after failed attempts
|
||||
- [ ] Secure password reset flow
|
||||
- [ ] Multi-factor authentication for sensitive operations
|
||||
- [ ] Session tokens are cryptographically random
|
||||
- [ ] Session timeout implemented
|
||||
|
||||
### Authorization
|
||||
- [ ] Authorization checks on every request
|
||||
- [ ] Principle of least privilege applied
|
||||
- [ ] Role-based access control (RBAC) properly implemented
|
||||
- [ ] No privilege escalation paths
|
||||
- [ ] Direct object reference checks (IDOR prevention)
|
||||
- [ ] API endpoints protected appropriately
|
||||
|
||||
### JWT Security
|
||||
```typescript
|
||||
// ❌ Insecure JWT configuration
|
||||
jwt.sign(payload, 'weak-secret');
|
||||
|
||||
// ✅ Secure JWT configuration
|
||||
jwt.sign(payload, process.env.JWT_SECRET, {
|
||||
algorithm: 'RS256',
|
||||
expiresIn: '15m',
|
||||
issuer: 'your-app',
|
||||
audience: 'your-api'
|
||||
});
|
||||
|
||||
// ❌ Not verifying JWT properly
|
||||
const decoded = jwt.decode(token); // No signature verification!
|
||||
|
||||
// ✅ Verify signature and claims
|
||||
const decoded = jwt.verify(token, publicKey, {
|
||||
algorithms: ['RS256'],
|
||||
issuer: 'your-app',
|
||||
audience: 'your-api'
|
||||
});
|
||||
```
|
||||
|
||||
## Input Validation
|
||||
|
||||
### SQL Injection Prevention
|
||||
```python
|
||||
# ❌ Vulnerable to SQL injection
|
||||
query = f"SELECT * FROM users WHERE id = {user_id}"
|
||||
|
||||
# ✅ Use parameterized queries
|
||||
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
|
||||
|
||||
# ✅ Use ORM with proper escaping
|
||||
User.objects.filter(id=user_id)
|
||||
```
|
||||
|
||||
### XSS Prevention
|
||||
```typescript
|
||||
// ❌ Vulnerable to XSS
|
||||
element.innerHTML = userInput;
|
||||
|
||||
// ✅ Use textContent for plain text
|
||||
element.textContent = userInput;
|
||||
|
||||
// ✅ Use DOMPurify for HTML
|
||||
element.innerHTML = DOMPurify.sanitize(userInput);
|
||||
|
||||
// ✅ React automatically escapes (but watch dangerouslySetInnerHTML)
|
||||
return <div>{userInput}</div>; // Safe
|
||||
return <div dangerouslySetInnerHTML={{__html: userInput}} />; // Dangerous!
|
||||
```
|
||||
|
||||
### Command Injection Prevention
|
||||
```python
|
||||
# ❌ Vulnerable to command injection
|
||||
os.system(f"convert {filename} output.png")
|
||||
|
||||
# ✅ Use subprocess with list arguments
|
||||
subprocess.run(['convert', filename, 'output.png'], check=True)
|
||||
|
||||
# ✅ Validate and sanitize input
|
||||
import shlex
|
||||
safe_filename = shlex.quote(filename)
|
||||
```
|
||||
|
||||
### Path Traversal Prevention
|
||||
```typescript
|
||||
// ❌ Vulnerable to path traversal
|
||||
const filePath = `./uploads/${req.params.filename}`;
|
||||
|
||||
// ✅ Validate and sanitize path
|
||||
const path = require('path');
|
||||
const safeName = path.basename(req.params.filename);
|
||||
const uploadsDir = path.resolve('./uploads');
|
||||
const filePath = path.resolve(uploadsDir, safeName);
|
||||
|
||||
// Verify it's still within uploads directory (both sides absolute)
|
||||
if (!filePath.startsWith(uploadsDir + path.sep)) {
|
||||
throw new Error('Invalid path');
|
||||
}
|
||||
```
|
||||
|
||||
## Data Protection
|
||||
|
||||
### Sensitive Data Handling
|
||||
- [ ] No secrets in source code
|
||||
- [ ] Secrets stored in environment variables or secret manager
|
||||
- [ ] Sensitive data encrypted at rest
|
||||
- [ ] Sensitive data encrypted in transit (HTTPS)
|
||||
- [ ] PII handled according to regulations (GDPR, etc.)
|
||||
- [ ] Sensitive data not logged
|
||||
- [ ] Secure data deletion when required
|
||||
|
||||
### Configuration Security
|
||||
```yaml
|
||||
# ❌ Secrets in config files
|
||||
database:
|
||||
password: "super-secret-password"
|
||||
|
||||
# ✅ Reference environment variables
|
||||
database:
|
||||
password: ${DATABASE_PASSWORD}
|
||||
```
|
||||
|
||||
### Error Messages
|
||||
```typescript
|
||||
// ❌ Leaking sensitive information
|
||||
catch (error) {
|
||||
return res.status(500).json({
|
||||
error: error.stack, // Exposes internal details
|
||||
query: sqlQuery // Exposes database structure
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Generic error messages
|
||||
catch (error) {
|
||||
logger.error('Database error', { error, userId }); // Log internally
|
||||
return res.status(500).json({
|
||||
error: 'An unexpected error occurred'
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## API Security
|
||||
|
||||
### Rate Limiting
|
||||
- [ ] Rate limiting on all public endpoints
|
||||
- [ ] Stricter limits on authentication endpoints
|
||||
- [ ] Per-user and per-IP limits
|
||||
- [ ] Graceful handling when limits exceeded
|
||||
|
||||
### CORS Configuration
|
||||
```typescript
|
||||
// ❌ Overly permissive CORS
|
||||
app.use(cors({ origin: '*' }));
|
||||
|
||||
// ✅ Restrictive CORS
|
||||
app.use(cors({
|
||||
origin: ['https://your-app.com'],
|
||||
methods: ['GET', 'POST'],
|
||||
credentials: true
|
||||
}));
|
||||
```
|
||||
|
||||
### HTTP Headers
|
||||
```typescript
|
||||
// Security headers to set
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
}
|
||||
},
|
||||
hsts: { maxAge: 31536000, includeSubDomains: true },
|
||||
noSniff: true,
|
||||
xssFilter: true,
|
||||
frameguard: { action: 'deny' }
|
||||
}));
|
||||
```
|
||||
|
||||
## Cryptography
|
||||
|
||||
### Secure Practices
|
||||
- [ ] Using well-established algorithms (AES-256, RSA-2048+)
|
||||
- [ ] Not implementing custom cryptography
|
||||
- [ ] Using cryptographically secure random number generation
|
||||
- [ ] Proper key management and rotation
|
||||
- [ ] Secure key storage (HSM, KMS)
|
||||
|
||||
### Common Mistakes
|
||||
```typescript
|
||||
// ❌ Weak random generation
|
||||
const token = Math.random().toString(36);
|
||||
|
||||
// ✅ Cryptographically secure random
|
||||
const crypto = require('crypto');
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
// ❌ MD5/SHA1 for passwords
|
||||
const hash = crypto.createHash('md5').update(password).digest('hex');
|
||||
|
||||
// ✅ Use bcrypt or argon2
|
||||
const bcrypt = require('bcrypt');
|
||||
const hash = await bcrypt.hash(password, 12);
|
||||
```
|
||||
|
||||
## Dependency Security
|
||||
|
||||
### Checklist
|
||||
- [ ] Dependencies from trusted sources only
|
||||
- [ ] No known vulnerabilities (npm audit, cargo audit)
|
||||
- [ ] Dependencies kept up to date
|
||||
- [ ] Lock files committed (package-lock.json, Cargo.lock)
|
||||
- [ ] Minimal dependency usage
|
||||
- [ ] License compliance verified
|
||||
|
||||
### Audit Commands
|
||||
```bash
|
||||
# Node.js
|
||||
npm audit
|
||||
npm audit fix
|
||||
|
||||
# Python
|
||||
pip-audit
|
||||
safety check
|
||||
|
||||
# Rust
|
||||
cargo audit
|
||||
|
||||
# General
|
||||
snyk test
|
||||
```
|
||||
|
||||
## Logging & Monitoring
|
||||
|
||||
### Secure Logging
|
||||
- [ ] No sensitive data in logs (passwords, tokens, PII)
|
||||
- [ ] Logs protected from tampering
|
||||
- [ ] Appropriate log retention
|
||||
- [ ] Security events logged (login attempts, permission changes)
|
||||
- [ ] Log injection prevented
|
||||
|
||||
```typescript
|
||||
// ❌ Logging sensitive data
|
||||
logger.info(`User login: ${email}, password: ${password}`);
|
||||
|
||||
// ✅ Safe logging
|
||||
logger.info('User login attempt', { email, success: true });
|
||||
```
|
||||
|
||||
## Security Review Severity Levels
|
||||
|
||||
| Severity | Description | Action |
|
||||
|----------|-------------|--------|
|
||||
| **Critical** | Immediate exploitation possible, data breach risk | Block merge, fix immediately |
|
||||
| **High** | Significant vulnerability, requires specific conditions | Block merge, fix before release |
|
||||
| **Medium** | Moderate risk, defense in depth concern | Should fix, can merge with tracking |
|
||||
| **Low** | Minor issue, best practice violation | Nice to fix, non-blocking |
|
||||
| **Info** | Suggestion for improvement | Optional enhancement |
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,932 @@
|
||||
# Swift Code Review Guide
|
||||
|
||||
A code review checklist for modern Swift (5.9+/6), covering SwiftUI, Swift Concurrency, and the Swift API Design Guidelines.
|
||||
|
||||
## Quick Review Checklist
|
||||
|
||||
### Must-Check Items
|
||||
- [ ] Are force-unwraps (`!`) and `try!` avoided in favor of safe unwrapping
|
||||
- [ ] Do closures that capture `self` use `[weak self]` to avoid retain cycles
|
||||
- [ ] Is the value vs reference type choice intentional (struct vs class)
|
||||
- [ ] Are errors propagated with `throws`/`Result` instead of being swallowed
|
||||
- [ ] Are concurrency boundaries data-race-safe (`Sendable`, `@MainActor`, actors)
|
||||
|
||||
### Common Issues
|
||||
- [ ] Fire-and-forget `Task {}` that leaks or is never cancelled
|
||||
- [ ] Wrong SwiftUI property wrapper (`@ObservedObject` where `@StateObject` is needed)
|
||||
- [ ] O(n^2) lookups in loops that could use a `Set` or `Dictionary`
|
||||
- [ ] Implicitly unwrapped optionals (`var x: T!`) outside of IBOutlets
|
||||
- [ ] Over-broad access control (`public`/`open` where `internal` suffices)
|
||||
- [ ] Naming that ignores the Swift API Design Guidelines
|
||||
|
||||
---
|
||||
|
||||
## 1. Optionals and Unwrapping
|
||||
|
||||
### 1.1 Avoid Force-Unwrapping
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: crashes at runtime if nil
|
||||
let name = user.name!
|
||||
let url = URL(string: urlString)!
|
||||
|
||||
// ✅ Correct: bind with guard let / if let
|
||||
guard let name = user.name else {
|
||||
return
|
||||
}
|
||||
|
||||
if let url = URL(string: urlString) {
|
||||
load(url)
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 Use Nil-Coalescing for Defaults
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: verbose and crash-prone
|
||||
let count: Int
|
||||
if let c = dictionary["count"] {
|
||||
count = c
|
||||
} else {
|
||||
count = 0
|
||||
}
|
||||
|
||||
// ✅ Correct: nil-coalescing
|
||||
let count = dictionary["count"] ?? 0
|
||||
```
|
||||
|
||||
### 1.3 Prefer guard let for Early Exit
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: deep nesting (pyramid of doom)
|
||||
func process(_ input: String?) {
|
||||
if let input = input {
|
||||
if let value = Int(input) {
|
||||
if value > 0 {
|
||||
handle(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Correct: guard keeps the happy path unindented
|
||||
func process(_ input: String?) {
|
||||
guard let input,
|
||||
let value = Int(input),
|
||||
value > 0 else {
|
||||
return
|
||||
}
|
||||
handle(value)
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 Avoid Implicitly Unwrapped Optionals
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: T! is a hidden force-unwrap on every access
|
||||
class ViewModel {
|
||||
var service: NetworkService!
|
||||
}
|
||||
|
||||
// ✅ Correct: inject a non-optional dependency
|
||||
class ViewModel {
|
||||
private let service: NetworkService
|
||||
|
||||
init(service: NetworkService) {
|
||||
self.service = service
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.5 Use Optional Chaining and map/flatMap
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: manual unwrapping just to transform
|
||||
var initial: String?
|
||||
if let name = user.name {
|
||||
initial = String(name.prefix(1))
|
||||
}
|
||||
|
||||
// ✅ Correct: optional chaining + map
|
||||
let initial = user.name.map { String($0.prefix(1)) }
|
||||
|
||||
// ✅ Correct: flatMap to avoid double optionals
|
||||
let port: Int? = components.port.flatMap { Int(exactly: $0) }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Memory Management and Retain Cycles
|
||||
|
||||
### 2.1 Use [weak self] in Escaping Closures
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: closure strongly captures self, creating a retain cycle
|
||||
class ImageLoader {
|
||||
var onComplete: (() -> Void)?
|
||||
|
||||
func load() {
|
||||
service.fetch { data in
|
||||
self.cache = data // self is retained by the closure
|
||||
self.onComplete?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Correct: capture self weakly and guard
|
||||
class ImageLoader {
|
||||
var onComplete: (() -> Void)?
|
||||
|
||||
func load() {
|
||||
service.fetch { [weak self] data in
|
||||
guard let self else { return }
|
||||
self.cache = data
|
||||
self.onComplete?()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 weak vs unowned
|
||||
|
||||
```swift
|
||||
// ✅ Use weak when the reference can legitimately become nil
|
||||
class Controller {
|
||||
weak var delegate: ControllerDelegate?
|
||||
}
|
||||
|
||||
// ✅ Use unowned only when the captured object is guaranteed to
|
||||
// outlive the closure (e.g. self owns the closure tightly).
|
||||
// unowned crashes if accessed after deallocation.
|
||||
class Owner {
|
||||
lazy var describe: () -> String = { [unowned self] in
|
||||
self.name
|
||||
}
|
||||
let name = "owner"
|
||||
}
|
||||
|
||||
// ❌ Wrong: unowned on something that can outlive self -> crash
|
||||
networkClient.onResponse = { [unowned self] in self.update() }
|
||||
// Prefer [weak self] here, since onResponse may fire after self is gone.
|
||||
```
|
||||
|
||||
### 2.3 Break Delegate Retain Cycles
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: strong delegate keeps both objects alive forever
|
||||
protocol DataSourceDelegate: AnyObject {}
|
||||
|
||||
class DataSource {
|
||||
var delegate: DataSourceDelegate? // strong by default
|
||||
}
|
||||
|
||||
// ✅ Correct: delegates should be weak (and protocol AnyObject-bound)
|
||||
class DataSource {
|
||||
weak var delegate: DataSourceDelegate?
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 Closures Stored as Properties
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: stored closure captures self strongly -> permanent cycle
|
||||
class Timer {
|
||||
var tick: (() -> Void)!
|
||||
func configure() {
|
||||
tick = { self.count += 1 }
|
||||
}
|
||||
var count = 0
|
||||
}
|
||||
|
||||
// ✅ Correct: weak capture for stored closures referencing self
|
||||
class Timer {
|
||||
var tick: (() -> Void)?
|
||||
func configure() {
|
||||
tick = { [weak self] in self?.count += 1 }
|
||||
}
|
||||
var count = 0
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Value vs Reference Types
|
||||
|
||||
### 3.1 Prefer Structs by Default
|
||||
|
||||
```swift
|
||||
// ✅ Use a struct for data/models with value semantics
|
||||
struct Coordinate {
|
||||
var latitude: Double
|
||||
var longitude: Double
|
||||
}
|
||||
|
||||
// Copies are independent; no shared mutable state, thread-friendly.
|
||||
var a = Coordinate(latitude: 1, longitude: 2)
|
||||
var b = a
|
||||
b.latitude = 99 // a is unchanged
|
||||
```
|
||||
|
||||
### 3.2 Use a Class for Identity or Shared State
|
||||
|
||||
```swift
|
||||
// ✅ Use a class when instances have identity or must be shared/mutated
|
||||
// by reference, or when you need inheritance / Objective-C interop.
|
||||
final class DatabaseConnection {
|
||||
private(set) var isOpen = false
|
||||
func open() { isOpen = true }
|
||||
}
|
||||
|
||||
// Two references point to the same connection.
|
||||
let conn1 = DatabaseConnection()
|
||||
let conn2 = conn1
|
||||
conn1.open()
|
||||
// conn2.isOpen == true
|
||||
```
|
||||
|
||||
### 3.3 Mark Classes final When Not Subclassed
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: open to subclassing unintentionally (slower dispatch, fragile API)
|
||||
class UserViewModel {}
|
||||
|
||||
// ✅ Correct: final enables static dispatch and signals intent
|
||||
final class UserViewModel {}
|
||||
```
|
||||
|
||||
### 3.4 Beware Reference Types Inside Structs
|
||||
|
||||
```swift
|
||||
// ❌ Surprising: struct copy still shares the inner class instance
|
||||
final class Box { var value = 0 }
|
||||
struct Container { var box = Box() }
|
||||
|
||||
var x = Container()
|
||||
var y = x
|
||||
y.box.value = 42 // x.box.value is also 42 (shared reference!)
|
||||
|
||||
// ✅ Correct: use value semantics throughout, or copy on write deliberately
|
||||
struct Container {
|
||||
var value = 0 // plain value type, copies are independent
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Error Handling
|
||||
|
||||
### 4.1 Avoid try! and try?
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: try! crashes on any thrown error
|
||||
let data = try! Data(contentsOf: url)
|
||||
|
||||
// ❌ Often wrong: try? silently discards the error and the cause
|
||||
let data = try? Data(contentsOf: url) // data is nil, you lose "why"
|
||||
|
||||
// ✅ Correct: propagate or handle with do-catch
|
||||
do {
|
||||
let data = try Data(contentsOf: url)
|
||||
process(data)
|
||||
} catch {
|
||||
log.error("failed to read \(url): \(error)")
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Define Meaningful Error Types
|
||||
|
||||
```swift
|
||||
// ✅ Recommended: an Error enum communicates failure modes precisely
|
||||
enum NetworkError: Error {
|
||||
case invalidURL
|
||||
case unauthorized
|
||||
case server(statusCode: Int)
|
||||
case decoding(underlying: Error)
|
||||
}
|
||||
|
||||
func fetch(_ path: String) throws -> Data {
|
||||
guard let url = URL(string: path) else {
|
||||
throw NetworkError.invalidURL
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Use Result for Stored or Deferred Outcomes
|
||||
|
||||
```swift
|
||||
// ✅ Result is useful at callback boundaries or when storing an outcome
|
||||
func load(completion: @escaping (Result<User, NetworkError>) -> Void) {
|
||||
// completion(.success(user)) or completion(.failure(.unauthorized))
|
||||
}
|
||||
|
||||
// ✅ Convert between Result and throws as needed
|
||||
let user = try result.get()
|
||||
```
|
||||
|
||||
### 4.4 Typed Throws (Swift 6)
|
||||
|
||||
```swift
|
||||
// ✅ Typed throws constrains the error type when it is fully known.
|
||||
// Use it for closed, exhaustive error domains; prefer untyped
|
||||
// `throws` for library APIs that may grow new error cases.
|
||||
func parse(_ raw: String) throws(ParsingError) -> Token {
|
||||
guard let token = Token(raw) else {
|
||||
throw ParsingError.malformed
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
do {
|
||||
let token = try parse(input)
|
||||
} catch {
|
||||
// `error` is statically known to be ParsingError
|
||||
handle(error)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 Don't Catch and Rethrow Without Value
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: catch that adds nothing but obscures the trace
|
||||
do {
|
||||
try work()
|
||||
} catch {
|
||||
throw error // pointless
|
||||
}
|
||||
|
||||
// ✅ Correct: only catch to add context or recover
|
||||
do {
|
||||
try work()
|
||||
} catch {
|
||||
throw AppError.workFailed(underlying: error)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Swift Concurrency
|
||||
|
||||
### 5.1 Prefer async/await Over Nested Callbacks
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: callback pyramid, error handling scattered
|
||||
func loadProfile(completion: @escaping (Result<Profile, Error>) -> Void) {
|
||||
fetchUser { userResult in
|
||||
switch userResult {
|
||||
case .success(let user):
|
||||
fetchAvatar(user) { avatarResult in /* ... */ }
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Correct: linear async/await
|
||||
func loadProfile() async throws -> Profile {
|
||||
let user = try await fetchUser()
|
||||
let avatar = try await fetchAvatar(user)
|
||||
return Profile(user: user, avatar: avatar)
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Use @MainActor for UI State
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: mutating UI state from a background context (data race / crash)
|
||||
func refresh() async {
|
||||
let items = try? await api.load()
|
||||
self.items = items ?? [] // may run off the main thread
|
||||
}
|
||||
|
||||
// ✅ Correct: isolate UI-facing types to the main actor
|
||||
@MainActor
|
||||
final class FeedViewModel: ObservableObject {
|
||||
@Published var items: [Item] = []
|
||||
|
||||
func refresh() async {
|
||||
let loaded = (try? await api.load()) ?? []
|
||||
items = loaded // guaranteed on the main actor
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Protect Mutable State with Actors
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: shared mutable state without synchronization (data race)
|
||||
final class Counter {
|
||||
var value = 0
|
||||
func increment() { value += 1 }
|
||||
}
|
||||
|
||||
// ✅ Correct: an actor serializes access to its mutable state
|
||||
actor Counter {
|
||||
private(set) var value = 0
|
||||
func increment() { value += 1 }
|
||||
}
|
||||
|
||||
let counter = Counter()
|
||||
await counter.increment() // access is awaited and serialized
|
||||
```
|
||||
|
||||
### 5.4 Conform Shared Types to Sendable
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: passing a non-Sendable class across actors (Swift 6 error)
|
||||
final class Config { // mutable, not Sendable
|
||||
var retries = 3
|
||||
}
|
||||
|
||||
// ✅ Correct: make shared types Sendable (immutable value type is ideal)
|
||||
struct Config: Sendable {
|
||||
let retries: Int
|
||||
}
|
||||
|
||||
// ✅ For reference types, use final + immutable stored properties,
|
||||
// or @unchecked Sendable only with manual synchronization.
|
||||
final class Cache: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var storage: [String: Data] = [:]
|
||||
// all access guarded by lock
|
||||
}
|
||||
```
|
||||
|
||||
### 5.5 Handle Task Cancellation
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: ignores cancellation, keeps working after the view is gone
|
||||
func search(_ query: String) async -> [Result] {
|
||||
var results: [Result] = []
|
||||
for page in 0..<100 {
|
||||
results += await fetchPage(query, page) // never stops
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// ✅ Correct: check for cancellation cooperatively
|
||||
func search(_ query: String) async throws -> [Result] {
|
||||
var results: [Result] = []
|
||||
for page in 0..<100 {
|
||||
try Task.checkCancellation()
|
||||
results += try await fetchPage(query, page)
|
||||
}
|
||||
return results
|
||||
}
|
||||
```
|
||||
|
||||
### 5.6 Don't Leak Fire-and-Forget Tasks
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: unstructured Task with no handle, never cancelled
|
||||
final class ViewModel {
|
||||
func onAppear() {
|
||||
Task {
|
||||
await self.stream() // runs forever even after dismissal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Correct: retain the handle and cancel it (or use .task in SwiftUI)
|
||||
final class ViewModel {
|
||||
private var streamTask: Task<Void, Never>?
|
||||
|
||||
func onAppear() {
|
||||
streamTask = Task { [weak self] in
|
||||
await self?.stream()
|
||||
}
|
||||
}
|
||||
|
||||
func onDisappear() {
|
||||
streamTask?.cancel()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.7 Use Structured Concurrency for Parallelism
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: sequential awaits where work could run concurrently
|
||||
let a = await loadA()
|
||||
let b = await loadB() // waits for A to finish first
|
||||
|
||||
// ✅ Correct: async let runs them concurrently
|
||||
async let a = loadA()
|
||||
async let b = loadB()
|
||||
let (resultA, resultB) = await (a, b)
|
||||
|
||||
// ✅ For a dynamic number of children, use a task group
|
||||
try await withThrowingTaskGroup(of: Item.self) { group in
|
||||
for id in ids {
|
||||
group.addTask { try await fetch(id) }
|
||||
}
|
||||
for try await item in group {
|
||||
store(item)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. SwiftUI
|
||||
|
||||
### 6.1 Choose the Right State Wrapper
|
||||
|
||||
```swift
|
||||
// ✅ @State: simple value-type state owned by this view
|
||||
struct Toggle: View {
|
||||
@State private var isOn = false
|
||||
var body: some View { /* ... */ }
|
||||
}
|
||||
|
||||
// ✅ @StateObject: the view CREATES and OWNS a reference-type model
|
||||
struct ProfileScreen: View {
|
||||
@StateObject private var model = ProfileViewModel()
|
||||
var body: some View { /* ... */ }
|
||||
}
|
||||
|
||||
// ✅ @ObservedObject: the model is OWNED elsewhere and passed in
|
||||
struct ProfileHeader: View {
|
||||
@ObservedObject var model: ProfileViewModel
|
||||
var body: some View { /* ... */ }
|
||||
}
|
||||
|
||||
// ✅ @Binding: a two-way reference to state owned by a parent
|
||||
struct SearchField: View {
|
||||
@Binding var text: String
|
||||
var body: some View { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 @StateObject vs @ObservedObject
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: @ObservedObject for an object the view itself creates.
|
||||
// SwiftUI may recreate the view, re-instantiating the model and
|
||||
// losing its state on every re-render.
|
||||
struct CounterView: View {
|
||||
@ObservedObject var model = CounterModel() // recreated unexpectedly
|
||||
}
|
||||
|
||||
// ✅ Correct: @StateObject ties the model's lifetime to the view
|
||||
struct CounterView: View {
|
||||
@StateObject private var model = CounterModel()
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 Preserve View Identity
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: index-based id reuses identity when the array reorders,
|
||||
// causing wrong animations and stale state.
|
||||
ForEach(0..<items.count, id: \.self) { i in
|
||||
ItemRow(item: items[i])
|
||||
}
|
||||
|
||||
// ✅ Correct: use a stable, unique identifier
|
||||
ForEach(items) { item in // Item: Identifiable
|
||||
ItemRow(item: item)
|
||||
}
|
||||
|
||||
// ✅ Use .id(...) to deliberately reset a view's state
|
||||
ProfileView(user: user)
|
||||
.id(user.id) // new identity per user -> fresh state
|
||||
```
|
||||
|
||||
### 6.4 Avoid Over-Rendering
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: a single huge body re-renders everything on any change
|
||||
struct Dashboard: View {
|
||||
@ObservedObject var model: DashboardModel
|
||||
var body: some View {
|
||||
VStack {
|
||||
// header + heavy chart + list all recompute together
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Correct: extract subviews so only the affected part re-renders.
|
||||
// Each child observes only the state it needs.
|
||||
struct Dashboard: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
HeaderView()
|
||||
ChartView()
|
||||
ItemList()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.5 Do Async Work with .task
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: kicking off work in onAppear without cancellation
|
||||
.onAppear {
|
||||
Task { await model.load() } // not cancelled when view disappears
|
||||
}
|
||||
|
||||
// ✅ Correct: .task is tied to the view's lifetime and auto-cancels
|
||||
.task {
|
||||
await model.load()
|
||||
}
|
||||
|
||||
// ✅ Re-run when an input changes
|
||||
.task(id: query) {
|
||||
await model.search(query)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Protocols and Generics
|
||||
|
||||
### 7.1 Protocol-Oriented Design
|
||||
|
||||
```swift
|
||||
// ✅ Compose behavior with protocols and default implementations
|
||||
protocol Identifiable2 {
|
||||
var id: String { get }
|
||||
}
|
||||
|
||||
protocol Describable {
|
||||
var description: String { get }
|
||||
}
|
||||
|
||||
extension Describable {
|
||||
var description: String { "no description" } // default
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 Prefer some Over any
|
||||
|
||||
```swift
|
||||
// ❌ Slower: `any` is an existential box with dynamic dispatch
|
||||
func makeShape() -> any Shape { Circle() }
|
||||
|
||||
// ✅ Faster: `some` is an opaque type resolved at compile time,
|
||||
// preserving the concrete type and enabling static dispatch.
|
||||
func makeShape() -> some Shape { Circle() }
|
||||
|
||||
// Use `any` only when you genuinely need heterogeneous values:
|
||||
let shapes: [any Shape] = [Circle(), Square()]
|
||||
```
|
||||
|
||||
### 7.3 Generic Constraints Over Existentials
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: existential parameter loses the concrete type and is slower
|
||||
func logTotal(_ items: [any Numeric]) {
|
||||
// awkward: the concrete numeric type is erased, so arithmetic needs casts
|
||||
}
|
||||
|
||||
// ✅ Correct: a generic constraint keeps full type information
|
||||
func total<T: Numeric>(_ items: [T]) -> T {
|
||||
items.reduce(.zero, +)
|
||||
}
|
||||
```
|
||||
|
||||
### 7.4 Associated Types with Primary Associated Types
|
||||
|
||||
```swift
|
||||
// ✅ Primary associated types (Swift 5.7+) allow lightweight constraints
|
||||
protocol Container<Item> {
|
||||
associatedtype Item
|
||||
var count: Int { get }
|
||||
subscript(_ index: Int) -> Item { get }
|
||||
}
|
||||
|
||||
// Constrain the element type without a where-clause:
|
||||
func first(in container: some Container<Int>) -> Int {
|
||||
container[0]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Access Control and API Design
|
||||
|
||||
### 8.1 Use the Narrowest Access Level
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: everything public exposes internal details as API surface
|
||||
public class Service {
|
||||
public var cache: [String: Data] = [:]
|
||||
public func reset() {}
|
||||
}
|
||||
|
||||
// ✅ Correct: expose only the intended API; hide the rest
|
||||
public final class Service {
|
||||
private var cache: [String: Data] = [:]
|
||||
public func reset() { cache.removeAll() }
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 private vs fileprivate vs internal vs public/open
|
||||
|
||||
```swift
|
||||
// private: visible only within the enclosing declaration (and its extensions in the same file)
|
||||
// fileprivate: visible within the same source file
|
||||
// internal: visible within the module (the default)
|
||||
// public: visible outside the module, but not subclassable/overridable
|
||||
// open: visible outside the module AND subclassable/overridable
|
||||
|
||||
// ✅ Use private(set) to expose read-only state
|
||||
public final class Account {
|
||||
public private(set) var balance: Decimal = 0
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 Follow the Swift API Design Guidelines
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: redundant words, unclear argument roles
|
||||
func insertObject(_ object: Element, atIndex index: Int)
|
||||
list.removeElement(at: 0)
|
||||
|
||||
// ✅ Correct: read at the call site like a phrase; omit needless words
|
||||
func insert(_ element: Element, at index: Int)
|
||||
list.insert(item, at: 0) // reads as "insert item at 0"
|
||||
list.remove(at: 0)
|
||||
|
||||
// ✅ Boolean properties read as assertions
|
||||
var isEmpty: Bool
|
||||
var hasChanges: Bool
|
||||
```
|
||||
|
||||
### 8.4 Name Methods by Side Effects
|
||||
|
||||
```swift
|
||||
// ✅ Mutating verb vs non-mutating noun pairs (the "ed/ing" rule)
|
||||
var sorted = array.sorted() // returns a new value (non-mutating)
|
||||
array.sort() // mutates in place (imperative verb)
|
||||
|
||||
let reversed = text.reversed()
|
||||
text.reverse()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Collections and Functional Style
|
||||
|
||||
### 9.1 Prefer map/filter/compactMap
|
||||
|
||||
```swift
|
||||
// ❌ Verbose: manual loop with mutable accumulator
|
||||
var names: [String] = []
|
||||
for user in users {
|
||||
if user.isActive {
|
||||
names.append(user.name)
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Correct: declarative transform
|
||||
let names = users.filter(\.isActive).map(\.name)
|
||||
```
|
||||
|
||||
### 9.2 compactMap to Drop nils
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: map leaves an [Int?] you then have to unwrap
|
||||
let numbers = strings.map { Int($0) } // [Int?]
|
||||
|
||||
// ✅ Correct: compactMap removes nils and unwraps
|
||||
let numbers = strings.compactMap { Int($0) } // [Int]
|
||||
```
|
||||
|
||||
### 9.3 Avoid O(n^2) Membership Checks
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: contains on an Array is O(n); the loop is O(n*m)
|
||||
let result = candidates.filter { blocked.contains($0) } // blocked: [ID]
|
||||
|
||||
// ✅ Correct: a Set makes membership O(1)
|
||||
let blockedSet = Set(blocked)
|
||||
let result = candidates.filter { blockedSet.contains($0) }
|
||||
```
|
||||
|
||||
### 9.4 reduce and Dictionary Grouping
|
||||
|
||||
```swift
|
||||
// ✅ Group with Dictionary(grouping:)
|
||||
let byFirstLetter = Dictionary(grouping: words) { $0.first }
|
||||
|
||||
// ❌ Wrong: reduce(into:) is preferred over reduce that copies each step
|
||||
let total = numbers.reduce(0) { $0 + $1 } // fine for scalars
|
||||
|
||||
// ✅ Use reduce(into:) when accumulating into a collection (avoids copies)
|
||||
let counts = words.reduce(into: [:]) { acc, word in
|
||||
acc[word, default: 0] += 1
|
||||
}
|
||||
```
|
||||
|
||||
### 9.5 Use lazy for Chained Transforms on Large Sequences
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: each step allocates an intermediate array
|
||||
let firstMatch = bigArray.map(expensive).filter(isValid).first
|
||||
|
||||
// ✅ Correct: lazy avoids intermediate arrays and stops early
|
||||
let firstMatch = bigArray.lazy.map(expensive).filter(isValid).first
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Testing
|
||||
|
||||
### 10.1 Arrange-Act-Assert with XCTest
|
||||
|
||||
```swift
|
||||
import XCTest
|
||||
@testable import MyApp
|
||||
|
||||
final class PriceCalculatorTests: XCTestCase {
|
||||
func testDiscountApplied() {
|
||||
// Arrange
|
||||
let calculator = PriceCalculator(discount: 0.1)
|
||||
// Act
|
||||
let total = calculator.total(for: 100)
|
||||
// Assert
|
||||
XCTAssertEqual(total, 90, accuracy: 0.001)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 10.2 Testing async Code
|
||||
|
||||
```swift
|
||||
// ✅ Mark the test method async and await directly
|
||||
func testFetchUser() async throws {
|
||||
let service = UserService(client: MockClient())
|
||||
let user = try await service.fetchUser(id: "42")
|
||||
XCTAssertEqual(user.id, "42")
|
||||
}
|
||||
|
||||
// ✅ Assert that an async call throws the expected error
|
||||
func testFetchUserUnauthorized() async {
|
||||
let service = UserService(client: UnauthorizedClient())
|
||||
do {
|
||||
_ = try await service.fetchUser(id: "42")
|
||||
XCTFail("expected to throw")
|
||||
} catch NetworkError.unauthorized {
|
||||
// expected
|
||||
} catch {
|
||||
XCTFail("unexpected error: \(error)")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 10.3 Inject Dependencies via Protocols
|
||||
|
||||
```swift
|
||||
// ✅ Depend on a protocol so tests can substitute a mock
|
||||
protocol HTTPClient {
|
||||
func get(_ url: URL) async throws -> Data
|
||||
}
|
||||
|
||||
struct MockClient: HTTPClient {
|
||||
var result: Result<Data, Error>
|
||||
func get(_ url: URL) async throws -> Data {
|
||||
try result.get()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 10.4 Avoid Sleeps; Await Expectations or Values
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: arbitrary sleep makes tests slow and flaky
|
||||
func testCallback() {
|
||||
var done = false
|
||||
object.run { done = true }
|
||||
Thread.sleep(forTimeInterval: 1)
|
||||
XCTAssertTrue(done)
|
||||
}
|
||||
|
||||
// ✅ Correct: use XCTestExpectation for callback APIs
|
||||
func testCallback() {
|
||||
let expectation = expectation(description: "callback fired")
|
||||
object.run { expectation.fulfill() }
|
||||
wait(for: [expectation], timeout: 1.0)
|
||||
}
|
||||
|
||||
// ✅ Better: refactor to async and await the value directly
|
||||
func testCallback() async {
|
||||
let value = await object.run()
|
||||
XCTAssertEqual(value, expected)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/)
|
||||
- [The Swift Programming Language](https://docs.swift.org/swift-book/)
|
||||
- [Swift Concurrency (TSPL)](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/)
|
||||
- [Migrating to Swift 6](https://www.swift.org/migration/documentation/migrationguide/)
|
||||
- [Apple: Managing Model Data in Your App (SwiftUI)](https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app)
|
||||
- [Apple: Automatic Reference Counting](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/automaticreferencecounting/)
|
||||
- [WWDC: Protocol-Oriented Programming in Swift](https://developer.apple.com/videos/play/wwdc2015/408/)
|
||||
- [Swift Evolution](https://github.com/apple/swift-evolution)
|
||||
@@ -0,0 +1,553 @@
|
||||
# TypeScript/JavaScript Code Review Guide
|
||||
|
||||
> TypeScript 代码审查指南,覆盖类型系统、泛型、条件类型、strict 模式、async/await 模式等核心主题。
|
||||
|
||||
## 目录
|
||||
|
||||
- [类型安全基础](#类型安全基础)
|
||||
- [泛型模式](#泛型模式)
|
||||
- [高级类型](#高级类型)
|
||||
- [Strict 模式配置](#strict-模式配置)
|
||||
- [异步处理](#异步处理)
|
||||
- [不可变性](#不可变性)
|
||||
- [ESLint 规则](#eslint-规则)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## 类型安全基础
|
||||
|
||||
### 避免使用 any
|
||||
|
||||
```typescript
|
||||
// ❌ Using any defeats type safety
|
||||
function processData(data: any) {
|
||||
return data.value; // 无类型检查,运行时可能崩溃
|
||||
}
|
||||
|
||||
// ✅ Use proper types
|
||||
interface DataPayload {
|
||||
value: string;
|
||||
}
|
||||
function processData(data: DataPayload) {
|
||||
return data.value;
|
||||
}
|
||||
|
||||
// ✅ 未知类型用 unknown + 类型守卫
|
||||
function processUnknown(data: unknown) {
|
||||
if (typeof data === 'object' && data !== null && 'value' in data) {
|
||||
return (data as { value: string }).value;
|
||||
}
|
||||
throw new Error('Invalid data');
|
||||
}
|
||||
```
|
||||
|
||||
### 类型收窄
|
||||
|
||||
```typescript
|
||||
// ❌ 不安全的类型断言
|
||||
function getLength(value: string | string[]) {
|
||||
return (value as string[]).length; // 如果是 string 会出错
|
||||
}
|
||||
|
||||
// ✅ 使用类型守卫
|
||||
function getLength(value: string | string[]): number {
|
||||
if (Array.isArray(value)) {
|
||||
return value.length;
|
||||
}
|
||||
return value.length;
|
||||
}
|
||||
|
||||
// ✅ 使用 in 操作符
|
||||
interface Dog { bark(): void }
|
||||
interface Cat { meow(): void }
|
||||
|
||||
function speak(animal: Dog | Cat) {
|
||||
if ('bark' in animal) {
|
||||
animal.bark();
|
||||
} else {
|
||||
animal.meow();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 字面量类型与 as const
|
||||
|
||||
```typescript
|
||||
// ❌ 类型过于宽泛
|
||||
const config = {
|
||||
endpoint: '/api',
|
||||
method: 'GET' // 类型是 string
|
||||
};
|
||||
|
||||
// ✅ 使用 as const 获得字面量类型
|
||||
const config = {
|
||||
endpoint: '/api',
|
||||
method: 'GET'
|
||||
} as const; // method 类型是 'GET'
|
||||
|
||||
// ✅ 用于函数参数
|
||||
function request(method: 'GET' | 'POST', url: string) { ... }
|
||||
request(config.method, config.endpoint); // 正确!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 泛型模式
|
||||
|
||||
### 基础泛型
|
||||
|
||||
```typescript
|
||||
// ❌ 重复代码
|
||||
function getFirstString(arr: string[]): string | undefined {
|
||||
return arr[0];
|
||||
}
|
||||
function getFirstNumber(arr: number[]): number | undefined {
|
||||
return arr[0];
|
||||
}
|
||||
|
||||
// ✅ 使用泛型
|
||||
function getFirst<T>(arr: T[]): T | undefined {
|
||||
return arr[0];
|
||||
}
|
||||
```
|
||||
|
||||
### 泛型约束
|
||||
|
||||
```typescript
|
||||
// ❌ 泛型没有约束,无法访问属性
|
||||
function getProperty<T>(obj: T, key: string) {
|
||||
return obj[key]; // Error: 无法索引
|
||||
}
|
||||
|
||||
// ✅ 使用 keyof 约束
|
||||
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
|
||||
return obj[key];
|
||||
}
|
||||
|
||||
const user = { name: 'Alice', age: 30 };
|
||||
getProperty(user, 'name'); // 返回类型是 string
|
||||
getProperty(user, 'age'); // 返回类型是 number
|
||||
getProperty(user, 'foo'); // Error: 'foo' 不在 keyof User
|
||||
```
|
||||
|
||||
### 泛型默认值
|
||||
|
||||
```typescript
|
||||
// ✅ 提供合理的默认类型
|
||||
interface ApiResponse<T = unknown> {
|
||||
data: T;
|
||||
status: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// 可以不指定泛型参数
|
||||
const response: ApiResponse = { data: null, status: 200, message: 'OK' };
|
||||
// 也可以指定
|
||||
const userResponse: ApiResponse<User> = { ... };
|
||||
```
|
||||
|
||||
### 常见泛型工具类型
|
||||
|
||||
```typescript
|
||||
// ✅ 善用内置工具类型
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
type PartialUser = Partial<User>; // 所有属性可选
|
||||
type RequiredUser = Required<User>; // 所有属性必需
|
||||
type ReadonlyUser = Readonly<User>; // 所有属性只读
|
||||
type UserKeys = keyof User; // 'id' | 'name' | 'email'
|
||||
type NameOnly = Pick<User, 'name'>; // { name: string }
|
||||
type WithoutId = Omit<User, 'id'>; // { name: string; email: string }
|
||||
type UserRecord = Record<string, User>; // { [key: string]: User }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 高级类型
|
||||
|
||||
### 条件类型
|
||||
|
||||
```typescript
|
||||
// ✅ 根据输入类型返回不同类型
|
||||
type IsString<T> = T extends string ? true : false;
|
||||
|
||||
type A = IsString<string>; // true
|
||||
type B = IsString<number>; // false
|
||||
|
||||
// ✅ 提取数组元素类型
|
||||
type ElementType<T> = T extends (infer U)[] ? U : never;
|
||||
|
||||
type Elem = ElementType<string[]>; // string
|
||||
|
||||
// ✅ 提取函数返回类型(内置 ReturnType)
|
||||
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
|
||||
```
|
||||
|
||||
### 映射类型
|
||||
|
||||
```typescript
|
||||
// ✅ 转换对象类型的所有属性
|
||||
type Nullable<T> = {
|
||||
[K in keyof T]: T[K] | null;
|
||||
};
|
||||
|
||||
interface User {
|
||||
name: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
type NullableUser = Nullable<User>;
|
||||
// { name: string | null; age: number | null }
|
||||
|
||||
// ✅ 添加前缀
|
||||
type Getters<T> = {
|
||||
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
|
||||
};
|
||||
|
||||
type UserGetters = Getters<User>;
|
||||
// { getName: () => string; getAge: () => number }
|
||||
```
|
||||
|
||||
### 模板字面量类型
|
||||
|
||||
```typescript
|
||||
// ✅ 类型安全的事件名称
|
||||
type EventName = 'click' | 'focus' | 'blur';
|
||||
type HandlerName = `on${Capitalize<EventName>}`;
|
||||
// 'onClick' | 'onFocus' | 'onBlur'
|
||||
|
||||
// ✅ API 路由类型
|
||||
type ApiRoute = `/api/${string}`;
|
||||
const route: ApiRoute = '/api/users'; // OK
|
||||
const badRoute: ApiRoute = '/users'; // Error
|
||||
```
|
||||
|
||||
### Discriminated Unions
|
||||
|
||||
```typescript
|
||||
// ✅ 使用判别属性实现类型安全
|
||||
type Result<T, E> =
|
||||
| { success: true; data: T }
|
||||
| { success: false; error: E };
|
||||
|
||||
function handleResult(result: Result<User, Error>) {
|
||||
if (result.success) {
|
||||
console.log(result.data.name); // TypeScript 知道 data 存在
|
||||
} else {
|
||||
console.log(result.error.message); // TypeScript 知道 error 存在
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Redux Action 模式
|
||||
type Action =
|
||||
| { type: 'INCREMENT'; payload: number }
|
||||
| { type: 'DECREMENT'; payload: number }
|
||||
| { type: 'RESET' };
|
||||
|
||||
function reducer(state: number, action: Action): number {
|
||||
switch (action.type) {
|
||||
case 'INCREMENT':
|
||||
return state + action.payload; // payload 类型已知
|
||||
case 'DECREMENT':
|
||||
return state - action.payload;
|
||||
case 'RESET':
|
||||
return 0; // 这里没有 payload
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Strict 模式配置
|
||||
|
||||
### 推荐的 tsconfig.json
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
// ✅ 必须开启的 strict 选项
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"useUnknownInCatchVariables": true,
|
||||
|
||||
// ✅ 额外推荐选项
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noPropertyAccessFromIndexSignature": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### noUncheckedIndexedAccess 的影响
|
||||
|
||||
```typescript
|
||||
// tsconfig: "noUncheckedIndexedAccess": true
|
||||
|
||||
const arr = [1, 2, 3];
|
||||
const first = arr[0]; // 类型是 number | undefined
|
||||
|
||||
// ❌ 直接使用可能出错
|
||||
console.log(first.toFixed(2)); // Error: 可能是 undefined
|
||||
|
||||
// ✅ 先检查
|
||||
if (first !== undefined) {
|
||||
console.log(first.toFixed(2));
|
||||
}
|
||||
|
||||
// ✅ 或使用非空断言(确定时)
|
||||
console.log(arr[0]!.toFixed(2));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 异步处理
|
||||
|
||||
### Promise 错误处理
|
||||
|
||||
```typescript
|
||||
// ❌ Not handling async errors
|
||||
async function fetchUser(id: string) {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
return response.json(); // 网络错误未处理
|
||||
}
|
||||
|
||||
// ✅ Handle errors properly
|
||||
async function fetchUser(id: string): Promise<User> {
|
||||
try {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`Failed to fetch user: ${error.message}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Promise.all vs Promise.allSettled
|
||||
|
||||
```typescript
|
||||
// ❌ Promise.all 一个失败全部失败
|
||||
async function fetchAllUsers(ids: string[]) {
|
||||
const users = await Promise.all(ids.map(fetchUser));
|
||||
return users; // 一个失败就全部失败
|
||||
}
|
||||
|
||||
// ✅ Promise.allSettled 获取所有结果
|
||||
async function fetchAllUsers(ids: string[]) {
|
||||
const results = await Promise.allSettled(ids.map(fetchUser));
|
||||
|
||||
const users: User[] = [];
|
||||
const errors: Error[] = [];
|
||||
|
||||
for (const result of results) {
|
||||
if (result.status === 'fulfilled') {
|
||||
users.push(result.value);
|
||||
} else {
|
||||
errors.push(result.reason);
|
||||
}
|
||||
}
|
||||
|
||||
return { users, errors };
|
||||
}
|
||||
```
|
||||
|
||||
### 竞态条件处理
|
||||
|
||||
```typescript
|
||||
// ❌ 竞态条件:旧请求可能覆盖新请求
|
||||
function useSearch() {
|
||||
const [query, setQuery] = useState('');
|
||||
const [results, setResults] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/search?q=${query}`)
|
||||
.then(r => r.json())
|
||||
.then(setResults); // 旧请求可能后返回!
|
||||
}, [query]);
|
||||
}
|
||||
|
||||
// ✅ 使用 AbortController
|
||||
function useSearch() {
|
||||
const [query, setQuery] = useState('');
|
||||
const [results, setResults] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
|
||||
fetch(`/api/search?q=${query}`, { signal: controller.signal })
|
||||
.then(r => r.json())
|
||||
.then(setResults)
|
||||
.catch(e => {
|
||||
if (e.name !== 'AbortError') throw e;
|
||||
});
|
||||
|
||||
return () => controller.abort();
|
||||
}, [query]);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 不可变性
|
||||
|
||||
### Readonly 与 ReadonlyArray
|
||||
|
||||
```typescript
|
||||
// ❌ 可变参数可能被意外修改
|
||||
function processUsers(users: User[]) {
|
||||
users.sort((a, b) => a.name.localeCompare(b.name)); // 修改了原数组!
|
||||
return users;
|
||||
}
|
||||
|
||||
// ✅ 使用 readonly 防止修改
|
||||
function processUsers(users: readonly User[]): User[] {
|
||||
return [...users].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
// ✅ 深度只读
|
||||
type DeepReadonly<T> = {
|
||||
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
|
||||
};
|
||||
```
|
||||
|
||||
### 不变式函数参数
|
||||
|
||||
```typescript
|
||||
// ✅ 使用 as const 和 readonly 保护数据
|
||||
function createConfig<T extends readonly string[]>(routes: T) {
|
||||
return routes;
|
||||
}
|
||||
|
||||
const routes = createConfig(['home', 'about', 'contact'] as const);
|
||||
// 类型是 readonly ['home', 'about', 'contact']
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ESLint 规则
|
||||
|
||||
### 推荐的 @typescript-eslint 规则
|
||||
|
||||
```javascript
|
||||
// eslint.config.js(flat config,typescript-eslint v8)
|
||||
import eslint from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
eslint.configs.recommended,
|
||||
// 需要类型信息的规则集,对应旧的 recommended-requiring-type-checking
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
tseslint.configs.strictTypeChecked,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
// 让带类型的规则自动找到对应 tsconfig
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// ✅ 类型安全
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'error',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'error',
|
||||
'@typescript-eslint/no-unsafe-call': 'error',
|
||||
'@typescript-eslint/no-unsafe-return': 'error',
|
||||
|
||||
// ✅ 最佳实践
|
||||
'@typescript-eslint/explicit-function-return-type': 'warn',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'@typescript-eslint/await-thenable': 'error',
|
||||
'@typescript-eslint/no-misused-promises': 'error',
|
||||
|
||||
// ✅ 代码风格
|
||||
'@typescript-eslint/consistent-type-imports': 'error',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'error',
|
||||
'@typescript-eslint/prefer-optional-chain': 'error',
|
||||
},
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### 常见 ESLint 错误修复
|
||||
|
||||
```typescript
|
||||
// ❌ no-floating-promises: Promise 必须被处理
|
||||
async function save() { ... }
|
||||
save(); // Error: 未处理的 Promise
|
||||
|
||||
// ✅ 显式处理
|
||||
await save();
|
||||
// 或
|
||||
save().catch(console.error);
|
||||
// 或明确忽略
|
||||
void save();
|
||||
|
||||
// ❌ no-misused-promises: 不能在非 async 位置使用 Promise
|
||||
const items = [1, 2, 3];
|
||||
items.forEach(async (item) => { // Error!
|
||||
await processItem(item);
|
||||
});
|
||||
|
||||
// ✅ 使用 for...of
|
||||
for (const item of items) {
|
||||
await processItem(item);
|
||||
}
|
||||
// 或 Promise.all
|
||||
await Promise.all(items.map(processItem));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### 类型系统
|
||||
- [ ] 没有使用 `any`(使用 `unknown` + 类型守卫代替)
|
||||
- [ ] 接口和类型定义完整且有意义的命名
|
||||
- [ ] 使用泛型提高代码复用性
|
||||
- [ ] 联合类型有正确的类型收窄
|
||||
- [ ] 善用工具类型(Partial、Pick、Omit 等)
|
||||
|
||||
### 泛型
|
||||
- [ ] 泛型有适当的约束(extends)
|
||||
- [ ] 泛型参数有合理的默认值
|
||||
- [ ] 避免过度泛型化(KISS 原则)
|
||||
|
||||
### Strict 模式
|
||||
- [ ] tsconfig.json 启用了 strict: true
|
||||
- [ ] 启用了 noUncheckedIndexedAccess
|
||||
- [ ] 没有使用 @ts-ignore(改用 @ts-expect-error)
|
||||
|
||||
### 异步代码
|
||||
- [ ] async 函数有错误处理
|
||||
- [ ] Promise rejection 被正确处理
|
||||
- [ ] 没有 floating promises(未处理的 Promise)
|
||||
- [ ] 并发请求使用 Promise.all 或 Promise.allSettled
|
||||
- [ ] 竞态条件使用 AbortController 处理
|
||||
|
||||
### 不可变性
|
||||
- [ ] 不直接修改函数参数
|
||||
- [ ] 使用 spread 操作符创建新对象/数组
|
||||
- [ ] 考虑使用 readonly 修饰符
|
||||
|
||||
### ESLint
|
||||
- [ ] 使用 @typescript-eslint/recommended
|
||||
- [ ] 没有 ESLint 警告或错误
|
||||
- [ ] 使用 consistent-type-imports
|
||||
@@ -0,0 +1,924 @@
|
||||
# Vue 3 Code Review Guide
|
||||
|
||||
> Vue 3 Composition API 代码审查指南,覆盖响应性系统、Props/Emits、Watchers、Composables、Vue 3.5 新特性等核心主题。
|
||||
|
||||
## 目录
|
||||
|
||||
- [响应性系统](#响应性系统)
|
||||
- [Props & Emits](#props--emits)
|
||||
- [Vue 3.5 新特性](#vue-35-新特性)
|
||||
- [Watchers](#watchers)
|
||||
- [模板最佳实践](#模板最佳实践)
|
||||
- [Composables](#composables)
|
||||
- [性能优化](#性能优化)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## 响应性系统
|
||||
|
||||
### ref vs reactive 选择
|
||||
|
||||
```vue
|
||||
<!-- ✅ 基本类型用 ref -->
|
||||
<script setup lang="ts">
|
||||
const count = ref(0)
|
||||
const name = ref('Vue')
|
||||
|
||||
// ref 需要 .value 访问
|
||||
count.value++
|
||||
</script>
|
||||
|
||||
<!-- ✅ 对象/数组用 reactive(可选)-->
|
||||
<script setup lang="ts">
|
||||
const state = reactive({
|
||||
user: null,
|
||||
loading: false,
|
||||
error: null
|
||||
})
|
||||
|
||||
// reactive 直接访问
|
||||
state.loading = true
|
||||
</script>
|
||||
|
||||
<!-- 💡 现代最佳实践:全部使用 ref,保持一致性 -->
|
||||
<script setup lang="ts">
|
||||
const user = ref<User | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<Error | null>(null)
|
||||
</script>
|
||||
```
|
||||
|
||||
### 解构 reactive 对象
|
||||
|
||||
```vue
|
||||
<!-- ❌ 解构 reactive 会丢失响应性 -->
|
||||
<script setup lang="ts">
|
||||
const state = reactive({ count: 0, name: 'Vue' })
|
||||
const { count, name } = state // 丢失响应性!
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 toRefs 保持响应性 -->
|
||||
<script setup lang="ts">
|
||||
const state = reactive({ count: 0, name: 'Vue' })
|
||||
const { count, name } = toRefs(state) // 保持响应性
|
||||
// 或者直接使用 ref
|
||||
const count = ref(0)
|
||||
const name = ref('Vue')
|
||||
</script>
|
||||
```
|
||||
|
||||
### computed 副作用
|
||||
|
||||
```vue
|
||||
<!-- ❌ computed 中产生副作用 -->
|
||||
<script setup lang="ts">
|
||||
const fullName = computed(() => {
|
||||
console.log('Computing...') // 副作用!
|
||||
otherRef.value = 'changed' // 修改其他状态!
|
||||
return `${firstName.value} ${lastName.value}`
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- ✅ computed 只用于派生状态 -->
|
||||
<script setup lang="ts">
|
||||
const fullName = computed(() => {
|
||||
return `${firstName.value} ${lastName.value}`
|
||||
})
|
||||
// 副作用放在 watch 或事件处理中
|
||||
watch(fullName, (name) => {
|
||||
console.log('Name changed:', name)
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### shallowRef 优化
|
||||
|
||||
```vue
|
||||
<!-- ❌ 大型对象使用 ref 会深度转换 -->
|
||||
<script setup lang="ts">
|
||||
const largeData = ref(hugeNestedObject) // 深度响应式,性能开销大
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 shallowRef 避免深度转换 -->
|
||||
<script setup lang="ts">
|
||||
const largeData = shallowRef(hugeNestedObject)
|
||||
|
||||
// 整体替换才会触发更新
|
||||
function updateData(newData) {
|
||||
largeData.value = newData // ✅ 触发更新
|
||||
}
|
||||
|
||||
// ❌ 修改嵌套属性不会触发更新
|
||||
// largeData.value.nested.prop = 'new'
|
||||
|
||||
// 需要手动触发时使用 triggerRef
|
||||
import { triggerRef } from 'vue'
|
||||
largeData.value.nested.prop = 'new'
|
||||
triggerRef(largeData)
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Props & Emits
|
||||
|
||||
### 直接修改 props
|
||||
|
||||
```vue
|
||||
<!-- ❌ 直接修改 props -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ user: User }>()
|
||||
props.user.name = 'New Name' // 永远不要直接修改 props!
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 emit 通知父组件更新 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ user: User }>()
|
||||
const emit = defineEmits<{
|
||||
update: [name: string]
|
||||
}>()
|
||||
const updateName = (name: string) => emit('update', name)
|
||||
</script>
|
||||
```
|
||||
|
||||
### defineProps 类型声明
|
||||
|
||||
```vue
|
||||
<!-- ❌ defineProps 缺少类型声明 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps(['title', 'count']) // 无类型检查
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用类型声明 + withDefaults -->
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
title: string
|
||||
count?: number
|
||||
items?: string[]
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
count: 0,
|
||||
items: () => [] // 对象/数组默认值需要工厂函数
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### defineEmits 类型安全
|
||||
|
||||
```vue
|
||||
<!-- ❌ defineEmits 缺少类型 -->
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits(['update', 'delete']) // 无类型检查
|
||||
emit('update', someValue) // 参数类型不安全
|
||||
</script>
|
||||
|
||||
<!-- ✅ 完整的类型定义 -->
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits<{
|
||||
update: [id: number, value: string]
|
||||
delete: [id: number]
|
||||
'custom-event': [payload: CustomPayload]
|
||||
}>()
|
||||
|
||||
// 现在有完整的类型检查
|
||||
emit('update', 1, 'new value') // ✅
|
||||
emit('update', 'wrong') // ❌ TypeScript 报错
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Vue 3.5 新特性
|
||||
|
||||
### Reactive Props Destructure (3.5+)
|
||||
|
||||
```vue
|
||||
<!-- Vue 3.5 之前:解构会丢失响应性 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ count: number }>()
|
||||
// 需要使用 props.count 或 toRefs
|
||||
</script>
|
||||
|
||||
<!-- ✅ Vue 3.5+:解构保持响应性 -->
|
||||
<script setup lang="ts">
|
||||
const { count, name = 'default' } = defineProps<{
|
||||
count: number
|
||||
name?: string
|
||||
}>()
|
||||
|
||||
// count 和 name 自动保持响应性!
|
||||
// 可以直接在模板和 watch 中使用
|
||||
watch(() => count, (newCount) => {
|
||||
console.log('Count changed:', newCount)
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- ✅ 配合默认值使用 -->
|
||||
<script setup lang="ts">
|
||||
const {
|
||||
title,
|
||||
count = 0,
|
||||
items = () => [] // 函数作为默认值(对象/数组)
|
||||
} = defineProps<{
|
||||
title: string
|
||||
count?: number
|
||||
items?: () => string[]
|
||||
}>()
|
||||
</script>
|
||||
```
|
||||
|
||||
### defineModel (3.4+)
|
||||
|
||||
```vue
|
||||
<!-- ❌ 传统 v-model 实现:冗长 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ modelValue: string }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
|
||||
|
||||
// 需要 computed 来双向绑定
|
||||
const value = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- ✅ defineModel:简洁的 v-model 实现 -->
|
||||
<script setup lang="ts">
|
||||
// 自动处理 props 和 emit
|
||||
const model = defineModel<string>()
|
||||
|
||||
// 直接使用
|
||||
model.value = 'new value' // 自动 emit
|
||||
</script>
|
||||
<template>
|
||||
<input v-model="model" />
|
||||
</template>
|
||||
|
||||
<!-- ✅ 命名 v-model -->
|
||||
<script setup lang="ts">
|
||||
// v-model:title 的实现
|
||||
const title = defineModel<string>('title')
|
||||
|
||||
// 带默认值和选项
|
||||
const count = defineModel<number>('count', {
|
||||
default: 0,
|
||||
required: false
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- ✅ 多个 v-model -->
|
||||
<script setup lang="ts">
|
||||
const firstName = defineModel<string>('firstName')
|
||||
const lastName = defineModel<string>('lastName')
|
||||
</script>
|
||||
<template>
|
||||
<!-- 父组件使用:<MyInput v-model:first-name="first" v-model:last-name="last" /> -->
|
||||
</template>
|
||||
|
||||
<!-- ✅ v-model 修饰符 -->
|
||||
<script setup lang="ts">
|
||||
const [model, modifiers] = defineModel<string>()
|
||||
|
||||
// 检查修饰符
|
||||
if (modifiers.capitalize) {
|
||||
// 处理 .capitalize 修饰符
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### useTemplateRef (3.5+)
|
||||
|
||||
```vue
|
||||
<!-- 传统方式:ref 属性与变量同名 -->
|
||||
<script setup lang="ts">
|
||||
const inputRef = ref<HTMLInputElement | null>(null)
|
||||
</script>
|
||||
<template>
|
||||
<input ref="inputRef" />
|
||||
</template>
|
||||
|
||||
<!-- ✅ useTemplateRef:更清晰的模板引用 -->
|
||||
<script setup lang="ts">
|
||||
import { useTemplateRef } from 'vue'
|
||||
|
||||
const input = useTemplateRef<HTMLInputElement>('my-input')
|
||||
|
||||
onMounted(() => {
|
||||
input.value?.focus()
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<input ref="my-input" />
|
||||
</template>
|
||||
|
||||
<!-- ✅ 动态 ref -->
|
||||
<script setup lang="ts">
|
||||
const refKey = ref('input-a')
|
||||
const dynamicInput = useTemplateRef<HTMLInputElement>(refKey)
|
||||
</script>
|
||||
```
|
||||
|
||||
### useId (3.5+)
|
||||
|
||||
```vue
|
||||
<!-- ❌ 手动生成 ID 可能冲突 -->
|
||||
<script setup lang="ts">
|
||||
const id = `input-${Math.random()}` // SSR 不一致!
|
||||
</script>
|
||||
|
||||
<!-- ✅ useId:SSR 安全的唯一 ID -->
|
||||
<script setup lang="ts">
|
||||
import { useId } from 'vue'
|
||||
|
||||
const id = useId() // 例如:'v-0'
|
||||
</script>
|
||||
<template>
|
||||
<label :for="id">Name</label>
|
||||
<input :id="id" />
|
||||
</template>
|
||||
|
||||
<!-- ✅ 表单组件中使用 -->
|
||||
<script setup lang="ts">
|
||||
const inputId = useId()
|
||||
const errorId = useId()
|
||||
</script>
|
||||
<template>
|
||||
<label :for="inputId">Email</label>
|
||||
<input
|
||||
:id="inputId"
|
||||
:aria-describedby="errorId"
|
||||
/>
|
||||
<span :id="errorId" class="error">{{ error }}</span>
|
||||
</template>
|
||||
```
|
||||
|
||||
### onWatcherCleanup (3.5+)
|
||||
|
||||
```vue
|
||||
<!-- 传统方式:watch 第三个参数 -->
|
||||
<script setup lang="ts">
|
||||
watch(source, async (value, oldValue, onCleanup) => {
|
||||
const controller = new AbortController()
|
||||
onCleanup(() => controller.abort())
|
||||
// ...
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- ✅ onWatcherCleanup:更灵活的清理 -->
|
||||
<script setup lang="ts">
|
||||
import { onWatcherCleanup } from 'vue'
|
||||
|
||||
watch(source, async (value) => {
|
||||
const controller = new AbortController()
|
||||
onWatcherCleanup(() => controller.abort())
|
||||
|
||||
// 可以在任意位置调用,不限于回调开头
|
||||
if (someCondition) {
|
||||
const anotherResource = createResource()
|
||||
onWatcherCleanup(() => anotherResource.dispose())
|
||||
}
|
||||
|
||||
await fetchData(value, controller.signal)
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### Deferred Teleport (3.5+)
|
||||
|
||||
```vue
|
||||
<!-- ❌ Teleport 目标必须在挂载时存在 -->
|
||||
<template>
|
||||
<Teleport to="#modal-container">
|
||||
<!-- 如果 #modal-container 不存在会报错 -->
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<!-- ✅ defer 属性延迟挂载 -->
|
||||
<template>
|
||||
<Teleport to="#modal-container" defer>
|
||||
<!-- 等待目标元素存在后再挂载 -->
|
||||
<Modal />
|
||||
</Teleport>
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Watchers
|
||||
|
||||
### watch vs watchEffect
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// ✅ watch:明确指定依赖,惰性执行
|
||||
watch(
|
||||
() => props.userId,
|
||||
async (userId) => {
|
||||
user.value = await fetchUser(userId)
|
||||
}
|
||||
)
|
||||
|
||||
// ✅ watchEffect:自动收集依赖,立即执行
|
||||
watchEffect(async () => {
|
||||
// 自动追踪 props.userId
|
||||
user.value = await fetchUser(props.userId)
|
||||
})
|
||||
|
||||
// 💡 选择指南:
|
||||
// - 需要旧值?用 watch
|
||||
// - 需要惰性执行?用 watch
|
||||
// - 依赖复杂?用 watchEffect
|
||||
</script>
|
||||
```
|
||||
|
||||
### watch 清理函数
|
||||
|
||||
```vue
|
||||
<!-- ❌ watch 缺少清理函数,可能内存泄漏 -->
|
||||
<script setup lang="ts">
|
||||
watch(searchQuery, async (query) => {
|
||||
const controller = new AbortController()
|
||||
const data = await fetch(`/api/search?q=${query}`, {
|
||||
signal: controller.signal
|
||||
})
|
||||
results.value = await data.json()
|
||||
// 如果 query 快速变化,旧请求不会被取消!
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 onCleanup 清理副作用 -->
|
||||
<script setup lang="ts">
|
||||
watch(searchQuery, async (query, _, onCleanup) => {
|
||||
const controller = new AbortController()
|
||||
onCleanup(() => controller.abort()) // 取消旧请求
|
||||
|
||||
try {
|
||||
const data = await fetch(`/api/search?q=${query}`, {
|
||||
signal: controller.signal
|
||||
})
|
||||
results.value = await data.json()
|
||||
} catch (e) {
|
||||
if (e.name !== 'AbortError') throw e
|
||||
}
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### watch 选项
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// ✅ immediate:立即执行一次
|
||||
watch(
|
||||
userId,
|
||||
async (id) => {
|
||||
user.value = await fetchUser(id)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// ✅ deep:深度监听(性能开销大,谨慎使用)
|
||||
watch(
|
||||
state,
|
||||
(newState) => {
|
||||
console.log('State changed deeply')
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// ✅ flush: 'post':DOM 更新后执行
|
||||
watch(
|
||||
source,
|
||||
() => {
|
||||
// 可以安全访问更新后的 DOM
|
||||
// nextTick 不再需要
|
||||
},
|
||||
{ flush: 'post' }
|
||||
)
|
||||
|
||||
// ✅ once: true (Vue 3.4+):只执行一次
|
||||
watch(
|
||||
source,
|
||||
(value) => {
|
||||
console.log('只会执行一次:', value)
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
</script>
|
||||
```
|
||||
|
||||
### 监听多个源
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// ✅ 监听多个 ref
|
||||
watch(
|
||||
[firstName, lastName],
|
||||
([newFirst, newLast], [oldFirst, oldLast]) => {
|
||||
console.log(`Name changed from ${oldFirst} ${oldLast} to ${newFirst} ${newLast}`)
|
||||
}
|
||||
)
|
||||
|
||||
// ✅ 监听 reactive 对象的特定属性
|
||||
watch(
|
||||
() => [state.count, state.name],
|
||||
([count, name]) => {
|
||||
console.log(`count: ${count}, name: ${name}`)
|
||||
}
|
||||
)
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 模板最佳实践
|
||||
|
||||
### v-for 的 key
|
||||
|
||||
```vue
|
||||
<!-- ❌ v-for 中使用 index 作为 key -->
|
||||
<template>
|
||||
<li v-for="(item, index) in items" :key="index">
|
||||
{{ item.name }}
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 使用唯一标识作为 key -->
|
||||
<template>
|
||||
<li v-for="item in items" :key="item.id">
|
||||
{{ item.name }}
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 复合 key(当没有唯一 ID 时)-->
|
||||
<template>
|
||||
<li v-for="(item, index) in items" :key="`${item.name}-${item.type}-${index}`">
|
||||
{{ item.name }}
|
||||
</li>
|
||||
</template>
|
||||
```
|
||||
|
||||
### v-if 和 v-for 优先级
|
||||
|
||||
```vue
|
||||
<!-- ❌ v-if 和 v-for 同时使用 -->
|
||||
<template>
|
||||
<li v-for="user in users" v-if="user.active" :key="user.id">
|
||||
{{ user.name }}
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 使用 computed 过滤 -->
|
||||
<script setup lang="ts">
|
||||
const activeUsers = computed(() =>
|
||||
users.value.filter(user => user.active)
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<li v-for="user in activeUsers" :key="user.id">
|
||||
{{ user.name }}
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 或用 template 包裹 -->
|
||||
<template>
|
||||
<template v-for="user in users" :key="user.id">
|
||||
<li v-if="user.active">
|
||||
{{ user.name }}
|
||||
</li>
|
||||
</template>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 事件处理
|
||||
|
||||
```vue
|
||||
<!-- ❌ 内联复杂逻辑 -->
|
||||
<template>
|
||||
<button @click="items = items.filter(i => i.id !== item.id); count--">
|
||||
Delete
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 使用方法 -->
|
||||
<script setup lang="ts">
|
||||
const deleteItem = (id: number) => {
|
||||
items.value = items.value.filter(i => i.id !== id)
|
||||
count.value--
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<button @click="deleteItem(item.id)">Delete</button>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 事件修饰符 -->
|
||||
<template>
|
||||
<!-- 阻止默认行为 -->
|
||||
<form @submit.prevent="handleSubmit">...</form>
|
||||
|
||||
<!-- 阻止冒泡 -->
|
||||
<button @click.stop="handleClick">...</button>
|
||||
|
||||
<!-- 只执行一次 -->
|
||||
<button @click.once="handleOnce">...</button>
|
||||
|
||||
<!-- 键盘修饰符 -->
|
||||
<input @keyup.enter="submit" @keyup.esc="cancel" />
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Composables
|
||||
|
||||
### Composable 设计原则
|
||||
|
||||
```typescript
|
||||
// ✅ 好的 composable 设计
|
||||
export function useCounter(initialValue = 0) {
|
||||
const count = ref(initialValue)
|
||||
|
||||
const increment = () => count.value++
|
||||
const decrement = () => count.value--
|
||||
const reset = () => count.value = initialValue
|
||||
|
||||
// 返回响应式引用和方法
|
||||
return {
|
||||
count: readonly(count), // 只读防止外部修改
|
||||
increment,
|
||||
decrement,
|
||||
reset
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 不要返回 .value
|
||||
export function useBadCounter() {
|
||||
const count = ref(0)
|
||||
return {
|
||||
count: count.value // ❌ 丢失响应性!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Props 传递给 composable
|
||||
|
||||
```vue
|
||||
<!-- ❌ 传递 props 到 composable 丢失响应性 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ userId: string }>()
|
||||
const { user } = useUser(props.userId) // 丢失响应性!
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 toRef 或 computed 保持响应性 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ userId: string }>()
|
||||
const userIdRef = toRef(props, 'userId')
|
||||
const { user } = useUser(userIdRef) // 保持响应性
|
||||
// 或使用 computed
|
||||
const { user } = useUser(computed(() => props.userId))
|
||||
|
||||
// ✅ Vue 3.5+:直接解构使用
|
||||
const { userId } = defineProps<{ userId: string }>()
|
||||
const { user } = useUser(() => userId) // getter 函数
|
||||
</script>
|
||||
```
|
||||
|
||||
### 异步 Composable
|
||||
|
||||
```typescript
|
||||
// ✅ 异步 composable 模式
|
||||
export function useFetch<T>(url: MaybeRefOrGetter<string>) {
|
||||
const data = ref<T | null>(null)
|
||||
const error = ref<Error | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const execute = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await fetch(toValue(url))
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
}
|
||||
data.value = await response.json()
|
||||
} catch (e) {
|
||||
error.value = e as Error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式 URL 时自动重新获取
|
||||
watchEffect(() => {
|
||||
toValue(url) // 追踪依赖
|
||||
execute()
|
||||
})
|
||||
|
||||
return {
|
||||
data: readonly(data),
|
||||
error: readonly(error),
|
||||
loading: readonly(loading),
|
||||
refetch: execute
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
const { data, loading, error, refetch } = useFetch<User[]>('/api/users')
|
||||
```
|
||||
|
||||
### 生命周期与清理
|
||||
|
||||
```typescript
|
||||
// ✅ Composable 中正确处理生命周期
|
||||
export function useEventListener(
|
||||
target: MaybeRefOrGetter<EventTarget>,
|
||||
event: string,
|
||||
handler: EventListener
|
||||
) {
|
||||
// 组件挂载后添加
|
||||
onMounted(() => {
|
||||
toValue(target).addEventListener(event, handler)
|
||||
})
|
||||
|
||||
// 组件卸载时移除
|
||||
onUnmounted(() => {
|
||||
toValue(target).removeEventListener(event, handler)
|
||||
})
|
||||
}
|
||||
|
||||
// ✅ 使用 effectScope 管理副作用
|
||||
export function useFeature() {
|
||||
const scope = effectScope()
|
||||
|
||||
scope.run(() => {
|
||||
// 所有响应式效果都在这个 scope 内
|
||||
const state = ref(0)
|
||||
watch(state, () => { /* ... */ })
|
||||
watchEffect(() => { /* ... */ })
|
||||
})
|
||||
|
||||
// 清理所有效果
|
||||
onUnmounted(() => scope.stop())
|
||||
|
||||
return { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能优化
|
||||
|
||||
### v-memo
|
||||
|
||||
```vue
|
||||
<!-- ✅ v-memo:缓存子树,避免重复渲染 -->
|
||||
<template>
|
||||
<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
|
||||
<!-- 只有当 item.id === selected 变化时才重新渲染 -->
|
||||
<ExpensiveComponent :item="item" :selected="item.id === selected" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 配合 v-for 使用 -->
|
||||
<template>
|
||||
<div
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
v-memo="[item.name, item.status]"
|
||||
>
|
||||
<!-- 只有 name 或 status 变化时重新渲染 -->
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### defineAsyncComponent
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
// ✅ 懒加载组件
|
||||
const HeavyChart = defineAsyncComponent(() =>
|
||||
import('./components/HeavyChart.vue')
|
||||
)
|
||||
|
||||
// ✅ 带加载和错误状态
|
||||
const AsyncModal = defineAsyncComponent({
|
||||
loader: () => import('./components/Modal.vue'),
|
||||
loadingComponent: LoadingSpinner,
|
||||
errorComponent: ErrorDisplay,
|
||||
delay: 200, // 延迟显示 loading(避免闪烁)
|
||||
timeout: 3000 // 超时时间
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### KeepAlive
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- ✅ 缓存动态组件 -->
|
||||
<KeepAlive>
|
||||
<component :is="currentTab" />
|
||||
</KeepAlive>
|
||||
|
||||
<!-- ✅ 指定缓存的组件 -->
|
||||
<KeepAlive include="TabA,TabB">
|
||||
<component :is="currentTab" />
|
||||
</KeepAlive>
|
||||
|
||||
<!-- ✅ 限制缓存数量 -->
|
||||
<KeepAlive :max="10">
|
||||
<component :is="currentTab" />
|
||||
</KeepAlive>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// KeepAlive 组件的生命周期钩子
|
||||
onActivated(() => {
|
||||
// 组件被激活时(从缓存恢复)
|
||||
refreshData()
|
||||
})
|
||||
|
||||
onDeactivated(() => {
|
||||
// 组件被停用时(进入缓存)
|
||||
pauseTimers()
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### 虚拟列表
|
||||
|
||||
```vue
|
||||
<!-- ✅ 大型列表使用虚拟滚动 -->
|
||||
<script setup lang="ts">
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
|
||||
const { list, containerProps, wrapperProps } = useVirtualList(
|
||||
items,
|
||||
{ itemHeight: 50 }
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<div v-bind="containerProps" style="height: 400px; overflow: auto">
|
||||
<div v-bind="wrapperProps">
|
||||
<div v-for="item in list" :key="item.data.id" style="height: 50px">
|
||||
{{ item.data.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### 响应性系统
|
||||
- [ ] ref 用于基本类型,reactive 用于对象(或统一用 ref)
|
||||
- [ ] 没有解构 reactive 对象(或使用了 toRefs)
|
||||
- [ ] props 传递给 composable 时保持了响应性
|
||||
- [ ] shallowRef/shallowReactive 用于大型对象优化
|
||||
- [ ] computed 中没有副作用
|
||||
|
||||
### Props & Emits
|
||||
- [ ] defineProps 使用 TypeScript 类型声明
|
||||
- [ ] 复杂默认值使用 withDefaults + 工厂函数
|
||||
- [ ] defineEmits 有完整的类型定义
|
||||
- [ ] 没有直接修改 props
|
||||
- [ ] 考虑使用 defineModel 简化 v-model(Vue 3.4+)
|
||||
|
||||
### Vue 3.5 新特性(如适用)
|
||||
- [ ] 使用 Reactive Props Destructure 简化 props 访问
|
||||
- [ ] 使用 useTemplateRef 替代 ref 属性
|
||||
- [ ] 表单使用 useId 生成 SSR 安全的 ID
|
||||
- [ ] 使用 onWatcherCleanup 处理复杂清理逻辑
|
||||
|
||||
### Watchers
|
||||
- [ ] watch/watchEffect 有适当的清理函数
|
||||
- [ ] 异步 watch 处理了竞态条件
|
||||
- [ ] flush: 'post' 用于 DOM 操作的 watcher
|
||||
- [ ] 避免过度使用 watcher(优先用 computed)
|
||||
- [ ] 考虑 once: true 用于一次性监听
|
||||
|
||||
### 模板
|
||||
- [ ] v-for 使用唯一且稳定的 key
|
||||
- [ ] v-if 和 v-for 没有在同一元素上
|
||||
- [ ] 事件处理使用方法而非内联复杂逻辑
|
||||
- [ ] 大型列表使用虚拟滚动
|
||||
|
||||
### Composables
|
||||
- [ ] 相关逻辑提取到 composables
|
||||
- [ ] composables 返回响应式引用(不是 .value)
|
||||
- [ ] 纯函数不要包装成 composable
|
||||
- [ ] 副作用在组件卸载时清理
|
||||
- [ ] 使用 effectScope 管理复杂副作用
|
||||
|
||||
### 性能
|
||||
- [ ] 大型组件拆分为小组件
|
||||
- [ ] 使用 defineAsyncComponent 懒加载
|
||||
- [ ] 避免不必要的响应式转换
|
||||
- [ ] v-memo 用于昂贵的列表渲染
|
||||
- [ ] KeepAlive 用于缓存动态组件
|
||||
Reference in New Issue
Block a user