feat: Initial agent-skills repo — 4 adapted skills for Mosaic Stack

Skills included:
- pr-reviewer: Adapted for Gitea/GitHub via platform-aware scripts
  (dropped fetch_pr_data.py and add_inline_comment.py, kept generate_review_files.py)
- code-review-excellence: Methodology and checklists (React, TS, Python, etc.)
- vercel-react-best-practices: 57 rules for React/Next.js performance
- tailwind-design-system: Tailwind CSS v4 patterns, CVA, design tokens

New shell scripts added to ~/.claude/scripts/git/:
- pr-diff.sh: Get PR diff (GitHub gh / Gitea API)
- pr-metadata.sh: Get PR metadata as normalized JSON

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-16 16:03:39 -06:00
commit d9bcdc4a8d
87 changed files with 19317 additions and 0 deletions

View File

@@ -0,0 +1,198 @@
---
name: code-review-excellence
description: |
Provides comprehensive code review guidance for React 19, Vue 3, Rust, TypeScript, Java, Python, and C/C++.
Helps catch bugs, improve code quality, and give constructive feedback.
Use when: reviewing pull requests, conducting PR reviews, code review, reviewing code changes,
establishing review standards, mentoring developers, architecture reviews, security audits,
checking code quality, finding bugs, giving feedback on code.
allowed-tools:
- Read
- Grep
- Glob
- Bash # 运行 lint/test/build 命令验证代码质量
- WebFetch # 查阅最新文档和最佳实践
---
# Code Review Excellence
Transform code reviews from gatekeeping to knowledge sharing through constructive feedback, systematic analysis, and collaborative improvement.
## When to Use This Skill
- Reviewing pull requests and code changes
- Establishing code review standards for teams
- Mentoring junior developers through reviews
- Conducting architecture reviews
- Creating review checklists and guidelines
- Improving team collaboration
- Reducing code review cycle time
- Maintaining code quality standards
## Core Principles
### 1. The Review Mindset
**Goals of Code Review:**
- Catch bugs and edge cases
- Ensure code maintainability
- Share knowledge across team
- Enforce coding standards
- Improve design and architecture
- Build team culture
**Not the Goals:**
- Show off knowledge
- Nitpick formatting (use linters)
- Block progress unnecessarily
- Rewrite to your preference
### 2. Effective Feedback
**Good Feedback is:**
- Specific and actionable
- Educational, not judgmental
- Focused on the code, not the person
- Balanced (praise good work too)
- Prioritized (critical vs nice-to-have)
```markdown
❌ Bad: "This is wrong."
✅ Good: "This could cause a race condition when multiple users
access simultaneously. Consider using a mutex here."
❌ Bad: "Why didn't you use X pattern?"
✅ Good: "Have you considered the Repository pattern? It would
make this easier to test. Here's an example: [link]"
❌ Bad: "Rename this variable."
✅ Good: "[nit] Consider `userCount` instead of `uc` for
clarity. Not blocking if you prefer to keep it."
```
### 3. Review Scope
**What to Review:**
- Logic correctness and edge cases
- Security vulnerabilities
- Performance implications
- Test coverage and quality
- Error handling
- Documentation and comments
- API design and naming
- Architectural fit
**What Not to Review Manually:**
- Code formatting (use Prettier, Black, etc.)
- Import organization
- Linting violations
- Simple typos
## Review Process
### Phase 1: Context Gathering (2-3 minutes)
Before diving into code, understand:
1. Read PR description and linked issue
2. Check PR size (>400 lines? Ask to split)
3. Review CI/CD status (tests passing?)
4. Understand the business requirement
5. Note any relevant architectural decisions
### Phase 2: High-Level Review (5-10 minutes)
1. **Architecture & Design** - Does the solution fit the problem?
- For significant changes, consult [Architecture Review Guide](reference/architecture-review-guide.md)
- Check: SOLID principles, coupling/cohesion, anti-patterns
2. **Performance Assessment** - Are there performance concerns?
- For performance-critical code, consult [Performance Review Guide](reference/performance-review-guide.md)
- Check: Algorithm complexity, N+1 queries, memory usage
3. **File Organization** - Are new files in the right places?
4. **Testing Strategy** - Are there tests covering edge cases?
### Phase 3: Line-by-Line Review (10-20 minutes)
For each file, check:
- **Logic & Correctness** - Edge cases, off-by-one, null checks, race conditions
- **Security** - Input validation, injection risks, XSS, sensitive data
- **Performance** - N+1 queries, unnecessary loops, memory leaks
- **Maintainability** - Clear names, single responsibility, comments
### Phase 4: Summary & Decision (2-3 minutes)
1. Summarize key concerns
2. Highlight what you liked
3. Make clear decision:
- ✅ Approve
- 💬 Comment (minor suggestions)
- 🔄 Request Changes (must address)
4. Offer to pair if complex
## Review Techniques
### Technique 1: The Checklist Method
Use checklists for consistent reviews. See [Security Review Guide](reference/security-review-guide.md) for comprehensive security checklist.
### Technique 2: The Question Approach
Instead of stating problems, ask questions:
```markdown
❌ "This will fail if the list is empty."
✅ "What happens if `items` is an empty array?"
❌ "You need error handling here."
✅ "How should this behave if the API call fails?"
```
### Technique 3: Suggest, Don't Command
Use collaborative language:
```markdown
❌ "You must change this to use async/await"
✅ "Suggestion: async/await might make this more readable. What do you think?"
❌ "Extract this into a function"
✅ "This logic appears in 3 places. Would it make sense to extract it?"
```
### Technique 4: Differentiate Severity
Use labels to indicate priority:
- 🔴 `[blocking]` - Must fix before merge
- 🟡 `[important]` - Should fix, discuss if disagree
- 🟢 `[nit]` - Nice to have, not blocking
- 💡 `[suggestion]` - Alternative approach to consider
- 📚 `[learning]` - Educational comment, no action needed
- 🎉 `[praise]` - Good work, keep it up!
## Language-Specific Guides
根据审查的代码语言,查阅对应的详细指南:
| Language/Framework | Reference File | Key Topics |
|-------------------|----------------|------------|
| **React** | [React Guide](reference/react.md) | Hooks, useEffect, React 19 Actions, RSC, Suspense, TanStack Query v5 |
| **Vue 3** | [Vue Guide](reference/vue.md) | Composition API, 响应性系统, Props/Emits, Watchers, Composables |
| **Rust** | [Rust Guide](reference/rust.md) | 所有权/借用, Unsafe 审查, 异步代码, 错误处理 |
| **TypeScript** | [TypeScript Guide](reference/typescript.md) | 类型安全, async/await, 不可变性 |
| **Python** | [Python Guide](reference/python.md) | 可变默认参数, 异常处理, 类属性 |
| **Java** | [Java Guide](reference/java.md) | Java 17/21 新特性, Spring Boot 3, 虚拟线程, Stream/Optional |
| **Go** | [Go Guide](reference/go.md) | 错误处理, goroutine/channel, context, 接口设计 |
| **C** | [C Guide](reference/c.md) | 指针/缓冲区, 内存安全, UB, 错误处理 |
| **C++** | [C++ Guide](reference/cpp.md) | RAII, 生命周期, Rule of 0/3/5, 异常安全 |
| **CSS/Less/Sass** | [CSS Guide](reference/css-less-sass.md) | 变量规范, !important, 性能优化, 响应式, 兼容性 |
| **Qt** | [Qt Guide](reference/qt.md) | 对象模型, 信号/槽, 内存管理, 线程安全, 性能 |
## Additional Resources
- [Architecture Review Guide](reference/architecture-review-guide.md) - 架构设计审查指南SOLID、反模式、耦合度
- [Performance Review Guide](reference/performance-review-guide.md) - 性能审查指南Web Vitals、N+1、复杂度
- [Common Bugs Checklist](reference/common-bugs-checklist.md) - 按语言分类的常见错误清单
- [Security Review Guide](reference/security-review-guide.md) - 安全审查指南
- [Code Review Best Practices](reference/code-review-best-practices.md) - 代码审查最佳实践
- [PR Review Template](assets/pr-review-template.md) - PR 审查评论模板
- [Review Checklist](assets/review-checklist.md) - 快速参考清单

