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:
198
skills/code-review-excellence/SKILL.md
Normal file
198
skills/code-review-excellence/SKILL.md
Normal 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) - 快速参考清单
|
||||
114
skills/code-review-excellence/assets/pr-review-template.md
Normal file
114
skills/code-review-excellence/assets/pr-review-template.md
Normal 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]?
|
||||
```
|
||||
121
skills/code-review-excellence/assets/review-checklist.md
Normal file
121
skills/code-review-excellence/assets/review-checklist.md
Normal 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
|
||||
@@ -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)
|
||||
285
skills/code-review-excellence/reference/c.md
Normal file
285
skills/code-review-excellence/reference/c.md
Normal 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
|
||||
@@ -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
|
||||
1227
skills/code-review-excellence/reference/common-bugs-checklist.md
Normal file
1227
skills/code-review-excellence/reference/common-bugs-checklist.md
Normal file
File diff suppressed because it is too large
Load Diff
385
skills/code-review-excellence/reference/cpp.md
Normal file
385
skills/code-review-excellence/reference/cpp.md
Normal 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
|
||||
656
skills/code-review-excellence/reference/css-less-sass.md
Normal file
656
skills/code-review-excellence/reference/css-less-sass.md
Normal 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)
|
||||
989
skills/code-review-excellence/reference/go.md
Normal file
989
skills/code-review-excellence/reference/go.md
Normal 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)
|
||||
405
skills/code-review-excellence/reference/java.md
Normal file
405
skills/code-review-excellence/reference/java.md
Normal 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 方法上加 @Transactional(AOP 不生效)
|
||||
@Transactional
|
||||
private void saveInternal() { ... }
|
||||
|
||||
// ✅ 在 Service 层公共方法加 @Transactional
|
||||
// ✅ 读操作显式标记 readOnly = true (性能优化)
|
||||
@Service
|
||||
public class UserService {
|
||||
@Transactional(readOnly = true)
|
||||
public User getUser(Long id) { ... }
|
||||
|
||||
@Transactional
|
||||
public void createUser(UserDto dto) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Entity 设计
|
||||
|
||||
```java
|
||||
// ❌ 在 Entity 中使用 Lombok @Data
|
||||
// @Data 生成的 equals/hashCode 包含所有字段,可能触发懒加载导致性能问题或异常
|
||||
@Entity
|
||||
@Data
|
||||
public class User { ... }
|
||||
|
||||
// ✅ 仅使用 @Getter, @Setter
|
||||
// ✅ 自定义 equals/hashCode (通常基于 ID)
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
public class User {
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof User)) return false;
|
||||
return id != null && id.equals(((User) o).id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 并发与虚拟线程
|
||||
|
||||
### 虚拟线程 (Java 21+)
|
||||
|
||||
```java
|
||||
// ❌ 传统线程池处理大量 I/O 阻塞任务(资源耗尽)
|
||||
ExecutorService executor = Executors.newFixedThreadPool(100);
|
||||
|
||||
// ✅ 使用虚拟线程处理 I/O 密集型任务(高吞吐量)
|
||||
// Spring Boot 3.2+ 开启:spring.threads.virtual.enabled=true
|
||||
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
|
||||
|
||||
// 在虚拟线程中,阻塞操作(如 DB 查询、HTTP 请求)几乎不消耗 OS 线程资源
|
||||
```
|
||||
|
||||
### 线程安全
|
||||
|
||||
```java
|
||||
// ❌ SimpleDateFormat 是线程不安全的
|
||||
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||
|
||||
// ✅ 使用 DateTimeFormatter (Java 8+)
|
||||
private static final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
// ❌ HashMap 在多线程环境可能死循环或数据丢失
|
||||
// ✅ 使用 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)
|
||||
- [ ] 魔法值提取为常量或枚举
|
||||
@@ -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/)
|
||||
1069
skills/code-review-excellence/reference/python.md
Normal file
1069
skills/code-review-excellence/reference/python.md
Normal file
File diff suppressed because it is too large
Load Diff
186
skills/code-review-excellence/reference/qt.md
Normal file
186
skills/code-review-excellence/reference/qt.md
Normal 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`?
|
||||
871
skills/code-review-excellence/reference/react.md
Normal file
871
skills/code-review-excellence/reference/react.md
Normal 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 查询
|
||||
- [ ] 测试行为而非实现细节
|
||||
840
skills/code-review-excellence/reference/rust.md
Normal file
840
skills/code-review-excellence/reference/rust.md
Normal 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 有文档示例
|
||||
265
skills/code-review-excellence/reference/security-review-guide.md
Normal file
265
skills/code-review-excellence/reference/security-review-guide.md
Normal 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 |
|
||||
543
skills/code-review-excellence/reference/typescript.md
Normal file
543
skills/code-review-excellence/reference/typescript.md
Normal 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
|
||||
924
skills/code-review-excellence/reference/vue.md
Normal file
924
skills/code-review-excellence/reference/vue.md
Normal 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>
|
||||
|
||||
<!-- ✅ useId:SSR 安全的唯一 ID -->
|
||||
<script setup lang="ts">
|
||||
import { useId } from 'vue'
|
||||
|
||||
const id = useId() // 例如:'v-0'
|
||||
</script>
|
||||
<template>
|
||||
<label :for="id">Name</label>
|
||||
<input :id="id" />
|
||||
</template>
|
||||
|
||||
<!-- ✅ 表单组件中使用 -->
|
||||
<script setup lang="ts">
|
||||
const inputId = useId()
|
||||
const errorId = useId()
|
||||
</script>
|
||||
<template>
|
||||
<label :for="inputId">Email</label>
|
||||
<input
|
||||
:id="inputId"
|
||||
:aria-describedby="errorId"
|
||||
/>
|
||||
<span :id="errorId" class="error">{{ error }}</span>
|
||||
</template>
|
||||
```
|
||||
|
||||
### onWatcherCleanup (3.5+)
|
||||
|
||||
```vue
|
||||
<!-- 传统方式:watch 第三个参数 -->
|
||||
<script setup lang="ts">
|
||||
watch(source, async (value, oldValue, onCleanup) => {
|
||||
const controller = new AbortController()
|
||||
onCleanup(() => controller.abort())
|
||||
// ...
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- ✅ onWatcherCleanup:更灵活的清理 -->
|
||||
<script setup lang="ts">
|
||||
import { onWatcherCleanup } from 'vue'
|
||||
|
||||
watch(source, async (value) => {
|
||||
const controller = new AbortController()
|
||||
onWatcherCleanup(() => controller.abort())
|
||||
|
||||
// 可以在任意位置调用,不限于回调开头
|
||||
if (someCondition) {
|
||||
const anotherResource = createResource()
|
||||
onWatcherCleanup(() => anotherResource.dispose())
|
||||
}
|
||||
|
||||
await fetchData(value, controller.signal)
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### Deferred Teleport (3.5+)
|
||||
|
||||
```vue
|
||||
<!-- ❌ Teleport 目标必须在挂载时存在 -->
|
||||
<template>
|
||||
<Teleport to="#modal-container">
|
||||
<!-- 如果 #modal-container 不存在会报错 -->
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<!-- ✅ defer 属性延迟挂载 -->
|
||||
<template>
|
||||
<Teleport to="#modal-container" defer>
|
||||
<!-- 等待目标元素存在后再挂载 -->
|
||||
<Modal />
|
||||
</Teleport>
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Watchers
|
||||
|
||||
### watch vs watchEffect
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// ✅ watch:明确指定依赖,惰性执行
|
||||
watch(
|
||||
() => props.userId,
|
||||
async (userId) => {
|
||||
user.value = await fetchUser(userId)
|
||||
}
|
||||
)
|
||||
|
||||
// ✅ watchEffect:自动收集依赖,立即执行
|
||||
watchEffect(async () => {
|
||||
// 自动追踪 props.userId
|
||||
user.value = await fetchUser(props.userId)
|
||||
})
|
||||
|
||||
// 💡 选择指南:
|
||||
// - 需要旧值?用 watch
|
||||
// - 需要惰性执行?用 watch
|
||||
// - 依赖复杂?用 watchEffect
|
||||
</script>
|
||||
```
|
||||
|
||||
### watch 清理函数
|
||||
|
||||
```vue
|
||||
<!-- ❌ watch 缺少清理函数,可能内存泄漏 -->
|
||||
<script setup lang="ts">
|
||||
watch(searchQuery, async (query) => {
|
||||
const controller = new AbortController()
|
||||
const data = await fetch(`/api/search?q=${query}`, {
|
||||
signal: controller.signal
|
||||
})
|
||||
results.value = await data.json()
|
||||
// 如果 query 快速变化,旧请求不会被取消!
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 onCleanup 清理副作用 -->
|
||||
<script setup lang="ts">
|
||||
watch(searchQuery, async (query, _, onCleanup) => {
|
||||
const controller = new AbortController()
|
||||
onCleanup(() => controller.abort()) // 取消旧请求
|
||||
|
||||
try {
|
||||
const data = await fetch(`/api/search?q=${query}`, {
|
||||
signal: controller.signal
|
||||
})
|
||||
results.value = await data.json()
|
||||
} catch (e) {
|
||||
if (e.name !== 'AbortError') throw e
|
||||
}
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### watch 选项
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// ✅ immediate:立即执行一次
|
||||
watch(
|
||||
userId,
|
||||
async (id) => {
|
||||
user.value = await fetchUser(id)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// ✅ deep:深度监听(性能开销大,谨慎使用)
|
||||
watch(
|
||||
state,
|
||||
(newState) => {
|
||||
console.log('State changed deeply')
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// ✅ flush: 'post':DOM 更新后执行
|
||||
watch(
|
||||
source,
|
||||
() => {
|
||||
// 可以安全访问更新后的 DOM
|
||||
// nextTick 不再需要
|
||||
},
|
||||
{ flush: 'post' }
|
||||
)
|
||||
|
||||
// ✅ once: true (Vue 3.4+):只执行一次
|
||||
watch(
|
||||
source,
|
||||
(value) => {
|
||||
console.log('只会执行一次:', value)
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
</script>
|
||||
```
|
||||
|
||||
### 监听多个源
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// ✅ 监听多个 ref
|
||||
watch(
|
||||
[firstName, lastName],
|
||||
([newFirst, newLast], [oldFirst, oldLast]) => {
|
||||
console.log(`Name changed from ${oldFirst} ${oldLast} to ${newFirst} ${newLast}`)
|
||||
}
|
||||
)
|
||||
|
||||
// ✅ 监听 reactive 对象的特定属性
|
||||
watch(
|
||||
() => [state.count, state.name],
|
||||
([count, name]) => {
|
||||
console.log(`count: ${count}, name: ${name}`)
|
||||
}
|
||||
)
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 模板最佳实践
|
||||
|
||||
### v-for 的 key
|
||||
|
||||
```vue
|
||||
<!-- ❌ v-for 中使用 index 作为 key -->
|
||||
<template>
|
||||
<li v-for="(item, index) in items" :key="index">
|
||||
{{ item.name }}
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 使用唯一标识作为 key -->
|
||||
<template>
|
||||
<li v-for="item in items" :key="item.id">
|
||||
{{ item.name }}
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 复合 key(当没有唯一 ID 时)-->
|
||||
<template>
|
||||
<li v-for="(item, index) in items" :key="`${item.name}-${item.type}-${index}`">
|
||||
{{ item.name }}
|
||||
</li>
|
||||
</template>
|
||||
```
|
||||
|
||||
### v-if 和 v-for 优先级
|
||||
|
||||
```vue
|
||||
<!-- ❌ v-if 和 v-for 同时使用 -->
|
||||
<template>
|
||||
<li v-for="user in users" v-if="user.active" :key="user.id">
|
||||
{{ user.name }}
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 使用 computed 过滤 -->
|
||||
<script setup lang="ts">
|
||||
const activeUsers = computed(() =>
|
||||
users.value.filter(user => user.active)
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<li v-for="user in activeUsers" :key="user.id">
|
||||
{{ user.name }}
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 或用 template 包裹 -->
|
||||
<template>
|
||||
<template v-for="user in users" :key="user.id">
|
||||
<li v-if="user.active">
|
||||
{{ user.name }}
|
||||
</li>
|
||||
</template>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 事件处理
|
||||
|
||||
```vue
|
||||
<!-- ❌ 内联复杂逻辑 -->
|
||||
<template>
|
||||
<button @click="items = items.filter(i => i.id !== item.id); count--">
|
||||
Delete
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 使用方法 -->
|
||||
<script setup lang="ts">
|
||||
const deleteItem = (id: number) => {
|
||||
items.value = items.value.filter(i => i.id !== id)
|
||||
count.value--
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<button @click="deleteItem(item.id)">Delete</button>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 事件修饰符 -->
|
||||
<template>
|
||||
<!-- 阻止默认行为 -->
|
||||
<form @submit.prevent="handleSubmit">...</form>
|
||||
|
||||
<!-- 阻止冒泡 -->
|
||||
<button @click.stop="handleClick">...</button>
|
||||
|
||||
<!-- 只执行一次 -->
|
||||
<button @click.once="handleOnce">...</button>
|
||||
|
||||
<!-- 键盘修饰符 -->
|
||||
<input @keyup.enter="submit" @keyup.esc="cancel" />
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Composables
|
||||
|
||||
### Composable 设计原则
|
||||
|
||||
```typescript
|
||||
// ✅ 好的 composable 设计
|
||||
export function useCounter(initialValue = 0) {
|
||||
const count = ref(initialValue)
|
||||
|
||||
const increment = () => count.value++
|
||||
const decrement = () => count.value--
|
||||
const reset = () => count.value = initialValue
|
||||
|
||||
// 返回响应式引用和方法
|
||||
return {
|
||||
count: readonly(count), // 只读防止外部修改
|
||||
increment,
|
||||
decrement,
|
||||
reset
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 不要返回 .value
|
||||
export function useBadCounter() {
|
||||
const count = ref(0)
|
||||
return {
|
||||
count: count.value // ❌ 丢失响应性!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Props 传递给 composable
|
||||
|
||||
```vue
|
||||
<!-- ❌ 传递 props 到 composable 丢失响应性 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ userId: string }>()
|
||||
const { user } = useUser(props.userId) // 丢失响应性!
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 toRef 或 computed 保持响应性 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ userId: string }>()
|
||||
const userIdRef = toRef(props, 'userId')
|
||||
const { user } = useUser(userIdRef) // 保持响应性
|
||||
// 或使用 computed
|
||||
const { user } = useUser(computed(() => props.userId))
|
||||
|
||||
// ✅ Vue 3.5+:直接解构使用
|
||||
const { userId } = defineProps<{ userId: string }>()
|
||||
const { user } = useUser(() => userId) // getter 函数
|
||||
</script>
|
||||
```
|
||||
|
||||
### 异步 Composable
|
||||
|
||||
```typescript
|
||||
// ✅ 异步 composable 模式
|
||||
export function useFetch<T>(url: MaybeRefOrGetter<string>) {
|
||||
const data = ref<T | null>(null)
|
||||
const error = ref<Error | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const execute = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await fetch(toValue(url))
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
}
|
||||
data.value = await response.json()
|
||||
} catch (e) {
|
||||
error.value = e as Error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式 URL 时自动重新获取
|
||||
watchEffect(() => {
|
||||
toValue(url) // 追踪依赖
|
||||
execute()
|
||||
})
|
||||
|
||||
return {
|
||||
data: readonly(data),
|
||||
error: readonly(error),
|
||||
loading: readonly(loading),
|
||||
refetch: execute
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
const { data, loading, error, refetch } = useFetch<User[]>('/api/users')
|
||||
```
|
||||
|
||||
### 生命周期与清理
|
||||
|
||||
```typescript
|
||||
// ✅ Composable 中正确处理生命周期
|
||||
export function useEventListener(
|
||||
target: MaybeRefOrGetter<EventTarget>,
|
||||
event: string,
|
||||
handler: EventListener
|
||||
) {
|
||||
// 组件挂载后添加
|
||||
onMounted(() => {
|
||||
toValue(target).addEventListener(event, handler)
|
||||
})
|
||||
|
||||
// 组件卸载时移除
|
||||
onUnmounted(() => {
|
||||
toValue(target).removeEventListener(event, handler)
|
||||
})
|
||||
}
|
||||
|
||||
// ✅ 使用 effectScope 管理副作用
|
||||
export function useFeature() {
|
||||
const scope = effectScope()
|
||||
|
||||
scope.run(() => {
|
||||
// 所有响应式效果都在这个 scope 内
|
||||
const state = ref(0)
|
||||
watch(state, () => { /* ... */ })
|
||||
watchEffect(() => { /* ... */ })
|
||||
})
|
||||
|
||||
// 清理所有效果
|
||||
onUnmounted(() => scope.stop())
|
||||
|
||||
return { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能优化
|
||||
|
||||
### v-memo
|
||||
|
||||
```vue
|
||||
<!-- ✅ v-memo:缓存子树,避免重复渲染 -->
|
||||
<template>
|
||||
<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
|
||||
<!-- 只有当 item.id === selected 变化时才重新渲染 -->
|
||||
<ExpensiveComponent :item="item" :selected="item.id === selected" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 配合 v-for 使用 -->
|
||||
<template>
|
||||
<div
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
v-memo="[item.name, item.status]"
|
||||
>
|
||||
<!-- 只有 name 或 status 变化时重新渲染 -->
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### defineAsyncComponent
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
// ✅ 懒加载组件
|
||||
const HeavyChart = defineAsyncComponent(() =>
|
||||
import('./components/HeavyChart.vue')
|
||||
)
|
||||
|
||||
// ✅ 带加载和错误状态
|
||||
const AsyncModal = defineAsyncComponent({
|
||||
loader: () => import('./components/Modal.vue'),
|
||||
loadingComponent: LoadingSpinner,
|
||||
errorComponent: ErrorDisplay,
|
||||
delay: 200, // 延迟显示 loading(避免闪烁)
|
||||
timeout: 3000 // 超时时间
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### KeepAlive
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- ✅ 缓存动态组件 -->
|
||||
<KeepAlive>
|
||||
<component :is="currentTab" />
|
||||
</KeepAlive>
|
||||
|
||||
<!-- ✅ 指定缓存的组件 -->
|
||||
<KeepAlive include="TabA,TabB">
|
||||
<component :is="currentTab" />
|
||||
</KeepAlive>
|
||||
|
||||
<!-- ✅ 限制缓存数量 -->
|
||||
<KeepAlive :max="10">
|
||||
<component :is="currentTab" />
|
||||
</KeepAlive>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// KeepAlive 组件的生命周期钩子
|
||||
onActivated(() => {
|
||||
// 组件被激活时(从缓存恢复)
|
||||
refreshData()
|
||||
})
|
||||
|
||||
onDeactivated(() => {
|
||||
// 组件被停用时(进入缓存)
|
||||
pauseTimers()
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### 虚拟列表
|
||||
|
||||
```vue
|
||||
<!-- ✅ 大型列表使用虚拟滚动 -->
|
||||
<script setup lang="ts">
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
|
||||
const { list, containerProps, wrapperProps } = useVirtualList(
|
||||
items,
|
||||
{ itemHeight: 50 }
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<div v-bind="containerProps" style="height: 400px; overflow: auto">
|
||||
<div v-bind="wrapperProps">
|
||||
<div v-for="item in list" :key="item.data.id" style="height: 50px">
|
||||
{{ item.data.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### 响应性系统
|
||||
- [ ] ref 用于基本类型,reactive 用于对象(或统一用 ref)
|
||||
- [ ] 没有解构 reactive 对象(或使用了 toRefs)
|
||||
- [ ] props 传递给 composable 时保持了响应性
|
||||
- [ ] shallowRef/shallowReactive 用于大型对象优化
|
||||
- [ ] computed 中没有副作用
|
||||
|
||||
### Props & Emits
|
||||
- [ ] defineProps 使用 TypeScript 类型声明
|
||||
- [ ] 复杂默认值使用 withDefaults + 工厂函数
|
||||
- [ ] defineEmits 有完整的类型定义
|
||||
- [ ] 没有直接修改 props
|
||||
- [ ] 考虑使用 defineModel 简化 v-model(Vue 3.4+)
|
||||
|
||||
### Vue 3.5 新特性(如适用)
|
||||
- [ ] 使用 Reactive Props Destructure 简化 props 访问
|
||||
- [ ] 使用 useTemplateRef 替代 ref 属性
|
||||
- [ ] 表单使用 useId 生成 SSR 安全的 ID
|
||||
- [ ] 使用 onWatcherCleanup 处理复杂清理逻辑
|
||||
|
||||
### Watchers
|
||||
- [ ] watch/watchEffect 有适当的清理函数
|
||||
- [ ] 异步 watch 处理了竞态条件
|
||||
- [ ] flush: 'post' 用于 DOM 操作的 watcher
|
||||
- [ ] 避免过度使用 watcher(优先用 computed)
|
||||
- [ ] 考虑 once: true 用于一次性监听
|
||||
|
||||
### 模板
|
||||
- [ ] v-for 使用唯一且稳定的 key
|
||||
- [ ] v-if 和 v-for 没有在同一元素上
|
||||
- [ ] 事件处理使用方法而非内联复杂逻辑
|
||||
- [ ] 大型列表使用虚拟滚动
|
||||
|
||||
### Composables
|
||||
- [ ] 相关逻辑提取到 composables
|
||||
- [ ] composables 返回响应式引用(不是 .value)
|
||||
- [ ] 纯函数不要包装成 composable
|
||||
- [ ] 副作用在组件卸载时清理
|
||||
- [ ] 使用 effectScope 管理复杂副作用
|
||||
|
||||
### 性能
|
||||
- [ ] 大型组件拆分为小组件
|
||||
- [ ] 使用 defineAsyncComponent 懒加载
|
||||
- [ ] 避免不必要的响应式转换
|
||||
- [ ] v-memo 用于昂贵的列表渲染
|
||||
- [ ] KeepAlive 用于缓存动态组件
|
||||
366
skills/code-review-excellence/scripts/pr-analyzer.py
Normal file
366
skills/code-review-excellence/scripts/pr-analyzer.py
Normal 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()
|
||||
Reference in New Issue
Block a user