View File

@@ -0,0 +1,114 @@
# PR Review Template
Copy and use this template for your code reviews.
---
## Summary
[Brief overview of what was reviewed - 1-2 sentences]
**PR Size:** [Small/Medium/Large] (~X lines)
**Review Time:** [X minutes]
## Strengths
- [What was done well]
- [Good patterns or approaches used]
- [Improvements from previous code]
## Required Changes
🔴 **[blocking]** [Issue description]
> [Code location or example]
> [Suggested fix or explanation]
🔴 **[blocking]** [Issue description]
> [Details]
## Important Suggestions
🟡 **[important]** [Issue description]
> [Why this matters]
> [Suggested approach]
## Minor Suggestions
🟢 **[nit]** [Minor improvement suggestion]
💡 **[suggestion]** [Alternative approach to consider]
## Questions
❓ [Clarification needed about X]
❓ [Question about design decision Y]
## Security Considerations
- [ ] No hardcoded secrets
- [ ] Input validation present
- [ ] Authorization checks in place
- [ ] No SQL/XSS injection risks
## Test Coverage
- [ ] Unit tests added/updated
- [ ] Edge cases covered
- [ ] Error cases tested
## Verdict
**[ ] ✅ Approve** - Ready to merge
**[ ] 💬 Comment** - Minor suggestions, can merge
**[ ] 🔄 Request Changes** - Must address blocking issues
---
## Quick Copy Templates
### Blocking Issue
```
🔴 **[blocking]** [Title]
[Description of the issue]
**Location:** `file.ts:123`
**Suggested fix:**
\`\`\`typescript
// Your suggested code
\`\`\`
```
### Important Suggestion
```
🟡 **[important]** [Title]
[Why this is important]
**Consider:**
- Option A: [description]
- Option B: [description]
```
### Minor Suggestion
```
🟢 **[nit]** [Suggestion]
Not blocking, but consider [improvement].
```
### Praise
```
🎉 **[praise]** Great work on [specific thing]!
[Why this is good]
```
### Question
```
❓ **[question]** [Your question]
I'm curious about the decision to [X]. Could you explain [Y]?
```

View File

@@ -0,0 +1,121 @@
# Code Review Quick Checklist
Quick reference checklist for code reviews.
## Pre-Review (2 min)
- [ ] Read PR description and linked issue
- [ ] Check PR size (<400 lines ideal)
- [ ] Verify CI/CD status (tests passing?)
- [ ] Understand the business requirement
## Architecture & Design (5 min)
- [ ] Solution fits the problem
- [ ] Consistent with existing patterns
- [ ] No simpler approach exists
- [ ] Will it scale?
- [ ] Changes in right location
## Logic & Correctness (10 min)
- [ ] Edge cases handled
- [ ] Null/undefined checks present
- [ ] Off-by-one errors checked
- [ ] Race conditions considered
- [ ] Error handling complete
- [ ] Correct data types used
## Security (5 min)
- [ ] No hardcoded secrets
- [ ] Input validated/sanitized
- [ ] SQL injection prevented
- [ ] XSS prevented
- [ ] Authorization checks present
- [ ] Sensitive data protected
## Performance (3 min)
- [ ] No N+1 queries
- [ ] Expensive operations optimized
- [ ] Large lists paginated
- [ ] No memory leaks
- [ ] Caching considered where appropriate
## Testing (5 min)
- [ ] Tests exist for new code
- [ ] Edge cases tested
- [ ] Error cases tested
- [ ] Tests are readable
- [ ] Tests are deterministic
## Code Quality (3 min)
- [ ] Clear variable/function names
- [ ] No code duplication
- [ ] Functions do one thing
- [ ] Complex code commented
- [ ] No magic numbers
## Documentation (2 min)
- [ ] Public APIs documented
- [ ] README updated if needed
- [ ] Breaking changes noted
- [ ] Complex logic explained
---
## Severity Labels
| Label | Meaning | Action |
|-------|---------|--------|
| 🔴 `[blocking]` | Must fix | Block merge |
| 🟡 `[important]` | Should fix | Discuss if disagree |
| 🟢 `[nit]` | Nice to have | Non-blocking |
| 💡 `[suggestion]` | Alternative | Consider |
| ❓ `[question]` | Need clarity | Respond |
| 🎉 `[praise]` | Good work | Celebrate! |
---
## Decision Matrix
| Situation | Decision |
|-----------|----------|
| Critical security issue | 🔴 Block, fix immediately |
| Breaking change without migration | 🔴 Block |
| Missing error handling | 🟡 Should fix |
| No tests for new code | 🟡 Should fix |
| Style preference | 🟢 Non-blocking |
| Minor naming improvement | 🟢 Non-blocking |
| Clever but working code | 💡 Suggest simpler |
---
## Time Budget
| PR Size | Target Time |
|---------|-------------|
| < 100 lines | 10-15 min |
| 100-400 lines | 20-40 min |
| > 400 lines | Ask to split |
---
## Red Flags
Watch for these patterns:
- `// TODO` in production code
- `console.log` left in code
- Commented out code
- `any` type in TypeScript
- Empty catch blocks
- `unwrap()` in Rust production code
- Magic numbers/strings
- Copy-pasted code blocks
- Missing null checks
- Hardcoded URLs/credentials

View File

@@ -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)

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -0,0 +1,656 @@
# 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
/* 变量 - 用于单个值 */
$primary-color: #3b82f6;
/* Mixin - 用于可配置的代码块 */
@mixin button-variant($bg, $text) {
background: $bg;
color: $text;
&:hover {
background: darken($bg, 10%);
}
}
/* Extend - 用于共享相同样式(谨慎使用) */
%visually-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
}
.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)

View File

@@ -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)

View File

@@ -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 方法上加 @TransactionalAOP 不生效)
@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 在多线程环境可能死循环或数据丢失
// ✅ 使用 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
- [ ] 魔法值提取为常量或枚举

View File

@@ -0,0 +1,752 @@
# 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 监控 |
---
## 参考资源
- [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/)

File diff suppressed because it is too large Load Diff

View File

@@ -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`?

View File

@@ -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 查询
- [ ] 测试行为而非实现细节

View File

@@ -0,0 +1,840 @@
# 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! {
// 如果 timeout 先完成read 被取消
// 部分读取的数据可能丢失!
result = stream.read(&mut buffer) => {
handle_data(&buffer[..result?]);
}
_ = 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! {
// tokio::io::AsyncReadExt::read 是取消安全的
// 取消时,未读取的数据留在流中
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 有文档示例

View File

@@ -0,0 +1,265 @@
# 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 filePath = path.join('./uploads', safeName);
// Verify it's still within uploads directory
if (!filePath.startsWith(path.resolve('./uploads'))) {
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 |

View File

@@ -0,0 +1,543 @@
# 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
// .eslintrc.js
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:@typescript-eslint/strict'
],
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

View File

@@ -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>
<!-- useIdSSR 安全的唯一 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-modelVue 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 用于缓存动态组件

View File

@@ -0,0 +1,366 @@
#!/usr/bin/env python3
"""
PR Analyzer - Analyze PR complexity and suggest review approach.
Usage:
python pr-analyzer.py [--diff-file FILE] [--stats]
Or pipe diff directly:
git diff main...HEAD | python pr-analyzer.py
"""
import sys
import re
import argparse
from collections import defaultdict
from dataclasses import dataclass
from typing import List, Dict, Optional
@dataclass
class FileStats:
"""Statistics for a single file."""
filename: str
additions: int = 0
deletions: int = 0
is_test: bool = False
is_config: bool = False
language: str = "unknown"
@dataclass
class PRAnalysis:
"""Complete PR analysis results."""
total_files: int
total_additions: int
total_deletions: int
files: List[FileStats]
complexity_score: float
size_category: str
estimated_review_time: int
risk_factors: List[str]
suggestions: List[str]
def detect_language(filename: str) -> str:
"""Detect programming language from filename."""
extensions = {
'.py': 'Python',
'.js': 'JavaScript',
'.ts': 'TypeScript',
'.tsx': 'TypeScript/React',
'.jsx': 'JavaScript/React',
'.rs': 'Rust',
'.go': 'Go',
'.c': 'C',
'.h': 'C/C++',
'.cpp': 'C++',
'.hpp': 'C++',
'.cc': 'C++',
'.cxx': 'C++',
'.hh': 'C++',
'.hxx': 'C++',
'.java': 'Java',
'.rb': 'Ruby',
'.sql': 'SQL',
'.md': 'Markdown',
'.json': 'JSON',
'.yaml': 'YAML',
'.yml': 'YAML',
'.toml': 'TOML',
'.css': 'CSS',
'.scss': 'SCSS',
'.html': 'HTML',
}
for ext, lang in extensions.items():
if filename.endswith(ext):
return lang
return 'unknown'
def is_test_file(filename: str) -> bool:
"""Check if file is a test file."""
test_patterns = [
r'test_.*\.py$',
r'.*_test\.py$',
r'.*\.test\.(js|ts|tsx)$',
r'.*\.spec\.(js|ts|tsx)$',
r'tests?/',
r'__tests__/',
]
return any(re.search(p, filename) for p in test_patterns)
def is_config_file(filename: str) -> bool:
"""Check if file is a configuration file."""
config_patterns = [
r'\.env',
r'config\.',
r'\.json$',
r'\.yaml$',
r'\.yml$',
r'\.toml$',
r'Cargo\.toml$',
r'package\.json$',
r'tsconfig\.json$',
]
return any(re.search(p, filename) for p in config_patterns)
def parse_diff(diff_content: str) -> List[FileStats]:
"""Parse git diff output and extract file statistics."""
files = []
current_file = None
for line in diff_content.split('\n'):
# New file header
if line.startswith('diff --git'):
if current_file:
files.append(current_file)
# Extract filename from "diff --git a/path b/path"
match = re.search(r'b/(.+)$', line)
if match:
filename = match.group(1)
current_file = FileStats(
filename=filename,
language=detect_language(filename),
is_test=is_test_file(filename),
is_config=is_config_file(filename),
)
elif current_file:
if line.startswith('+') and not line.startswith('+++'):
current_file.additions += 1
elif line.startswith('-') and not line.startswith('---'):
current_file.deletions += 1
if current_file:
files.append(current_file)
return files
def calculate_complexity(files: List[FileStats]) -> float:
"""Calculate complexity score (0-1 scale)."""
if not files:
return 0.0
total_changes = sum(f.additions + f.deletions for f in files)
# Base complexity from size
size_factor = min(total_changes / 1000, 1.0)
# Factor for number of files
file_factor = min(len(files) / 20, 1.0)
# Factor for non-test code ratio
test_lines = sum(f.additions + f.deletions for f in files if f.is_test)
non_test_ratio = 1 - (test_lines / max(total_changes, 1))
# Factor for language diversity
languages = set(f.language for f in files if f.language != 'unknown')
lang_factor = min(len(languages) / 5, 1.0)
complexity = (
size_factor * 0.4 +
file_factor * 0.2 +
non_test_ratio * 0.2 +
lang_factor * 0.2
)
return round(complexity, 2)
def categorize_size(total_changes: int) -> str:
"""Categorize PR size."""
if total_changes < 50:
return "XS (Extra Small)"
elif total_changes < 200:
return "S (Small)"
elif total_changes < 400:
return "M (Medium)"
elif total_changes < 800:
return "L (Large)"
else:
return "XL (Extra Large) - Consider splitting"
def estimate_review_time(files: List[FileStats], complexity: float) -> int:
"""Estimate review time in minutes."""
total_changes = sum(f.additions + f.deletions for f in files)
# Base time: ~1 minute per 20 lines
base_time = total_changes / 20
# Adjust for complexity
adjusted_time = base_time * (1 + complexity)
# Minimum 5 minutes, maximum 120 minutes
return max(5, min(120, int(adjusted_time)))
def identify_risk_factors(files: List[FileStats]) -> List[str]:
"""Identify potential risk factors in the PR."""
risks = []
total_changes = sum(f.additions + f.deletions for f in files)
test_changes = sum(f.additions + f.deletions for f in files if f.is_test)
# Large PR
if total_changes > 400:
risks.append("Large PR (>400 lines) - harder to review thoroughly")
# No tests
if test_changes == 0 and total_changes > 50:
risks.append("No test changes - verify test coverage")
# Low test ratio
if total_changes > 100 and test_changes / max(total_changes, 1) < 0.2:
risks.append("Low test ratio (<20%) - consider adding more tests")
# Security-sensitive files
security_patterns = ['.env', 'auth', 'security', 'password', 'token', 'secret']
for f in files:
if any(p in f.filename.lower() for p in security_patterns):
risks.append(f"Security-sensitive file: {f.filename}")
break
# Database changes
for f in files:
if 'migration' in f.filename.lower() or f.language == 'SQL':
risks.append("Database changes detected - review carefully")
break
# Config changes
config_files = [f for f in files if f.is_config]
if config_files:
risks.append(f"Configuration changes in {len(config_files)} file(s)")
return risks
def generate_suggestions(files: List[FileStats], complexity: float, risks: List[str]) -> List[str]:
"""Generate review suggestions."""
suggestions = []
total_changes = sum(f.additions + f.deletions for f in files)
if total_changes > 800:
suggestions.append("Consider splitting this PR into smaller, focused changes")
if complexity > 0.7:
suggestions.append("High complexity - allocate extra review time")
suggestions.append("Consider pair reviewing for critical sections")
if "No test changes" in str(risks):
suggestions.append("Request test additions before approval")
# Language-specific suggestions
languages = set(f.language for f in files)
if 'TypeScript' in languages or 'TypeScript/React' in languages:
suggestions.append("Check for proper type usage (avoid 'any')")
if 'Rust' in languages:
suggestions.append("Check for unwrap() usage and error handling")
if 'C' in languages or 'C++' in languages or 'C/C++' in languages:
suggestions.append("Check for memory safety, bounds checks, and UB risks")
if 'SQL' in languages:
suggestions.append("Review for SQL injection and query performance")
if not suggestions:
suggestions.append("Standard review process should suffice")
return suggestions
def analyze_pr(diff_content: str) -> PRAnalysis:
"""Perform complete PR analysis."""
files = parse_diff(diff_content)
total_additions = sum(f.additions for f in files)
total_deletions = sum(f.deletions for f in files)
total_changes = total_additions + total_deletions
complexity = calculate_complexity(files)
risks = identify_risk_factors(files)
suggestions = generate_suggestions(files, complexity, risks)
return PRAnalysis(
total_files=len(files),
total_additions=total_additions,
total_deletions=total_deletions,
files=files,
complexity_score=complexity,
size_category=categorize_size(total_changes),
estimated_review_time=estimate_review_time(files, complexity),
risk_factors=risks,
suggestions=suggestions,
)
def print_analysis(analysis: PRAnalysis, show_files: bool = False):
"""Print analysis results."""
print("\n" + "=" * 60)
print("PR ANALYSIS REPORT")
print("=" * 60)
print(f"\n📊 SUMMARY")
print(f" Files changed: {analysis.total_files}")
print(f" Additions: +{analysis.total_additions}")
print(f" Deletions: -{analysis.total_deletions}")
print(f" Total changes: {analysis.total_additions + analysis.total_deletions}")
print(f"\n📏 SIZE: {analysis.size_category}")
print(f" Complexity score: {analysis.complexity_score}/1.0")
print(f" Estimated review time: ~{analysis.estimated_review_time} minutes")
if analysis.risk_factors:
print(f"\n⚠️ RISK FACTORS:")
for risk in analysis.risk_factors:
print(f"{risk}")
print(f"\n💡 SUGGESTIONS:")
for suggestion in analysis.suggestions:
print(f"{suggestion}")
if show_files:
print(f"\n📁 FILES:")
# Group by language
by_lang: Dict[str, List[FileStats]] = defaultdict(list)
for f in analysis.files:
by_lang[f.language].append(f)
for lang, lang_files in sorted(by_lang.items()):
print(f"\n [{lang}]")
for f in lang_files:
prefix = "🧪" if f.is_test else "⚙️" if f.is_config else "📄"
print(f" {prefix} {f.filename} (+{f.additions}/-{f.deletions})")
print("\n" + "=" * 60)
def main():
parser = argparse.ArgumentParser(description='Analyze PR complexity')
parser.add_argument('--diff-file', '-f', help='Path to diff file')
parser.add_argument('--stats', '-s', action='store_true', help='Show file details')
args = parser.parse_args()
# Read diff from file or stdin
if args.diff_file:
with open(args.diff_file, 'r') as f:
diff_content = f.read()
elif not sys.stdin.isatty():
diff_content = sys.stdin.read()
else:
print("Usage: git diff main...HEAD | python pr-analyzer.py")
print(" python pr-analyzer.py -f diff.txt")
sys.exit(1)
if not diff_content.strip():
print("No diff content provided")
sys.exit(1)
analysis = analyze_pr(diff_content)
print_analysis(analysis, show_files=args.stats)
if __name__ == '__main__':
main()