format: apply repo prettier (3.8.1) to the folded skills tree
963 markdown files reformatted with the repository's pinned prettier so pnpm format:check covers the folded tree like every other repo file. The formatter's embedded-language pass also normalized code fences (TS semicolons, closed HTML tags in examples, lowercased CSS hex colors, one renumbered list that skipped an index). Alphanumeric token deltas vs the fold commit were audited file-by-file; all are formatter-equivalent markup normalizations plus the four sanitized skills.
This commit is contained in:
+79
-53
@@ -7,11 +7,13 @@
|
||||
### S - 单一职责原则 (SRP)
|
||||
|
||||
**检查要点:**
|
||||
|
||||
- 这个类/模块是否只有一个改变的理由?
|
||||
- 类中的方法是否都服务于同一个目的?
|
||||
- 如果要向非技术人员描述这个类,能否用一句话说清楚?
|
||||
|
||||
**代码审查中的识别信号:**
|
||||
|
||||
```
|
||||
⚠️ 类名包含 "And"、"Manager"、"Handler"、"Processor" 等泛化词汇
|
||||
⚠️ 一个类超过 200-300 行代码
|
||||
@@ -20,17 +22,20 @@
|
||||
```
|
||||
|
||||
**审查问题:**
|
||||
|
||||
- "这个类负责哪些事情?能否拆分?"
|
||||
- "如果 X 需求变化,哪些方法需要改?如果 Y 需求变化呢?"
|
||||
|
||||
### O - 开闭原则 (OCP)
|
||||
|
||||
**检查要点:**
|
||||
|
||||
- 添加新功能时,是否需要修改现有代码?
|
||||
- 是否可以通过扩展(继承、组合)来添加新行为?
|
||||
- 是否存在大量的 if/else 或 switch 语句来处理不同类型?
|
||||
|
||||
**代码审查中的识别信号:**
|
||||
|
||||
```
|
||||
⚠️ switch/if-else 链处理不同类型
|
||||
⚠️ 添加新功能需要修改核心类
|
||||
@@ -38,17 +43,20 @@
|
||||
```
|
||||
|
||||
**审查问题:**
|
||||
|
||||
- "如果要添加新的 X 类型,需要修改哪些文件?"
|
||||
- "这个 switch 语句会随着新类型增加而增长吗?"
|
||||
|
||||
### L - 里氏替换原则 (LSP)
|
||||
|
||||
**检查要点:**
|
||||
|
||||
- 子类是否可以完全替代父类使用?
|
||||
- 子类是否改变了父类方法的预期行为?
|
||||
- 是否存在子类抛出父类未声明的异常?
|
||||
|
||||
**代码审查中的识别信号:**
|
||||
|
||||
```
|
||||
⚠️ 显式类型转换 (casting)
|
||||
⚠️ 子类方法抛出 NotImplementedException
|
||||
@@ -57,17 +65,20 @@
|
||||
```
|
||||
|
||||
**审查问题:**
|
||||
|
||||
- "如果用子类替换父类,调用方代码是否需要修改?"
|
||||
- "这个方法在子类中的行为是否符合父类的契约?"
|
||||
|
||||
### I - 接口隔离原则 (ISP)
|
||||
|
||||
**检查要点:**
|
||||
|
||||
- 接口是否足够小且专注?
|
||||
- 实现类是否被迫实现不需要的方法?
|
||||
- 客户端是否依赖了它不使用的方法?
|
||||
|
||||
**代码审查中的识别信号:**
|
||||
|
||||
```
|
||||
⚠️ 接口超过 5-7 个方法
|
||||
⚠️ 实现类有空方法或抛出 NotImplementedException
|
||||
@@ -76,17 +87,20 @@
|
||||
```
|
||||
|
||||
**审查问题:**
|
||||
|
||||
- "这个接口的所有方法是否都被每个实现类使用?"
|
||||
- "能否将这个大接口拆分为更小的专用接口?"
|
||||
|
||||
### D - 依赖倒置原则 (DIP)
|
||||
|
||||
**检查要点:**
|
||||
|
||||
- 高层模块是否依赖于抽象而非具体实现?
|
||||
- 是否使用依赖注入而非直接 new 对象?
|
||||
- 抽象是否由高层模块定义而非低层模块?
|
||||
|
||||
**代码审查中的识别信号:**
|
||||
|
||||
```
|
||||
⚠️ 高层模块直接 new 低层模块的具体类
|
||||
⚠️ 导入具体实现类而非接口/抽象类
|
||||
@@ -95,6 +109,7 @@
|
||||
```
|
||||
|
||||
**审查问题:**
|
||||
|
||||
- "这个类的依赖能否在测试时被 mock 替换?"
|
||||
- "如果要更换数据库/API 实现,需要修改多少地方?"
|
||||
|
||||
@@ -104,21 +119,21 @@
|
||||
|
||||
### 致命反模式
|
||||
|
||||
| 反模式 | 识别信号 | 影响 |
|
||||
|--------|----------|------|
|
||||
| **大泥球 (Big Ball of Mud)** | 没有清晰的模块边界,任何代码都可能调用任何其他代码 | 难以理解、修改和测试 |
|
||||
| **上帝类 (God Object)** | 单个类承担过多职责,知道太多、做太多 | 高耦合,难以重用和测试 |
|
||||
| **意大利面条代码** | 控制流程混乱,goto 或深层嵌套,难以追踪执行路径 | 难以理解和维护 |
|
||||
| **熔岩流 (Lava Flow)** | 没人敢动的古老代码,缺乏文档和测试 | 技术债务累积 |
|
||||
| 反模式 | 识别信号 | 影响 |
|
||||
| ---------------------------- | -------------------------------------------------- | ---------------------- |
|
||||
| **大泥球 (Big Ball of Mud)** | 没有清晰的模块边界,任何代码都可能调用任何其他代码 | 难以理解、修改和测试 |
|
||||
| **上帝类 (God Object)** | 单个类承担过多职责,知道太多、做太多 | 高耦合,难以重用和测试 |
|
||||
| **意大利面条代码** | 控制流程混乱,goto 或深层嵌套,难以追踪执行路径 | 难以理解和维护 |
|
||||
| **熔岩流 (Lava Flow)** | 没人敢动的古老代码,缺乏文档和测试 | 技术债务累积 |
|
||||
|
||||
### 设计反模式
|
||||
|
||||
| 反模式 | 识别信号 | 建议 |
|
||||
|--------|----------|------|
|
||||
| **金锤子 (Golden Hammer)** | 对所有问题使用同一种技术/模式 | 根据问题选择合适的解决方案 |
|
||||
| **过度工程 (Gas Factory)** | 简单问题用复杂方案解决,滥用设计模式 | YAGNI 原则,先简单后复杂 |
|
||||
| **船锚 (Boat Anchor)** | 为"将来可能需要"而写的未使用代码 | 删除未使用代码,需要时再写 |
|
||||
| **复制粘贴编程** | 相同逻辑出现在多处 | 提取公共方法或模块 |
|
||||
| 反模式 | 识别信号 | 建议 |
|
||||
| -------------------------- | ------------------------------------ | -------------------------- |
|
||||
| **金锤子 (Golden Hammer)** | 对所有问题使用同一种技术/模式 | 根据问题选择合适的解决方案 |
|
||||
| **过度工程 (Gas Factory)** | 简单问题用复杂方案解决,滥用设计模式 | YAGNI 原则,先简单后复杂 |
|
||||
| **船锚 (Boat Anchor)** | 为"将来可能需要"而写的未使用代码 | 删除未使用代码,需要时再写 |
|
||||
| **复制粘贴编程** | 相同逻辑出现在多处 | 提取公共方法或模块 |
|
||||
|
||||
### 审查问题
|
||||
|
||||
@@ -134,25 +149,25 @@
|
||||
|
||||
### 耦合类型(从好到差)
|
||||
|
||||
| 类型 | 描述 | 示例 |
|
||||
|------|------|------|
|
||||
| **消息耦合** ✅ | 通过参数传递数据 | `calculate(price, quantity)` |
|
||||
| **数据耦合** ✅ | 共享简单数据结构 | `processOrder(orderDTO)` |
|
||||
| 类型 | 描述 | 示例 |
|
||||
| --------------- | -------------------------- | ----------------------------- |
|
||||
| **消息耦合** ✅ | 通过参数传递数据 | `calculate(price, quantity)` |
|
||||
| **数据耦合** ✅ | 共享简单数据结构 | `processOrder(orderDTO)` |
|
||||
| **印记耦合** ⚠️ | 共享复杂数据结构但只用部分 | 传入整个 User 对象但只用 name |
|
||||
| **控制耦合** ⚠️ | 传递控制标志影响行为 | `process(data, isAdmin=true)` |
|
||||
| **公共耦合** ❌ | 共享全局变量 | 多个模块读写同一个全局状态 |
|
||||
| **内容耦合** ❌ | 直接访问另一模块的内部 | 直接操作另一个类的私有属性 |
|
||||
| **控制耦合** ⚠️ | 传递控制标志影响行为 | `process(data, isAdmin=true)` |
|
||||
| **公共耦合** ❌ | 共享全局变量 | 多个模块读写同一个全局状态 |
|
||||
| **内容耦合** ❌ | 直接访问另一模块的内部 | 直接操作另一个类的私有属性 |
|
||||
|
||||
### 内聚类型(从好到差)
|
||||
|
||||
| 类型 | 描述 | 质量 |
|
||||
|------|------|------|
|
||||
| **功能内聚** | 所有元素完成单一任务 | ✅ 最佳 |
|
||||
| **顺序内聚** | 输出作为下一步输入 | ✅ 良好 |
|
||||
| **通信内聚** | 操作相同数据 | ⚠️ 可接受 |
|
||||
| **时间内聚** | 同时执行的任务 | ⚠️ 较差 |
|
||||
| **逻辑内聚** | 逻辑相关但功能不同 | ❌ 差 |
|
||||
| **偶然内聚** | 没有明显关系 | ❌ 最差 |
|
||||
| 类型 | 描述 | 质量 |
|
||||
| ------------ | -------------------- | --------- |
|
||||
| **功能内聚** | 所有元素完成单一任务 | ✅ 最佳 |
|
||||
| **顺序内聚** | 输出作为下一步输入 | ✅ 良好 |
|
||||
| **通信内聚** | 操作相同数据 | ⚠️ 可接受 |
|
||||
| **时间内聚** | 同时执行的任务 | ⚠️ 较差 |
|
||||
| **逻辑内聚** | 逻辑相关但功能不同 | ❌ 差 |
|
||||
| **偶然内聚** | 没有明显关系 | ❌ 最差 |
|
||||
|
||||
### 度量指标参考
|
||||
|
||||
@@ -220,19 +235,23 @@ interface UserRepository {
|
||||
|
||||
// infrastructure/MySQLUserRepository.ts (实现)
|
||||
class MySQLUserRepository implements UserRepository {
|
||||
findById(id: string): Promise<User> { /* ... */ }
|
||||
findById(id: string): Promise<User> {
|
||||
/* ... */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 审查清单
|
||||
|
||||
**层次边界检查:**
|
||||
|
||||
- [ ] Domain 层是否有外部依赖(数据库、HTTP、文件系统)?
|
||||
- [ ] Application 层是否直接操作数据库或调用外部 API?
|
||||
- [ ] Controller 是否包含业务逻辑?
|
||||
- [ ] 是否存在跨层调用(UI 直接调用 Repository)?
|
||||
|
||||
**关注点分离检查:**
|
||||
|
||||
- [ ] 业务逻辑是否与展示逻辑分离?
|
||||
- [ ] 数据访问是否封装在专门的层?
|
||||
- [ ] 配置和环境相关代码是否集中管理?
|
||||
@@ -251,13 +270,13 @@ class MySQLUserRepository implements UserRepository {
|
||||
|
||||
### 何时使用设计模式
|
||||
|
||||
| 模式 | 适用场景 | 不适用场景 |
|
||||
|------|----------|------------|
|
||||
| **Factory** | 需要创建不同类型对象,类型在运行时确定 | 只有一种类型,或类型固定不变 |
|
||||
| **Strategy** | 算法需要在运行时切换,有多种可互换的行为 | 只有一种算法,或算法不会变化 |
|
||||
| **Observer** | 一对多依赖,状态变化需要通知多个对象 | 简单的直接调用即可满足需求 |
|
||||
| **Singleton** | 确实需要全局唯一实例,如配置管理 | 可以通过依赖注入传递的对象 |
|
||||
| **Decorator** | 需要动态添加职责,避免继承爆炸 | 职责固定,不需要动态组合 |
|
||||
| 模式 | 适用场景 | 不适用场景 |
|
||||
| ------------- | ---------------------------------------- | ---------------------------- |
|
||||
| **Factory** | 需要创建不同类型对象,类型在运行时确定 | 只有一种类型,或类型固定不变 |
|
||||
| **Strategy** | 算法需要在运行时切换,有多种可互换的行为 | 只有一种算法,或算法不会变化 |
|
||||
| **Observer** | 一对多依赖,状态变化需要通知多个对象 | 简单的直接调用即可满足需求 |
|
||||
| **Singleton** | 确实需要全局唯一实例,如配置管理 | 可以通过依赖注入传递的对象 |
|
||||
| **Decorator** | 需要动态添加职责,避免继承爆炸 | 职责固定,不需要动态组合 |
|
||||
|
||||
### 过度设计警告信号
|
||||
|
||||
@@ -275,11 +294,13 @@ class MySQLUserRepository implements UserRepository {
|
||||
|
||||
```markdown
|
||||
✅ 正确使用模式:
|
||||
|
||||
- 解决了实际的可扩展性问题
|
||||
- 代码更容易理解和测试
|
||||
- 添加新功能变得更简单
|
||||
|
||||
❌ 过度使用模式:
|
||||
|
||||
- 为了使用模式而使用
|
||||
- 增加了不必要的复杂度
|
||||
- 违反了 YAGNI 原则
|
||||
@@ -298,16 +319,19 @@ class MySQLUserRepository implements UserRepository {
|
||||
### 扩展性检查清单
|
||||
|
||||
**功能扩展性:**
|
||||
|
||||
- [ ] 添加新功能是否需要修改核心代码?
|
||||
- [ ] 是否提供了扩展点(hooks、plugins、events)?
|
||||
- [ ] 配置是否外部化(配置文件、环境变量)?
|
||||
|
||||
**数据扩展性:**
|
||||
|
||||
- [ ] 数据模型是否支持新增字段?
|
||||
- [ ] 是否考虑了数据量增长的场景?
|
||||
- [ ] 查询是否有合适的索引?
|
||||
|
||||
**负载扩展性:**
|
||||
|
||||
- [ ] 是否可以水平扩展(添加更多实例)?
|
||||
- [ ] 是否有状态依赖(session、本地缓存)?
|
||||
- [ ] 数据库连接是否使用连接池?
|
||||
@@ -330,9 +354,9 @@ class OrderService {
|
||||
// ❌ 差的扩展设计:硬编码所有行为
|
||||
class OrderService {
|
||||
async createOrder(order: Order) {
|
||||
await this.sendEmail(order); // 硬编码
|
||||
await this.updateInventory(order); // 硬编码
|
||||
await this.notifyWarehouse(order); // 硬编码
|
||||
await this.sendEmail(order); // 硬编码
|
||||
await this.updateInventory(order); // 硬编码
|
||||
await this.notifyWarehouse(order); // 硬编码
|
||||
return await this.save(order);
|
||||
}
|
||||
}
|
||||
@@ -353,6 +377,7 @@ class OrderService {
|
||||
### 目录组织
|
||||
|
||||
**按功能/领域组织(推荐):**
|
||||
|
||||
```
|
||||
src/
|
||||
├── user/
|
||||
@@ -370,6 +395,7 @@ src/
|
||||
```
|
||||
|
||||
**按技术层组织(不推荐):**
|
||||
|
||||
```
|
||||
src/
|
||||
├── controllers/ ← 不同领域混在一起
|
||||
@@ -382,13 +408,13 @@ src/
|
||||
|
||||
### 命名约定检查
|
||||
|
||||
| 类型 | 约定 | 示例 |
|
||||
|------|------|------|
|
||||
| 类名 | PascalCase,名词 | `UserService`, `OrderRepository` |
|
||||
| 方法名 | camelCase,动词 | `createUser`, `findOrderById` |
|
||||
| 接口名 | I 前缀或无前缀 | `IUserService` 或 `UserService` |
|
||||
| 常量 | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` |
|
||||
| 私有属性 | 下划线前缀或无 | `_cache` 或 `#cache` |
|
||||
| 类型 | 约定 | 示例 |
|
||||
| -------- | ---------------- | -------------------------------- |
|
||||
| 类名 | PascalCase,名词 | `UserService`, `OrderRepository` |
|
||||
| 方法名 | camelCase,动词 | `createUser`, `findOrderById` |
|
||||
| 接口名 | I 前缀或无前缀 | `IUserService` 或 `UserService` |
|
||||
| 常量 | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` |
|
||||
| 私有属性 | 下划线前缀或无 | `_cache` 或 `#cache` |
|
||||
|
||||
### 文件大小指南
|
||||
|
||||
@@ -452,14 +478,14 @@ src/
|
||||
|
||||
## 工具推荐
|
||||
|
||||
| 工具 | 用途 | 语言支持 |
|
||||
|------|------|----------|
|
||||
| **SonarQube** | 代码质量、耦合度分析 | 多语言 |
|
||||
| **NDepend** | 依赖分析、架构规则 | .NET |
|
||||
| **JDepend** | 包依赖分析 | Java |
|
||||
| **Madge** | 模块依赖图 | JavaScript/TypeScript |
|
||||
| **ESLint** | 代码规范、复杂度检查 | JavaScript/TypeScript |
|
||||
| **CodeScene** | 技术债务、热点分析 | 多语言 |
|
||||
| 工具 | 用途 | 语言支持 |
|
||||
| ------------- | -------------------- | --------------------- |
|
||||
| **SonarQube** | 代码质量、耦合度分析 | 多语言 |
|
||||
| **NDepend** | 依赖分析、架构规则 | .NET |
|
||||
| **JDepend** | 包依赖分析 | Java |
|
||||
| **Madge** | 模块依赖图 | JavaScript/TypeScript |
|
||||
| **ESLint** | 代码规范、复杂度检查 | JavaScript/TypeScript |
|
||||
| **CodeScene** | 技术债务、热点分析 | 多语言 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -264,22 +264,26 @@ 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
|
||||
|
||||
+19
-5
@@ -7,6 +7,7 @@ Comprehensive guidelines for conducting effective code reviews.
|
||||
### Goals of Code Review
|
||||
|
||||
**Primary Goals:**
|
||||
|
||||
- Catch bugs and edge cases before production
|
||||
- Ensure code maintainability and readability
|
||||
- Share knowledge across the team
|
||||
@@ -14,6 +15,7 @@ Comprehensive guidelines for conducting effective code reviews.
|
||||
- Improve design and architecture decisions
|
||||
|
||||
**Secondary Goals:**
|
||||
|
||||
- Mentor junior developers
|
||||
- Build team culture and trust
|
||||
- Document design decisions through discussions
|
||||
@@ -29,11 +31,11 @@ Comprehensive guidelines for conducting effective code reviews.
|
||||
|
||||
### 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 |
|
||||
| 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
|
||||
|
||||
@@ -44,18 +46,21 @@ Comprehensive guidelines for conducting effective code reviews.
|
||||
## 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
|
||||
@@ -66,11 +71,13 @@ Comprehensive guidelines for conducting effective code reviews.
|
||||
### 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
|
||||
@@ -86,6 +93,7 @@ Comprehensive guidelines for conducting effective code reviews.
|
||||
## Review Prioritization
|
||||
|
||||
### Must Fix (Blocking)
|
||||
|
||||
- Security vulnerabilities
|
||||
- Data corruption risks
|
||||
- Breaking changes without migration
|
||||
@@ -93,6 +101,7 @@ Comprehensive guidelines for conducting effective code reviews.
|
||||
- Missing error handling for user-facing features
|
||||
|
||||
### Should Fix (Important)
|
||||
|
||||
- Test coverage gaps
|
||||
- Moderate performance concerns
|
||||
- Code duplication
|
||||
@@ -100,6 +109,7 @@ Comprehensive guidelines for conducting effective code reviews.
|
||||
- Missing documentation for complex logic
|
||||
|
||||
### Nice to Have (Non-blocking)
|
||||
|
||||
- Style preferences beyond linting
|
||||
- Minor optimizations
|
||||
- Additional test cases
|
||||
@@ -108,6 +118,7 @@ Comprehensive guidelines for conducting effective code reviews.
|
||||
## 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..."
|
||||
@@ -115,6 +126,7 @@ Comprehensive guidelines for conducting effective code reviews.
|
||||
- **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
|
||||
@@ -123,6 +135,7 @@ Comprehensive guidelines for conducting effective code reviews.
|
||||
## Metrics and Improvement
|
||||
|
||||
### Track These Metrics
|
||||
|
||||
- Time to first review
|
||||
- Review cycle time
|
||||
- Number of review rounds
|
||||
@@ -130,6 +143,7 @@ Comprehensive guidelines for conducting effective code reviews.
|
||||
- Review coverage percentage
|
||||
|
||||
### Continuous Improvement
|
||||
|
||||
- Hold retrospectives on review process
|
||||
- Share learnings from escaped bugs
|
||||
- Update checklists based on common issues
|
||||
|
||||
+126
-75
@@ -5,6 +5,7 @@ Language-specific bugs and issues to watch for during code review.
|
||||
## Universal Issues
|
||||
|
||||
### Logic Errors
|
||||
|
||||
- [ ] Off-by-one errors in loops and array access
|
||||
- [ ] Incorrect boolean logic (De Morgan's law violations)
|
||||
- [ ] Missing null/undefined checks
|
||||
@@ -14,6 +15,7 @@ Language-specific bugs and issues to watch for during code review.
|
||||
- [ ] Floating point comparison issues
|
||||
|
||||
### Resource Management
|
||||
|
||||
- [ ] Memory leaks (unclosed connections, listeners)
|
||||
- [ ] File handles not closed
|
||||
- [ ] Database connections not released
|
||||
@@ -21,6 +23,7 @@ Language-specific bugs and issues to watch for during code review.
|
||||
- [ ] Timers/intervals not cleared
|
||||
|
||||
### Error Handling
|
||||
|
||||
- [ ] Swallowed exceptions (empty catch blocks)
|
||||
- [ ] Generic exception handling hiding specific errors
|
||||
- [ ] Missing error propagation
|
||||
@@ -30,26 +33,34 @@ Language-specific bugs and issues to watch for during code review.
|
||||
## TypeScript/JavaScript
|
||||
|
||||
### Type Issues
|
||||
|
||||
```typescript
|
||||
// ❌ Using any defeats type safety
|
||||
function process(data: any) { return data.value; }
|
||||
function process(data: any) {
|
||||
return data.value;
|
||||
}
|
||||
|
||||
// ✅ Use proper types
|
||||
interface Data { value: string; }
|
||||
function process(data: Data) { return data.value; }
|
||||
interface Data {
|
||||
value: string;
|
||||
}
|
||||
function process(data: Data) {
|
||||
return data.value;
|
||||
}
|
||||
```
|
||||
|
||||
### Async/Await Pitfalls
|
||||
|
||||
```typescript
|
||||
// ❌ Missing await
|
||||
async function fetch() {
|
||||
const data = fetchData(); // Missing await!
|
||||
const data = fetchData(); // Missing await!
|
||||
return data.json();
|
||||
}
|
||||
|
||||
// ❌ Unhandled promise rejection
|
||||
async function risky() {
|
||||
const result = await fetchData(); // No try-catch
|
||||
const result = await fetchData(); // No try-catch
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -68,11 +79,12 @@ async function safe() {
|
||||
### React Specific
|
||||
|
||||
#### Hooks 规则违反
|
||||
|
||||
```tsx
|
||||
// ❌ 条件调用 Hooks — 违反 Hooks 规则
|
||||
function BadComponent({ show }) {
|
||||
if (show) {
|
||||
const [value, setValue] = useState(0); // Error!
|
||||
const [value, setValue] = useState(0); // Error!
|
||||
}
|
||||
return <div>...</div>;
|
||||
}
|
||||
@@ -86,36 +98,35 @@ function GoodComponent({ show }) {
|
||||
|
||||
// ❌ 循环中调用 Hooks
|
||||
function BadLoop({ items }) {
|
||||
items.forEach(item => {
|
||||
const [selected, setSelected] = useState(false); // Error!
|
||||
items.forEach((item) => {
|
||||
const [selected, setSelected] = useState(false); // Error!
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 将状态提升或使用不同的数据结构
|
||||
function GoodLoop({ items }) {
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
return items.map(item => (
|
||||
<Item key={item.id} selected={selectedIds.has(item.id)} />
|
||||
));
|
||||
return items.map((item) => <Item key={item.id} selected={selectedIds.has(item.id)} />);
|
||||
}
|
||||
```
|
||||
|
||||
#### useEffect 常见错误
|
||||
|
||||
```tsx
|
||||
// ❌ 依赖数组不完整 — stale closure
|
||||
function StaleClosureExample({ userId, onSuccess }) {
|
||||
const [data, setData] = useState(null);
|
||||
useEffect(() => {
|
||||
fetchData(userId).then(result => {
|
||||
fetchData(userId).then((result) => {
|
||||
setData(result);
|
||||
onSuccess(result); // onSuccess 可能是 stale 的!
|
||||
onSuccess(result); // onSuccess 可能是 stale 的!
|
||||
});
|
||||
}, [userId]); // 缺少 onSuccess 依赖
|
||||
}, [userId]); // 缺少 onSuccess 依赖
|
||||
}
|
||||
|
||||
// ✅ 完整的依赖数组
|
||||
useEffect(() => {
|
||||
fetchData(userId).then(result => {
|
||||
fetchData(userId).then((result) => {
|
||||
setData(result);
|
||||
onSuccess(result);
|
||||
});
|
||||
@@ -125,15 +136,15 @@ useEffect(() => {
|
||||
function InfiniteLoop() {
|
||||
const [count, setCount] = useState(0);
|
||||
useEffect(() => {
|
||||
setCount(count + 1); // 触发重渲染,又触发 effect
|
||||
}, [count]); // 无限循环!
|
||||
setCount(count + 1); // 触发重渲染,又触发 effect
|
||||
}, [count]); // 无限循环!
|
||||
}
|
||||
|
||||
// ❌ 缺少清理函数 — 内存泄漏
|
||||
function MemoryLeak({ userId }) {
|
||||
const [user, setUser] = useState(null);
|
||||
useEffect(() => {
|
||||
fetchUser(userId).then(setUser); // 组件卸载后仍然调用 setUser
|
||||
fetchUser(userId).then(setUser); // 组件卸载后仍然调用 setUser
|
||||
}, [userId]);
|
||||
}
|
||||
|
||||
@@ -142,10 +153,12 @@ function NoLeak({ userId }) {
|
||||
const [user, setUser] = useState(null);
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
fetchUser(userId).then(data => {
|
||||
fetchUser(userId).then((data) => {
|
||||
if (!cancelled) setUser(data);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [userId]);
|
||||
}
|
||||
|
||||
@@ -154,22 +167,19 @@ function BadDerived({ items }) {
|
||||
const [total, setTotal] = useState(0);
|
||||
useEffect(() => {
|
||||
setTotal(items.reduce((a, b) => a + b.price, 0));
|
||||
}, [items]); // 不必要的 effect + 额外渲染
|
||||
}, [items]); // 不必要的 effect + 额外渲染
|
||||
}
|
||||
|
||||
// ✅ 直接计算或用 useMemo
|
||||
function GoodDerived({ items }) {
|
||||
const total = useMemo(
|
||||
() => items.reduce((a, b) => a + b.price, 0),
|
||||
[items]
|
||||
);
|
||||
const total = useMemo(() => items.reduce((a, b) => a + b.price, 0), [items]);
|
||||
}
|
||||
|
||||
// ❌ useEffect 用于事件响应
|
||||
function BadEvent() {
|
||||
const [query, setQuery] = useState('');
|
||||
useEffect(() => {
|
||||
if (query) logSearch(query); // 应该在事件处理器中
|
||||
if (query) logSearch(query); // 应该在事件处理器中
|
||||
}, [query]);
|
||||
}
|
||||
|
||||
@@ -183,11 +193,12 @@ function GoodEvent() {
|
||||
```
|
||||
|
||||
#### useMemo / useCallback 误用
|
||||
|
||||
```tsx
|
||||
// ❌ 过度优化 — 常量不需要 memo
|
||||
function OverOptimized() {
|
||||
const config = useMemo(() => ({ api: '/v1' }), []); // 无意义
|
||||
const noop = useCallback(() => {}, []); // 无意义
|
||||
const config = useMemo(() => ({ api: '/v1' }), []); // 无意义
|
||||
const noop = useCallback(() => {}, []); // 无意义
|
||||
}
|
||||
|
||||
// ❌ 空依赖的 useMemo(可能隐藏 bug)
|
||||
@@ -200,7 +211,7 @@ function EmptyDeps({ user }) {
|
||||
function UselessCallback({ data }) {
|
||||
const process = useCallback(() => {
|
||||
return data.map(transform);
|
||||
}, [data]); // 如果 data 每次都是新引用,完全无效
|
||||
}, [data]); // 如果 data 每次都是新引用,完全无效
|
||||
}
|
||||
|
||||
// ❌ useMemo/useCallback 没有配合 React.memo
|
||||
@@ -224,6 +235,7 @@ function Parent() {
|
||||
```
|
||||
|
||||
#### 组件设计问题
|
||||
|
||||
```tsx
|
||||
// ❌ 在组件内定义组件
|
||||
function Parent() {
|
||||
@@ -242,21 +254,22 @@ function Parent() {
|
||||
function BadProps() {
|
||||
return (
|
||||
<MemoComponent
|
||||
style={{ color: 'red' }} // 每次渲染新对象
|
||||
onClick={() => handle()} // 每次渲染新函数
|
||||
items={data.filter(x => x)} // 每次渲染新数组
|
||||
style={{ color: 'red' }} // 每次渲染新对象
|
||||
onClick={() => handle()} // 每次渲染新函数
|
||||
items={data.filter((x) => x)} // 每次渲染新数组
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ❌ 直接修改 props
|
||||
function MutateProps({ user }) {
|
||||
user.name = 'Changed'; // 永远不要这样做!
|
||||
user.name = 'Changed'; // 永远不要这样做!
|
||||
return <div>{user.name}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
#### Server Components 错误 (React 19+)
|
||||
|
||||
```tsx
|
||||
// ❌ 在 Server Component 中使用客户端 API
|
||||
// app/page.tsx (默认是 Server Component)
|
||||
@@ -288,10 +301,11 @@ export default function Layout({ children }) { ... }
|
||||
```
|
||||
|
||||
#### 测试常见错误
|
||||
|
||||
```tsx
|
||||
// ❌ 使用 container 查询
|
||||
const { container } = render(<Component />);
|
||||
const button = container.querySelector('button'); // 不推荐
|
||||
const button = container.querySelector('button'); // 不推荐
|
||||
|
||||
// ✅ 使用 screen 和语义查询
|
||||
render(<Component />);
|
||||
@@ -310,13 +324,14 @@ expect(component.state.isOpen).toBe(true);
|
||||
expect(screen.getByRole('dialog')).toBeVisible();
|
||||
|
||||
// ❌ 等待同步查询
|
||||
await screen.getByText('Hello'); // getBy 是同步的
|
||||
await screen.getByText('Hello'); // getBy 是同步的
|
||||
|
||||
// ✅ 异步用 findBy
|
||||
await screen.findByText('Hello'); // findBy 会等待
|
||||
await screen.findByText('Hello'); // findBy 会等待
|
||||
```
|
||||
|
||||
### React Common Mistakes Checklist
|
||||
|
||||
- [ ] Hooks 不在顶层调用(条件/循环中)
|
||||
- [ ] useEffect 依赖数组不完整
|
||||
- [ ] useEffect 缺少清理函数
|
||||
@@ -339,18 +354,18 @@ await screen.findByText('Hello'); // findBy 会等待
|
||||
|
||||
// ❌ 在 Action 中直接 setState 而不是返回状态
|
||||
const [state, action] = useActionState(async (prev, formData) => {
|
||||
setSomeState(newValue); // 错误!应该返回新状态
|
||||
setSomeState(newValue); // 错误!应该返回新状态
|
||||
}, initialState);
|
||||
|
||||
// ✅ 返回新状态
|
||||
const [state, action] = useActionState(async (prev, formData) => {
|
||||
const result = await submitForm(formData);
|
||||
return { ...prev, data: result }; // 返回新状态
|
||||
return { ...prev, data: result }; // 返回新状态
|
||||
}, initialState);
|
||||
|
||||
// ❌ 忘记处理 isPending
|
||||
const [state, action] = useActionState(submitAction, null);
|
||||
return <button>Submit</button>; // 用户可以重复点击
|
||||
return <button>Submit</button>; // 用户可以重复点击
|
||||
|
||||
// ✅ 使用 isPending 禁用按钮
|
||||
const [state, action, isPending] = useActionState(submitAction, null);
|
||||
@@ -360,8 +375,12 @@ return <button disabled={isPending}>Submit</button>;
|
||||
|
||||
// ❌ 在 form 同级调用 useFormStatus
|
||||
function Form() {
|
||||
const { pending } = useFormStatus(); // 永远是 undefined!
|
||||
return <form><button disabled={pending}>Submit</button></form>;
|
||||
const { pending } = useFormStatus(); // 永远是 undefined!
|
||||
return (
|
||||
<form>
|
||||
<button disabled={pending}>Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ 在子组件中调用
|
||||
@@ -370,7 +389,11 @@ function SubmitButton() {
|
||||
return <button disabled={pending}>Submit</button>;
|
||||
}
|
||||
function Form() {
|
||||
return <form><SubmitButton /></form>;
|
||||
return (
|
||||
<form>
|
||||
<SubmitButton />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
// === useOptimistic 错误 ===
|
||||
@@ -379,7 +402,7 @@ function Form() {
|
||||
function PaymentButton() {
|
||||
const [optimisticPaid, setPaid] = useOptimistic(false);
|
||||
const handlePay = async () => {
|
||||
setPaid(true); // 危险:显示已支付但可能失败
|
||||
setPaid(true); // 危险:显示已支付但可能失败
|
||||
await processPayment();
|
||||
};
|
||||
}
|
||||
@@ -394,12 +417,13 @@ const handleLike = async () => {
|
||||
try {
|
||||
await likePost();
|
||||
} catch {
|
||||
toast.error('点赞失败,请重试'); // 通知用户
|
||||
toast.error('点赞失败,请重试'); // 通知用户
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### React 19 Forms Checklist
|
||||
|
||||
- [ ] useActionState 返回新状态而不是 setState
|
||||
- [ ] useActionState 正确使用 isPending 禁用提交
|
||||
- [ ] useFormStatus 在 form 子组件中调用
|
||||
@@ -416,9 +440,9 @@ const handleLike = async () => {
|
||||
function BadPage() {
|
||||
return (
|
||||
<Suspense fallback={<FullPageLoader />}>
|
||||
<FastHeader /> {/* 快 */}
|
||||
<FastHeader /> {/* 快 */}
|
||||
<SlowMainContent /> {/* 慢——阻塞整个页面 */}
|
||||
<FastFooter /> {/* 快 */}
|
||||
<FastFooter /> {/* 快 */}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -440,7 +464,7 @@ function GoodPage() {
|
||||
function NoErrorHandling() {
|
||||
return (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<DataFetcher /> {/* 抛错导致白屏 */}
|
||||
<DataFetcher /> {/* 抛错导致白屏 */}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -460,7 +484,7 @@ function WithErrorHandling() {
|
||||
|
||||
// ❌ 在组件外创建 Promise(每次渲染新 Promise)
|
||||
function BadUse() {
|
||||
const data = use(fetchData()); // 每次渲染都创建新 Promise!
|
||||
const data = use(fetchData()); // 每次渲染都创建新 Promise!
|
||||
return <div>{data}</div>;
|
||||
}
|
||||
|
||||
@@ -479,7 +503,7 @@ function Child({ dataPromise }) {
|
||||
// ❌ 在 layout.tsx 中 await 慢数据——阻塞所有子页面
|
||||
// app/layout.tsx
|
||||
export default async function Layout({ children }) {
|
||||
const config = await fetchSlowConfig(); // 阻塞整个应用!
|
||||
const config = await fetchSlowConfig(); // 阻塞整个应用!
|
||||
return <ConfigProvider value={config}>{children}</ConfigProvider>;
|
||||
}
|
||||
|
||||
@@ -495,6 +519,7 @@ export default function Layout({ children }) {
|
||||
```
|
||||
|
||||
### Suspense Checklist
|
||||
|
||||
- [ ] 慢内容有独立的 Suspense 边界
|
||||
- [ ] 每个 Suspense 有对应的 Error Boundary
|
||||
- [ ] fallback 是有意义的骨架屏(不是简单 spinner)
|
||||
@@ -510,7 +535,7 @@ export default function Layout({ children }) {
|
||||
// ❌ queryKey 不包含查询参数
|
||||
function BadQuery({ userId, filters }) {
|
||||
const { data } = useQuery({
|
||||
queryKey: ['users'], // 缺少 userId 和 filters!
|
||||
queryKey: ['users'], // 缺少 userId 和 filters!
|
||||
queryFn: () => fetchUsers(userId, filters),
|
||||
});
|
||||
// userId 或 filters 变化时数据不会更新
|
||||
@@ -535,7 +560,7 @@ const { data } = useQuery({
|
||||
const { data } = useQuery({
|
||||
queryKey: ['data'],
|
||||
queryFn: fetchData,
|
||||
staleTime: 5 * 60 * 1000, // 5 分钟内不会自动 refetch
|
||||
staleTime: 5 * 60 * 1000, // 5 分钟内不会自动 refetch
|
||||
});
|
||||
|
||||
// === useSuspenseQuery 错误 ===
|
||||
@@ -544,7 +569,7 @@ const { data } = useQuery({
|
||||
const { data } = useSuspenseQuery({
|
||||
queryKey: ['user', userId],
|
||||
queryFn: () => fetchUser(userId),
|
||||
enabled: !!userId, // 错误!useSuspenseQuery 不支持 enabled
|
||||
enabled: !!userId, // 错误!useSuspenseQuery 不支持 enabled
|
||||
});
|
||||
|
||||
// ✅ 条件渲染实现
|
||||
@@ -610,7 +635,7 @@ const mutation = useMutation({
|
||||
// === v5 迁移错误 ===
|
||||
|
||||
// ❌ 使用废弃的 API
|
||||
const { data, isLoading } = useQuery(['key'], fetchFn); // v4 语法
|
||||
const { data, isLoading } = useQuery(['key'], fetchFn); // v4 语法
|
||||
|
||||
// ✅ v5 单一对象参数
|
||||
const { data, isPending } = useQuery({
|
||||
@@ -623,12 +648,13 @@ if (isLoading) return <Spinner />;
|
||||
// v5 中 isLoading = isPending && isFetching
|
||||
|
||||
// ✅ 根据意图选择
|
||||
if (isPending) return <Spinner />; // 没有缓存数据
|
||||
if (isPending) return <Spinner />; // 没有缓存数据
|
||||
// 或
|
||||
if (isFetching) return <Refreshing />; // 正在后台刷新
|
||||
if (isFetching) return <Refreshing />; // 正在后台刷新
|
||||
```
|
||||
|
||||
### TanStack Query Checklist
|
||||
|
||||
- [ ] queryKey 包含所有影响数据的参数
|
||||
- [ ] 设置了合理的 staleTime(不是默认 0)
|
||||
- [ ] useSuspenseQuery 不使用 enabled
|
||||
@@ -638,6 +664,7 @@ if (isFetching) return <Refreshing />; // 正在后台刷新
|
||||
- [ ] 理解 isPending vs isLoading vs isFetching
|
||||
|
||||
### TypeScript/JavaScript Common Mistakes
|
||||
|
||||
- [ ] `==` instead of `===`
|
||||
- [ ] Modifying array/object during iteration
|
||||
- [ ] `this` context lost in callbacks
|
||||
@@ -648,21 +675,23 @@ if (isFetching) return <Refreshing />; // 正在后台刷新
|
||||
## Vue 3
|
||||
|
||||
### 响应性丢失
|
||||
|
||||
```vue
|
||||
<!-- ❌ 解构 reactive 丢失响应性 -->
|
||||
<script setup>
|
||||
const state = reactive({ count: 0 })
|
||||
const { count } = state // count 不是响应式的!
|
||||
const state = reactive({ count: 0 });
|
||||
const { count } = state; // count 不是响应式的!
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 toRefs -->
|
||||
<script setup>
|
||||
const state = reactive({ count: 0 })
|
||||
const { count } = toRefs(state) // count.value 是响应式的
|
||||
const state = reactive({ count: 0 });
|
||||
const { count } = toRefs(state); // count.value 是响应式的
|
||||
</script>
|
||||
```
|
||||
|
||||
### Props 响应性传递
|
||||
|
||||
```vue
|
||||
<!-- ❌ 传递 props 值到 composable 丢失响应性 -->
|
||||
<script setup>
|
||||
@@ -680,48 +709,53 @@ const { data } = useFetch(toRef(props, 'id'))
|
||||
```
|
||||
|
||||
### Watch 清理
|
||||
|
||||
```vue
|
||||
<!-- ❌ 异步 watch 无清理,导致竞态 -->
|
||||
<script setup>
|
||||
watch(id, async (newId) => {
|
||||
const data = await fetchData(newId)
|
||||
result.value = data // 旧请求可能覆盖新结果!
|
||||
})
|
||||
const data = await fetchData(newId);
|
||||
result.value = data; // 旧请求可能覆盖新结果!
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 onCleanup 取消旧请求 -->
|
||||
<script setup>
|
||||
watch(id, async (newId, _, onCleanup) => {
|
||||
const controller = new AbortController()
|
||||
onCleanup(() => controller.abort())
|
||||
const controller = new AbortController();
|
||||
onCleanup(() => controller.abort());
|
||||
|
||||
const data = await fetchData(newId, controller.signal)
|
||||
result.value = data
|
||||
})
|
||||
const data = await fetchData(newId, controller.signal);
|
||||
result.value = data;
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### Computed 副作用
|
||||
|
||||
```vue
|
||||
<!-- ❌ computed 中修改其他状态 -->
|
||||
<script setup>
|
||||
const total = computed(() => {
|
||||
sideEffect.value++ // 副作用!每次访问都会执行
|
||||
return items.value.reduce((a, b) => a + b, 0)
|
||||
})
|
||||
sideEffect.value++; // 副作用!每次访问都会执行
|
||||
return items.value.reduce((a, b) => a + b, 0);
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- ✅ computed 只做纯计算 -->
|
||||
<script setup>
|
||||
const total = computed(() => {
|
||||
return items.value.reduce((a, b) => a + b, 0)
|
||||
})
|
||||
return items.value.reduce((a, b) => a + b, 0);
|
||||
});
|
||||
// 副作用放 watch
|
||||
watch(total, () => { sideEffect.value++ })
|
||||
watch(total, () => {
|
||||
sideEffect.value++;
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### 模板常见错误
|
||||
|
||||
```vue
|
||||
<!-- ❌ v-if 和 v-for 同时使用(v-if 优先级更高) -->
|
||||
<template>
|
||||
@@ -739,6 +773,7 @@ watch(total, () => { sideEffect.value++ })
|
||||
```
|
||||
|
||||
### Common Mistakes
|
||||
|
||||
- [ ] 解构 reactive 对象丢失响应性
|
||||
- [ ] props 传递给 composable 时未保持响应性
|
||||
- [ ] watch 异步回调无清理函数
|
||||
@@ -753,6 +788,7 @@ watch(total, () => { sideEffect.value++ })
|
||||
## Python
|
||||
|
||||
### Mutable Default Arguments
|
||||
|
||||
```python
|
||||
# ❌ Bug: List shared across all calls
|
||||
def add_item(item, items=[]):
|
||||
@@ -768,6 +804,7 @@ def add_item(item, items=None):
|
||||
```
|
||||
|
||||
### Exception Handling
|
||||
|
||||
```python
|
||||
# ❌ Catching everything, including KeyboardInterrupt
|
||||
try:
|
||||
@@ -784,6 +821,7 @@ except ValueError as e:
|
||||
```
|
||||
|
||||
### Class Attributes
|
||||
|
||||
```python
|
||||
# ❌ Shared mutable class attribute
|
||||
class User:
|
||||
@@ -796,6 +834,7 @@ class User:
|
||||
```
|
||||
|
||||
### Common Mistakes
|
||||
|
||||
- [ ] Using `is` instead of `==` for value comparison
|
||||
- [ ] Forgetting `self` parameter in methods
|
||||
- [ ] Modifying list while iterating
|
||||
@@ -1139,18 +1178,21 @@ struct Good<'a> {
|
||||
### Rust 审查清单
|
||||
|
||||
**所有权与借用**
|
||||
|
||||
- [ ] clone() 是有意为之,不是绕过借用检查器
|
||||
- [ ] 避免在结构体中存储借用(除非必要)
|
||||
- [ ] Rc/Arc 使用合理,没有隐藏不必要的共享状态
|
||||
- [ ] 没有不必要的 RefCell(运行时检查 vs 编译时)
|
||||
|
||||
**Unsafe 代码**
|
||||
|
||||
- [ ] 每个 unsafe 块有 SAFETY 注释
|
||||
- [ ] unsafe fn 有 # Safety 文档
|
||||
- [ ] 安全不变量被清晰记录
|
||||
- [ ] unsafe 边界尽可能小
|
||||
|
||||
**异步/并发**
|
||||
|
||||
- [ ] 没有在异步上下文中阻塞
|
||||
- [ ] 没有跨 .await 持有 std::sync 锁
|
||||
- [ ] spawn 的任务满足 'static 约束
|
||||
@@ -1158,25 +1200,29 @@ struct Good<'a> {
|
||||
- [ ] 锁的顺序一致(避免死锁)
|
||||
|
||||
**错误处理**
|
||||
|
||||
- [ ] 库代码使用 thiserror,应用代码使用 anyhow
|
||||
- [ ] 错误有足够的上下文信息
|
||||
- [ ] 没有在生产代码中 unwrap/expect
|
||||
- [ ] must_use 返回值被正确处理
|
||||
|
||||
**性能**
|
||||
|
||||
- [ ] 避免不必要的 collect()
|
||||
- [ ] 大数据结构传引用
|
||||
- [ ] 字符串拼接使用 String::with_capacity 或 write!
|
||||
- [ ] impl Trait 优于 Box<dyn Trait>(当可能时)
|
||||
|
||||
**类型系统**
|
||||
|
||||
- [ ] 善用 newtype 模式增加类型安全
|
||||
- [ ] 枚举穷尽匹配(没有 _ 通配符隐藏新变体)
|
||||
- [ ] 枚举穷尽匹配(没有 \_ 通配符隐藏新变体)
|
||||
- [ ] 生命周期尽可能简化
|
||||
|
||||
## SQL
|
||||
|
||||
### Injection Vulnerabilities
|
||||
|
||||
```sql
|
||||
-- ❌ String concatenation (SQL injection risk)
|
||||
query = "SELECT * FROM users WHERE id = " + user_id
|
||||
@@ -1187,13 +1233,15 @@ cursor.execute(query, (user_id,))
|
||||
```
|
||||
|
||||
### Performance Issues
|
||||
|
||||
- [ ] Missing indexes on filtered/joined columns
|
||||
- [ ] SELECT * instead of specific columns
|
||||
- [ ] SELECT \* instead of specific columns
|
||||
- [ ] N+1 query patterns
|
||||
- [ ] Missing LIMIT on large tables
|
||||
- [ ] Inefficient subqueries vs JOINs
|
||||
|
||||
### Common Mistakes
|
||||
|
||||
- [ ] Not handling NULL comparisons correctly
|
||||
- [ ] Missing transactions for related operations
|
||||
- [ ] Incorrect JOIN types
|
||||
@@ -1203,6 +1251,7 @@ cursor.execute(query, (user_id,))
|
||||
## API Design
|
||||
|
||||
### REST Issues
|
||||
|
||||
- [ ] Inconsistent resource naming
|
||||
- [ ] Wrong HTTP methods (POST for idempotent operations)
|
||||
- [ ] Missing pagination for list endpoints
|
||||
@@ -1210,6 +1259,7 @@ cursor.execute(query, (user_id,))
|
||||
- [ ] Missing rate limiting
|
||||
|
||||
### Data Validation
|
||||
|
||||
- [ ] Missing input validation
|
||||
- [ ] Incorrect data type validation
|
||||
- [ ] Missing length/range checks
|
||||
@@ -1219,6 +1269,7 @@ cursor.execute(query, (user_id,))
|
||||
## Testing
|
||||
|
||||
### Test Quality Issues
|
||||
|
||||
- [ ] Testing implementation details instead of behavior
|
||||
- [ ] Missing edge case tests
|
||||
- [ ] Flaky tests (non-deterministic)
|
||||
|
||||
@@ -357,6 +357,7 @@ 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
|
||||
@@ -364,22 +365,26 @@ clang-format -i src/*.cpp include/*.h
|
||||
- [ ] 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
|
||||
|
||||
+87
-56
@@ -105,8 +105,12 @@ CSS 及预处理器代码审查指南,覆盖性能、可维护性、响应式
|
||||
|
||||
```css
|
||||
/* ✅ 工具类 - 明确需要覆盖 */
|
||||
.hidden { display: none !important; }
|
||||
.sr-only { position: absolute !important; }
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute !important;
|
||||
}
|
||||
|
||||
/* ✅ 覆盖第三方库样式(无法修改源码时) */
|
||||
.third-party-modal {
|
||||
@@ -115,7 +119,9 @@ CSS 及预处理器代码审查指南,覆盖性能、可维护性、响应式
|
||||
|
||||
/* ✅ 打印样式 */
|
||||
@media print {
|
||||
.no-print { display: none !important; }
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -124,16 +130,20 @@ CSS 及预处理器代码审查指南,覆盖性能、可维护性、响应式
|
||||
```css
|
||||
/* ❌ 解决特异性问题 - 应该重构选择器 */
|
||||
.button {
|
||||
background: blue !important; /* 为什么需要 !important? */
|
||||
background: blue !important; /* 为什么需要 !important? */
|
||||
}
|
||||
|
||||
/* ❌ 覆盖自己写的样式 */
|
||||
.card { padding: 20px; }
|
||||
.card { padding: 30px !important; } /* 直接修改原规则 */
|
||||
.card {
|
||||
padding: 20px;
|
||||
}
|
||||
.card {
|
||||
padding: 30px !important;
|
||||
} /* 直接修改原规则 */
|
||||
|
||||
/* ❌ 在组件样式中 */
|
||||
.my-component .title {
|
||||
font-size: 24px !important; /* 破坏组件封装 */
|
||||
font-size: 24px !important; /* 破坏组件封装 */
|
||||
}
|
||||
```
|
||||
|
||||
@@ -159,10 +169,10 @@ button.my-btn {
|
||||
|
||||
/* ✅ 使用 :where() 降低被覆盖样式的特异性 */
|
||||
:where(.btn) {
|
||||
background: blue; /* 特异性为 0 */
|
||||
background: blue; /* 特异性为 0 */
|
||||
}
|
||||
.my-btn {
|
||||
background: red; /* 可以正常覆盖 */
|
||||
background: red; /* 可以正常覆盖 */
|
||||
}
|
||||
```
|
||||
|
||||
@@ -190,7 +200,9 @@ button.my-btn {
|
||||
|
||||
/* ✅ 明确指定属性 */
|
||||
.button {
|
||||
transition: background-color 0.3s ease, transform 0.3s ease;
|
||||
transition:
|
||||
background-color 0.3s ease,
|
||||
transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* ✅ 多属性时使用变量 */
|
||||
@@ -208,11 +220,11 @@ button.my-btn {
|
||||
```css
|
||||
/* ❌ 每帧触发重绘 - 严重影响性能 */
|
||||
.card {
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
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);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* ✅ 使用伪元素 + opacity */
|
||||
@@ -223,7 +235,7 @@ button.my-btn {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
@@ -239,24 +251,31 @@ button.my-btn {
|
||||
```css
|
||||
/* ❌ 动画这些属性会触发布局重计算 */
|
||||
.bad-animation {
|
||||
transition: width 0.3s, height 0.3s, top 0.3s, left 0.3s, margin 0.3s;
|
||||
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;
|
||||
transition:
|
||||
transform 0.3s,
|
||||
opacity 0.3s;
|
||||
}
|
||||
|
||||
/* 位移用 translate 代替 top/left */
|
||||
.move {
|
||||
transform: translateX(100px); /* ✅ */
|
||||
/* left: 100px; */ /* ❌ */
|
||||
transform: translateX(100px); /* ✅ */
|
||||
/* left: 100px; */ /* ❌ */
|
||||
}
|
||||
|
||||
/* 缩放用 scale 代替 width/height */
|
||||
.grow {
|
||||
transform: scale(1.1); /* ✅ */
|
||||
/* width: 110%; */ /* ❌ */
|
||||
transform: scale(1.1); /* ✅ */
|
||||
/* width: 110%; */ /* ❌ */
|
||||
}
|
||||
```
|
||||
|
||||
@@ -276,11 +295,17 @@ button.my-btn {
|
||||
}
|
||||
|
||||
/* ❌ 通配符选择器 */
|
||||
* { box-sizing: border-box; } /* 影响所有元素 */
|
||||
[class*="icon-"] { display: inline; } /* 属性选择器较慢 */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
} /* 影响所有元素 */
|
||||
[class*='icon-'] {
|
||||
display: inline;
|
||||
} /* 属性选择器较慢 */
|
||||
|
||||
/* ✅ 限制范围 */
|
||||
.icon-box * { box-sizing: border-box; }
|
||||
.icon-box * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
```
|
||||
|
||||
#### 大量阴影和滤镜
|
||||
@@ -289,17 +314,17 @@ button.my-btn {
|
||||
/* ⚠️ 复杂阴影影响渲染性能 */
|
||||
.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 层阴影 */
|
||||
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); /* 更消耗性能 */
|
||||
backdrop-filter: blur(10px); /* 更消耗性能 */
|
||||
}
|
||||
```
|
||||
|
||||
@@ -318,7 +343,7 @@ button.my-btn {
|
||||
|
||||
/* 使用 contain 限制重绘范围 */
|
||||
.card {
|
||||
contain: layout paint; /* 告诉浏览器内部变化不影响外部 */
|
||||
contain: layout paint; /* 告诉浏览器内部变化不影响外部 */
|
||||
}
|
||||
```
|
||||
|
||||
@@ -387,16 +412,20 @@ button.my-btn {
|
||||
```css
|
||||
/* 推荐断点(基于内容而非设备) */
|
||||
:root {
|
||||
--breakpoint-sm: 640px; /* 大手机 */
|
||||
--breakpoint-md: 768px; /* 平板竖屏 */
|
||||
--breakpoint-lg: 1024px; /* 平板横屏/小笔记本 */
|
||||
--breakpoint-xl: 1280px; /* 桌面 */
|
||||
--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 */ }
|
||||
@media (min-width: 768px) {
|
||||
/* md */
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
/* lg */
|
||||
}
|
||||
```
|
||||
|
||||
### 响应式审查清单
|
||||
@@ -425,7 +454,7 @@ button.my-btn {
|
||||
|
||||
/* ❌ 固定高度的文本容器 */
|
||||
.text-box {
|
||||
height: 100px; /* 文字可能溢出 */
|
||||
height: 100px; /* 文字可能溢出 */
|
||||
}
|
||||
|
||||
/* ✅ 最小高度 */
|
||||
@@ -435,7 +464,7 @@ button.my-btn {
|
||||
|
||||
/* ❌ 小触摸目标 */
|
||||
.small-button {
|
||||
padding: 4px 8px; /* 太小,难以点击 */
|
||||
padding: 4px 8px; /* 太小,难以点击 */
|
||||
}
|
||||
|
||||
/* ✅ 足够的触摸区域 */
|
||||
@@ -452,22 +481,22 @@ button.my-btn {
|
||||
|
||||
### 需要检查的特性
|
||||
|
||||
| 特性 | 兼容性 | 建议 |
|
||||
|------|--------|------|
|
||||
| CSS Grid | 现代浏览器 ✅ | IE 需要 Autoprefixer + 测试 |
|
||||
| Flexbox | 广泛支持 ✅ | 旧版需要前缀 |
|
||||
| CSS Variables | 现代浏览器 ✅ | IE 不支持,需要回退 |
|
||||
| `gap` (flexbox) | 较新 ⚠️ | Safari 14.1+ |
|
||||
| `:has()` | 较新 ⚠️ | Firefox 121+ |
|
||||
| `container queries` | 较新 ⚠️ | 2023 年后的浏览器 |
|
||||
| `@layer` | 较新 ⚠️ | 检查目标浏览器 |
|
||||
| 特性 | 兼容性 | 建议 |
|
||||
| ------------------- | ------------- | --------------------------- |
|
||||
| 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: #3b82f6; /* 回退值 */
|
||||
background: var(--color-primary); /* 现代浏览器 */
|
||||
}
|
||||
|
||||
@@ -540,7 +569,7 @@ module.exports = {
|
||||
.content {
|
||||
.article {
|
||||
.title {
|
||||
color: red; // 编译为 .page .container .content .article .title
|
||||
color: red; // 编译为 .page .container .content .article .title
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -554,7 +583,9 @@ module.exports = {
|
||||
}
|
||||
|
||||
&__content {
|
||||
p { margin-bottom: 1em; }
|
||||
p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -637,13 +668,13 @@ $primary-color: #3b82f6;
|
||||
|
||||
## 工具推荐
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| [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/) | 浏览器兼容性查询 |
|
||||
| 工具 | 用途 |
|
||||
| ----------------------------------------------- | ---------------- |
|
||||
| [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/) | 浏览器兼容性查询 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
## 快速审查清单
|
||||
|
||||
### 必查项
|
||||
|
||||
- [ ] 错误是否正确处理(不忽略、有上下文)
|
||||
- [ ] goroutine 是否有退出机制(避免泄漏)
|
||||
- [ ] context 是否正确传递和取消
|
||||
@@ -12,6 +13,7 @@
|
||||
- [ ] 是否使用 `gofmt` 格式化代码
|
||||
|
||||
### 高频问题
|
||||
|
||||
- [ ] 循环变量捕获问题(Go < 1.22)
|
||||
- [ ] nil 检查是否完整
|
||||
- [ ] map 是否初始化后使用
|
||||
|
||||
@@ -377,29 +377,34 @@ class UserRepositoryTest {
|
||||
## 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)
|
||||
- [ ] 魔法值提取为常量或枚举
|
||||
|
||||
+89
-81
@@ -18,13 +18,13 @@
|
||||
|
||||
### 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** | 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 优化检查
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
|
||||
- [ ] LCP 元素是否设置 `fetchpriority="high"`?
|
||||
- [ ] 是否使用 WebP/AVIF 格式?
|
||||
- [ ] 是否有服务端渲染或静态生成?
|
||||
@@ -59,21 +60,17 @@
|
||||
<link rel="stylesheet" href="all-styles.css" />
|
||||
|
||||
<!-- ✅ 关键 CSS 内联 + 异步加载其余 -->
|
||||
<style>/* 首屏关键样式 */</style>
|
||||
<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-face {
|
||||
font-family: 'CustomFont';
|
||||
src: url('font.woff2');
|
||||
font-display: swap; /* 先用系统字体,加载后切换 */
|
||||
}
|
||||
@font-face { font-family: 'CustomFont'; src: url('font.woff2'); font-display: swap; /*
|
||||
先用系统字体,加载后切换 */ }
|
||||
```
|
||||
|
||||
### INP 优化检查
|
||||
@@ -89,7 +86,7 @@ button.addEventListener('click', () => {
|
||||
// ✅ 拆分长任务
|
||||
button.addEventListener('click', async () => {
|
||||
// 让出主线程
|
||||
await scheduler.yield?.() ?? new Promise(r => setTimeout(r, 0));
|
||||
(await scheduler.yield?.()) ?? new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
// 分批处理
|
||||
for (const chunk of chunks) {
|
||||
@@ -109,7 +106,9 @@ worker.onmessage = (e) => updateUI(e.data);
|
||||
|
||||
```css
|
||||
/* ❌ 未指定尺寸的媒体 */
|
||||
img { width: 100%; }
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ✅ 预留空间 */
|
||||
img {
|
||||
@@ -118,7 +117,8 @@ img {
|
||||
}
|
||||
|
||||
/* ❌ 动态插入内容导致布局偏移 */
|
||||
.ad-container { }
|
||||
.ad-container {
|
||||
}
|
||||
|
||||
/* ✅ 预留固定高度 */
|
||||
.ad-container {
|
||||
@@ -127,6 +127,7 @@ img {
|
||||
```
|
||||
|
||||
**CLS 审查清单:**
|
||||
|
||||
- [ ] 图片/视频是否有 width/height 或 aspect-ratio?
|
||||
- [ ] 字体加载是否使用 `font-display: swap`?
|
||||
- [ ] 动态内容是否预留空间?
|
||||
@@ -175,7 +176,7 @@ import { format } from 'date-fns';
|
||||
// ❌ 未使用 Tree Shaking
|
||||
export default {
|
||||
fn1() {},
|
||||
fn2() {}, // 未使用但被打包
|
||||
fn2() {}, // 未使用但被打包
|
||||
};
|
||||
|
||||
// ✅ 命名导出支持 Tree Shaking
|
||||
@@ -184,6 +185,7 @@ export function fn2() {}
|
||||
```
|
||||
|
||||
**Bundle 审查清单:**
|
||||
|
||||
- [ ] 是否使用动态 import() 进行代码分割?
|
||||
- [ ] 大型库是否按需导入?
|
||||
- [ ] 是否分析过 bundle 大小?(webpack-bundle-analyzer)
|
||||
@@ -196,9 +198,11 @@ export function fn2() {}
|
||||
function List({ items }) {
|
||||
return (
|
||||
<ul>
|
||||
{items.map(item => <li key={item.id}>{item.name}</li>)}
|
||||
{items.map((item) => (
|
||||
<li key={item.id}>{item.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
); // 10000 条数据 = 10000 个 DOM 节点
|
||||
); // 10000 条数据 = 10000 个 DOM 节点
|
||||
}
|
||||
|
||||
// ✅ 虚拟列表 - 只渲染可见项
|
||||
@@ -206,20 +210,15 @@ 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 height={400} itemCount={items.length} itemSize={35}>
|
||||
{({ index, style }) => <div style={style}>{items[index].name}</div>}
|
||||
</FixedSizeList>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**大数据审查要点:**
|
||||
|
||||
- [ ] 列表超过 100 项是否使用虚拟滚动?
|
||||
- [ ] 表格是否支持分页或虚拟化?
|
||||
- [ ] 是否有不必要的全量渲染?
|
||||
@@ -276,7 +275,7 @@ function createHandler() {
|
||||
// ✅ 只保留必要数据
|
||||
function createHandler() {
|
||||
const largeData = new Array(1000000).fill('x');
|
||||
const length = largeData.length; // 只保留需要的值
|
||||
const length = largeData.length; // 只保留需要的值
|
||||
|
||||
return function handler() {
|
||||
console.log(length);
|
||||
@@ -314,11 +313,11 @@ useEffect(() => {
|
||||
|
||||
### 检测工具
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| Chrome DevTools Memory | 堆快照分析 |
|
||||
| MemLab (Meta) | 自动化内存泄漏检测 |
|
||||
| Performance Monitor | 实时内存监控 |
|
||||
| 工具 | 用途 |
|
||||
| ---------------------- | ------------------ |
|
||||
| Chrome DevTools Memory | 堆快照分析 |
|
||||
| MemLab (Meta) | 自动化内存泄漏检测 |
|
||||
| Performance Monitor | 实时内存监控 |
|
||||
|
||||
---
|
||||
|
||||
@@ -346,7 +345,7 @@ posts = Post.objects.prefetch_related('tags').all()
|
||||
// ❌ N+1 问题
|
||||
const users = await userRepository.find();
|
||||
for (const user of users) {
|
||||
const posts = await user.posts; // 每次循环都查询
|
||||
const posts = await user.posts; // 每次循环都查询
|
||||
}
|
||||
|
||||
// ✅ Eager Loading
|
||||
@@ -405,12 +404,14 @@ cursor.execute("SELECT * FROM users WHERE id IN %s", (tuple(user_ids),))
|
||||
|
||||
```markdown
|
||||
🔴 必须检查:
|
||||
|
||||
- [ ] 是否存在 N+1 查询?
|
||||
- [ ] WHERE 子句列是否有索引?
|
||||
- [ ] 是否避免了 SELECT *?
|
||||
- [ ] 是否避免了 SELECT \*?
|
||||
- [ ] 大表查询是否有 LIMIT?
|
||||
|
||||
🟡 建议检查:
|
||||
|
||||
- [ ] 是否使用了 EXPLAIN 分析查询计划?
|
||||
- [ ] 复合索引列顺序是否正确?
|
||||
- [ ] 是否有未使用的索引?
|
||||
@@ -426,14 +427,14 @@ cursor.execute("SELECT * FROM users WHERE id IN %s", (tuple(user_ids),))
|
||||
```javascript
|
||||
// ❌ 返回全部数据
|
||||
app.get('/users', async (req, res) => {
|
||||
const users = await User.findAll(); // 可能返回 100000 条
|
||||
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 limit = Math.min(parseInt(req.query.limit) || 20, 100); // 最大 100
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const { rows, count } = await User.findAndCountAll({
|
||||
@@ -479,8 +480,8 @@ async function getUser(id) {
|
||||
// ✅ HTTP 缓存头
|
||||
app.get('/static-data', (req, res) => {
|
||||
res.set({
|
||||
'Cache-Control': 'public, max-age=86400', // 24 小时
|
||||
'ETag': 'abc123',
|
||||
'Cache-Control': 'public, max-age=86400', // 24 小时
|
||||
ETag: 'abc123',
|
||||
});
|
||||
res.json(data);
|
||||
});
|
||||
@@ -511,8 +512,8 @@ app.get('/users', async (req, res) => {
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
const limiter = rateLimit({
|
||||
windowMs: 60 * 1000, // 1 分钟
|
||||
max: 100, // 最多 100 次请求
|
||||
windowMs: 60 * 1000, // 1 分钟
|
||||
max: 100, // 最多 100 次请求
|
||||
message: { error: 'Too many requests, please try again later.' },
|
||||
});
|
||||
|
||||
@@ -536,14 +537,14 @@ app.use('/api/', limiter);
|
||||
|
||||
### 常见复杂度对比
|
||||
|
||||
| 复杂度 | 名称 | 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 | ∞ | ∞ | 递归斐波那契 |
|
||||
| 复杂度 | 名称 | 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 | ∞ | ∞ | 递归斐波那契 |
|
||||
|
||||
### 代码审查中的识别
|
||||
|
||||
@@ -580,7 +581,8 @@ function findDuplicates(arr) {
|
||||
function removeDuplicates(arr) {
|
||||
const result = [];
|
||||
for (const item of arr) {
|
||||
if (!result.includes(item)) { // includes 是 O(n)
|
||||
if (!result.includes(item)) {
|
||||
// includes 是 O(n)
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
@@ -613,7 +615,7 @@ function getUser(id) {
|
||||
|
||||
```javascript
|
||||
// ⚠️ O(n) 空间 - 创建新数组
|
||||
const doubled = arr.map(x => x * 2);
|
||||
const doubled = arr.map((x) => x * 2);
|
||||
|
||||
// ✅ O(1) 空间 - 原地修改(如果允许)
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
@@ -623,7 +625,7 @@ for (let i = 0; i < arr.length; i++) {
|
||||
// ⚠️ 递归深度过大可能栈溢出
|
||||
function factorial(n) {
|
||||
if (n <= 1) return 1;
|
||||
return n * factorial(n - 1); // O(n) 栈空间
|
||||
return n * factorial(n - 1); // O(n) 栈空间
|
||||
}
|
||||
|
||||
// ✅ 迭代版本 O(1) 空间
|
||||
@@ -651,34 +653,40 @@ function factorial(n) {
|
||||
### 🔴 必须检查(阻塞级)
|
||||
|
||||
**前端:**
|
||||
|
||||
- [ ] LCP 图片是否懒加载?(不应该)
|
||||
- [ ] 是否有 `transition: all`?
|
||||
- [ ] 是否动画 width/height/top/left?
|
||||
- [ ] 列表 >100 项是否虚拟化?
|
||||
|
||||
**后端:**
|
||||
|
||||
- [ ] 是否存在 N+1 查询?
|
||||
- [ ] 列表接口是否有分页?
|
||||
- [ ] 是否有 SELECT * 查大表?
|
||||
- [ ] 是否有 SELECT \* 查大表?
|
||||
|
||||
**通用:**
|
||||
|
||||
- [ ] 是否有 O(n²) 或更差的嵌套循环?
|
||||
- [ ] useEffect/事件监听是否有清理?
|
||||
|
||||
### 🟡 建议检查(重要级)
|
||||
|
||||
**前端:**
|
||||
|
||||
- [ ] 是否使用代码分割?
|
||||
- [ ] 大型库是否按需导入?
|
||||
- [ ] 图片是否使用 WebP/AVIF?
|
||||
- [ ] 是否有未使用的依赖?
|
||||
|
||||
**后端:**
|
||||
|
||||
- [ ] 热点数据是否有缓存?
|
||||
- [ ] WHERE 列是否有索引?
|
||||
- [ ] 是否有慢查询监控?
|
||||
|
||||
**API:**
|
||||
|
||||
- [ ] 是否启用响应压缩?
|
||||
- [ ] 是否有速率限制?
|
||||
- [ ] 是否只返回必要字段?
|
||||
@@ -696,21 +704,21 @@ function factorial(n) {
|
||||
|
||||
### 前端指标
|
||||
|
||||
| 指标 | 好 | 需改进 | 差 |
|
||||
|------|-----|--------|-----|
|
||||
| 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 |
|
||||
| 指标 | 好 | 需改进 | 差 |
|
||||
| ---------------- | ------- | --------- | ------- |
|
||||
| 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 |
|
||||
| 数据库查询 | < 50ms | 50-200ms | > 200ms |
|
||||
| 页面加载 | < 3s | 3-5s | > 5s |
|
||||
|
||||
---
|
||||
|
||||
@@ -718,27 +726,27 @@ function factorial(n) {
|
||||
|
||||
### 前端性能
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| [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/) | 运行时性能分析 |
|
||||
| 工具 | 用途 |
|
||||
| -------------------------------------------------------------------------------------- | -------------------- |
|
||||
| [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 | 堆快照分析 |
|
||||
| Chrome Memory Tab | 堆快照分析 |
|
||||
|
||||
### 后端性能
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| EXPLAIN | 数据库查询计划分析 |
|
||||
| [pganalyze](https://pganalyze.com/) | PostgreSQL 性能监控 |
|
||||
| [New Relic](https://newrelic.com/) / [Datadog](https://www.datadoghq.com/) | APM 监控 |
|
||||
| 工具 | 用途 |
|
||||
| -------------------------------------------------------------------------- | ------------------- |
|
||||
| EXPLAIN | 数据库查询计划分析 |
|
||||
| [pganalyze](https://pganalyze.com/) | PostgreSQL 性能监控 |
|
||||
| [New Relic](https://newrelic.com/) / [Datadog](https://www.datadoghq.com/) | APM 监控 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1023,6 +1023,7 @@ def handle_response(response: dict):
|
||||
## Review Checklist
|
||||
|
||||
### 类型安全
|
||||
|
||||
- [ ] 函数有类型注解(参数和返回值)
|
||||
- [ ] 使用 `Optional` 明确可能为 None
|
||||
- [ ] 泛型类型正确使用
|
||||
@@ -1030,6 +1031,7 @@ def handle_response(response: dict):
|
||||
- [ ] 避免使用 `Any`,必要时添加注释说明
|
||||
|
||||
### 异步代码
|
||||
|
||||
- [ ] async/await 正确配对使用
|
||||
- [ ] 没有在异步代码中使用阻塞调用
|
||||
- [ ] 正确处理 `CancelledError`
|
||||
@@ -1037,18 +1039,21 @@ def handle_response(response: dict):
|
||||
- [ ] 资源正确清理(async context manager)
|
||||
|
||||
### 异常处理
|
||||
|
||||
- [ ] 捕获特定异常类型,不使用裸 `except:`
|
||||
- [ ] 异常链使用 `from` 保留原因
|
||||
- [ ] 自定义异常继承自合适的基类
|
||||
- [ ] 异常信息有意义,便于调试
|
||||
|
||||
### 数据结构
|
||||
|
||||
- [ ] 没有使用可变默认参数(list、dict、set)
|
||||
- [ ] 类属性不是可变对象
|
||||
- [ ] 选择正确的数据结构(set vs list 查找)
|
||||
- [ ] 大数据集使用生成器而非列表
|
||||
|
||||
### 测试
|
||||
|
||||
- [ ] 测试覆盖率达标(建议 ≥80%)
|
||||
- [ ] 测试命名清晰描述测试场景
|
||||
- [ ] 边界情况有测试覆盖
|
||||
@@ -1056,6 +1061,7 @@ def handle_response(response: dict):
|
||||
- [ ] 异步代码有对应的异步测试
|
||||
|
||||
### 代码风格
|
||||
|
||||
- [ ] 遵循 PEP 8 风格指南
|
||||
- [ ] 函数和类有 docstring
|
||||
- [ ] 导入顺序正确(标准库、第三方、本地)
|
||||
@@ -1063,6 +1069,7 @@ def handle_response(response: dict):
|
||||
- [ ] 使用现代 Python 特性(f-string、walrus operator 等)
|
||||
|
||||
### 性能
|
||||
|
||||
- [ ] 避免循环中重复创建对象
|
||||
- [ ] 字符串拼接使用 join
|
||||
- [ ] 合理使用缓存(@lru_cache)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
## 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
|
||||
@@ -32,6 +33,7 @@ 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
|
||||
@@ -46,6 +48,7 @@ if (safePtr) {
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
---
|
||||
@@ -53,6 +56,7 @@ For asynchronous deletion, especially in slots or event handlers, use `deleteLat
|
||||
## Signals & Slots
|
||||
|
||||
### Prefer Function Pointer Syntax
|
||||
|
||||
Use compile-time checked syntax (Qt 5+).
|
||||
|
||||
```cpp
|
||||
@@ -64,12 +68,15 @@ 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
|
||||
@@ -85,6 +92,7 @@ void MyClass::setValue(int 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).
|
||||
@@ -99,14 +107,15 @@ 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.
|
||||
- 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;
|
||||
list[0] = 1;
|
||||
}
|
||||
|
||||
// ✅ Read-only reference
|
||||
@@ -118,12 +127,13 @@ 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 { ... }
|
||||
void run() override { ... }
|
||||
};
|
||||
|
||||
// ✅ Worker object moved to thread
|
||||
@@ -135,6 +145,7 @@ 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.
|
||||
|
||||
---
|
||||
@@ -142,13 +153,17 @@ thread->start();
|
||||
## 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`).
|
||||
|
||||
@@ -157,6 +172,7 @@ Never execute long-running operations on the main thread (freezes GUI).
|
||||
## 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.
|
||||
|
||||
@@ -172,6 +188,7 @@ public:
|
||||
```
|
||||
|
||||
### qobject_cast
|
||||
|
||||
Use `qobject_cast<T*>` for QObjects instead of `dynamic_cast`. It is faster and doesn't require RTTI.
|
||||
|
||||
---
|
||||
@@ -183,4 +200,4 @@ Use `qobject_cast<T*>` for QObjects instead of `dynamic_cast`. It is faster and
|
||||
- [ ] **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`?
|
||||
- [ ] **Resources**: Are resources (images, styles) loaded from `.qrc`?
|
||||
|
||||
@@ -23,7 +23,7 @@ React 审查重点:Hooks 规则、性能优化的适度性、组件设计、
|
||||
// ❌ 条件调用 Hooks — 违反 Hooks 规则
|
||||
function BadComponent({ isLoggedIn }) {
|
||||
if (isLoggedIn) {
|
||||
const [user, setUser] = useState(null); // Error!
|
||||
const [user, setUser] = useState(null); // Error!
|
||||
}
|
||||
return <div>...</div>;
|
||||
}
|
||||
@@ -46,7 +46,7 @@ function BadEffect({ userId }) {
|
||||
const [user, setUser] = useState(null);
|
||||
useEffect(() => {
|
||||
fetchUser(userId).then(setUser);
|
||||
}, []); // 缺少 userId 依赖!
|
||||
}, []); // 缺少 userId 依赖!
|
||||
}
|
||||
|
||||
// ✅ 完整的依赖数组
|
||||
@@ -54,10 +54,12 @@ function GoodEffect({ userId }) {
|
||||
const [user, setUser] = useState(null);
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
fetchUser(userId).then(data => {
|
||||
fetchUser(userId).then((data) => {
|
||||
if (!cancelled) setUser(data);
|
||||
});
|
||||
return () => { cancelled = true; }; // 清理函数
|
||||
return () => {
|
||||
cancelled = true;
|
||||
}; // 清理函数
|
||||
}, [userId]);
|
||||
}
|
||||
|
||||
@@ -65,17 +67,14 @@ function GoodEffect({ userId }) {
|
||||
function BadDerived({ items }) {
|
||||
const [filteredItems, setFilteredItems] = useState([]);
|
||||
useEffect(() => {
|
||||
setFilteredItems(items.filter(i => i.active));
|
||||
}, [items]); // 不必要的 effect + 额外渲染
|
||||
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]
|
||||
);
|
||||
const filteredItems = useMemo(() => items.filter((i) => i.active), [items]);
|
||||
return <List items={filteredItems} />;
|
||||
}
|
||||
|
||||
@@ -84,7 +83,7 @@ function BadEventEffect() {
|
||||
const [query, setQuery] = useState('');
|
||||
useEffect(() => {
|
||||
if (query) {
|
||||
analytics.track('search', { query }); // 应该在事件处理器中
|
||||
analytics.track('search', { query }); // 应该在事件处理器中
|
||||
}
|
||||
}, [query]);
|
||||
}
|
||||
@@ -106,15 +105,15 @@ function GoodEvent() {
|
||||
```tsx
|
||||
// ❌ 过度优化 — 常量不需要 useMemo
|
||||
function OverOptimized() {
|
||||
const config = useMemo(() => ({ timeout: 5000 }), []); // 无意义
|
||||
const config = useMemo(() => ({ timeout: 5000 }), []); // 无意义
|
||||
const handleClick = useCallback(() => {
|
||||
console.log('clicked');
|
||||
}, []); // 如果不传给 memo 组件,无意义
|
||||
}, []); // 如果不传给 memo 组件,无意义
|
||||
}
|
||||
|
||||
// ✅ 只在需要时优化
|
||||
function ProperlyOptimized() {
|
||||
const config = { timeout: 5000 }; // 简单对象直接定义
|
||||
const config = { timeout: 5000 }; // 简单对象直接定义
|
||||
const handleClick = () => console.log('clicked');
|
||||
}
|
||||
|
||||
@@ -147,7 +146,8 @@ function Parent({ rawItems }) {
|
||||
```tsx
|
||||
// ❌ 在组件内定义组件 — 每次渲染都创建新组件
|
||||
function BadParent() {
|
||||
function ChildComponent() { // 每次渲染都是新函数!
|
||||
function ChildComponent() {
|
||||
// 每次渲染都是新函数!
|
||||
return <div>child</div>;
|
||||
}
|
||||
return <ChildComponent />;
|
||||
@@ -165,8 +165,8 @@ function GoodParent() {
|
||||
function BadProps() {
|
||||
return (
|
||||
<MemoizedComponent
|
||||
style={{ color: 'red' }} // 每次渲染新对象
|
||||
onClick={() => {}} // 每次渲染新函数
|
||||
style={{ color: 'red' }} // 每次渲染新对象
|
||||
onClick={() => {}} // 每次渲染新函数
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -188,7 +188,7 @@ function GoodProps() {
|
||||
function BadApp() {
|
||||
return (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<DataComponent /> {/* 错误会导致整个应用崩溃 */}
|
||||
<DataComponent /> {/* 错误会导致整个应用崩溃 */}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -287,15 +287,13 @@ function NewForm() {
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
},
|
||||
{ success: false, data: null, error: null }
|
||||
{ success: false, data: null, error: null },
|
||||
);
|
||||
|
||||
return (
|
||||
<form action={formAction}>
|
||||
<input name="email" />
|
||||
<button disabled={isPending}>
|
||||
{isPending ? 'Submitting...' : 'Submit'}
|
||||
</button>
|
||||
<button disabled={isPending}>{isPending ? 'Submitting...' : 'Submit'}</button>
|
||||
{state.error && <p className="error">{state.error}</p>}
|
||||
</form>
|
||||
);
|
||||
@@ -316,16 +314,12 @@ import { useFormStatus } from 'react-dom';
|
||||
function SubmitButton() {
|
||||
const { pending, data, method, action } = useFormStatus();
|
||||
// 注意:必须在 <form> 内部的子组件中使用
|
||||
return (
|
||||
<button disabled={pending}>
|
||||
{pending ? 'Submitting...' : 'Submit'}
|
||||
</button>
|
||||
);
|
||||
return <button disabled={pending}>{pending ? 'Submitting...' : 'Submit'}</button>;
|
||||
}
|
||||
|
||||
// ❌ useFormStatus 在 form 同级组件中调用——不工作
|
||||
function BadForm() {
|
||||
const { pending } = useFormStatus(); // 这里无法获取状态!
|
||||
const { pending } = useFormStatus(); // 这里无法获取状态!
|
||||
return (
|
||||
<form action={action}>
|
||||
<button disabled={pending}>Submit</button>
|
||||
@@ -337,7 +331,7 @@ function BadForm() {
|
||||
function GoodForm() {
|
||||
return (
|
||||
<form action={action}>
|
||||
<SubmitButton /> {/* useFormStatus 在这里面调用 */}
|
||||
<SubmitButton /> {/* useFormStatus 在这里面调用 */}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -353,7 +347,7 @@ function SlowLike({ postId, likes }) {
|
||||
|
||||
const handleLike = async () => {
|
||||
setIsPending(true);
|
||||
const newCount = await likePost(postId); // 等待...
|
||||
const newCount = await likePost(postId); // 等待...
|
||||
setLikeCount(newCount);
|
||||
setIsPending(false);
|
||||
};
|
||||
@@ -365,13 +359,13 @@ import { useOptimistic } from 'react';
|
||||
function FastLike({ postId, likes }) {
|
||||
const [optimisticLikes, addOptimisticLike] = useOptimistic(
|
||||
likes,
|
||||
(currentLikes, increment: number) => currentLikes + increment
|
||||
(currentLikes, increment: number) => currentLikes + increment,
|
||||
);
|
||||
|
||||
const handleLike = async () => {
|
||||
addOptimisticLike(1); // 立即更新 UI
|
||||
addOptimisticLike(1); // 立即更新 UI
|
||||
try {
|
||||
await likePost(postId); // 后台同步
|
||||
await likePost(postId); // 后台同步
|
||||
} catch {
|
||||
// React 自动回滚到 likes 原值
|
||||
}
|
||||
@@ -398,7 +392,7 @@ function ClientForm() {
|
||||
|
||||
// ✅ Server Action + useActionState
|
||||
// actions.ts
|
||||
'use server';
|
||||
('use server');
|
||||
export async function createPost(prevState: any, formData: FormData) {
|
||||
const title = formData.get('title');
|
||||
await db.posts.create({ title });
|
||||
@@ -407,7 +401,7 @@ export async function createPost(prevState: any, formData: FormData) {
|
||||
}
|
||||
|
||||
// form.tsx
|
||||
'use client';
|
||||
('use client');
|
||||
import { createPost } from './actions';
|
||||
|
||||
function PostForm() {
|
||||
@@ -436,7 +430,9 @@ function OldComponent() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData().then(setData).finally(() => setIsLoading(false));
|
||||
fetchData()
|
||||
.then(setData)
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
if (isLoading) return <Spinner />;
|
||||
@@ -447,7 +443,7 @@ function OldComponent() {
|
||||
function NewComponent() {
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<DataView /> {/* 内部使用 use() 或支持 Suspense 的数据获取 */}
|
||||
<DataView /> {/* 内部使用 use() 或支持 Suspense 的数据获取 */}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -461,8 +457,8 @@ function BadLayout() {
|
||||
return (
|
||||
<Suspense fallback={<FullPageSpinner />}>
|
||||
<Header />
|
||||
<MainContent /> {/* 慢 */}
|
||||
<Sidebar /> {/* 快 */}
|
||||
<MainContent /> {/* 慢 */}
|
||||
<Sidebar /> {/* 快 */}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -471,13 +467,13 @@ function BadLayout() {
|
||||
function GoodLayout() {
|
||||
return (
|
||||
<>
|
||||
<Header /> {/* 立即显示 */}
|
||||
<Header /> {/* 立即显示 */}
|
||||
<div className="flex">
|
||||
<Suspense fallback={<ContentSkeleton />}>
|
||||
<MainContent /> {/* 独立加载 */}
|
||||
<MainContent /> {/* 独立加载 */}
|
||||
</Suspense>
|
||||
<Suspense fallback={<SidebarSkeleton />}>
|
||||
<Sidebar /> {/* 独立加载 */}
|
||||
<Sidebar /> {/* 独立加载 */}
|
||||
</Suspense>
|
||||
</div>
|
||||
</>
|
||||
@@ -508,17 +504,19 @@ export default function Loading() {
|
||||
import { use } from 'react';
|
||||
|
||||
function Comments({ commentsPromise }) {
|
||||
const comments = use(commentsPromise); // 自动触发 Suspense
|
||||
const comments = use(commentsPromise); // 自动触发 Suspense
|
||||
return (
|
||||
<ul>
|
||||
{comments.map(c => <li key={c.id}>{c.text}</li>)}
|
||||
{comments.map((c) => (
|
||||
<li key={c.id}>{c.text}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
// 父组件创建 Promise,子组件消费
|
||||
function Post({ postId }) {
|
||||
const commentsPromise = fetchComments(postId); // 不 await
|
||||
const commentsPromise = fetchComments(postId); // 不 await
|
||||
return (
|
||||
<article>
|
||||
<PostContent id={postId} />
|
||||
@@ -540,16 +538,16 @@ TanStack Query 是 React 生态中最流行的数据获取库,v5 是当前稳
|
||||
|
||||
```tsx
|
||||
// ❌ 不正确的默认配置
|
||||
const queryClient = new QueryClient(); // 默认配置可能不适合
|
||||
const queryClient = new QueryClient(); // 默认配置可能不适合
|
||||
|
||||
// ✅ 生产环境推荐配置
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 5, // 5 分钟内数据视为新鲜
|
||||
gcTime: 1000 * 60 * 30, // 30 分钟后垃圾回收(v5 重命名)
|
||||
staleTime: 1000 * 60 * 5, // 5 分钟内数据视为新鲜
|
||||
gcTime: 1000 * 60 * 30, // 30 分钟后垃圾回收(v5 重命名)
|
||||
retry: 3,
|
||||
refetchOnWindowFocus: false, // 根据需求决定
|
||||
refetchOnWindowFocus: false, // 根据需求决定
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -568,8 +566,8 @@ function Component1() {
|
||||
|
||||
function prefetchUser(queryClient, userId) {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['users', userId], // 重复!
|
||||
queryFn: () => fetchUser(userId), // 重复!
|
||||
queryKey: ['users', userId], // 重复!
|
||||
queryFn: () => fetchUser(userId), // 重复!
|
||||
});
|
||||
}
|
||||
|
||||
@@ -608,21 +606,21 @@ useQuery({
|
||||
useQuery({
|
||||
queryKey: ['data'],
|
||||
queryFn: fetchData,
|
||||
staleTime: 1000 * 60, // 1 分钟内不会重新请求
|
||||
staleTime: 1000 * 60, // 1 分钟内不会重新请求
|
||||
});
|
||||
|
||||
// ❌ 在 queryFn 中使用不稳定的引用
|
||||
function BadQuery({ filters }) {
|
||||
useQuery({
|
||||
queryKey: ['items'], // queryKey 没有包含 filters!
|
||||
queryFn: () => fetchItems(filters), // filters 变化不会触发重新请求
|
||||
queryKey: ['items'], // queryKey 没有包含 filters!
|
||||
queryFn: () => fetchItems(filters), // filters 变化不会触发重新请求
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ queryKey 包含所有影响数据的参数
|
||||
function GoodQuery({ filters }) {
|
||||
useQuery({
|
||||
queryKey: ['items', filters], // filters 是 queryKey 的一部分
|
||||
queryKey: ['items', filters], // filters 是 queryKey 的一部分
|
||||
queryFn: () => fetchItems(filters),
|
||||
});
|
||||
}
|
||||
@@ -634,13 +632,13 @@ function GoodQuery({ filters }) {
|
||||
|
||||
#### useSuspenseQuery 的限制
|
||||
|
||||
| 特性 | useQuery | useSuspenseQuery |
|
||||
|------|----------|------------------|
|
||||
| `enabled` 选项 | ✅ 支持 | ❌ 不支持 |
|
||||
| `placeholderData` | ✅ 支持 | ❌ 不支持 |
|
||||
| `data` 类型 | `T \| undefined` | `T`(保证有值)|
|
||||
| 错误处理 | `error` 属性 | 抛出到 Error Boundary |
|
||||
| 加载状态 | `isLoading` 属性 | 挂起到 Suspense |
|
||||
| 特性 | useQuery | useSuspenseQuery |
|
||||
| ----------------- | ---------------- | --------------------- |
|
||||
| `enabled` 选项 | ✅ 支持 | ❌ 不支持 |
|
||||
| `placeholderData` | ✅ 支持 | ❌ 不支持 |
|
||||
| `data` 类型 | `T \| undefined` | `T`(保证有值) |
|
||||
| 错误处理 | `error` 属性 | 抛出到 Error Boundary |
|
||||
| 加载状态 | `isLoading` 属性 | 挂起到 Suspense |
|
||||
|
||||
#### 不支持 enabled 的替代方案
|
||||
|
||||
@@ -650,7 +648,7 @@ function BadSuspenseQuery({ userId }) {
|
||||
const { data } = useSuspenseQuery({
|
||||
queryKey: ['user', userId],
|
||||
queryFn: () => fetchUser(userId),
|
||||
enabled: !!userId, // useSuspenseQuery 不支持 enabled!
|
||||
enabled: !!userId, // useSuspenseQuery 不支持 enabled!
|
||||
});
|
||||
}
|
||||
|
||||
@@ -764,7 +762,9 @@ function TodoList() {
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{todos?.map(todo => <TodoItem key={todo.id} todo={todo} />)}
|
||||
{todos?.map((todo) => (
|
||||
<TodoItem key={todo.id} todo={todo} />
|
||||
))}
|
||||
{/* 乐观显示正在添加的 todo */}
|
||||
{isPending && <TodoItem todo={variables} isOptimistic />}
|
||||
</ul>
|
||||
@@ -867,5 +867,5 @@ if (isLoading) return <Spinner />; // 首次加载中
|
||||
- [ ] 使用 @testing-library/react
|
||||
- [ ] 用 screen 查询元素
|
||||
- [ ] 用 userEvent 代替 fireEvent
|
||||
- [ ] 优先使用 *ByRole 查询
|
||||
- [ ] 优先使用 \*ByRole 查询
|
||||
- [ ] 测试行为而非实现细节
|
||||
|
||||
@@ -764,11 +764,13 @@ fn create_handler() -> impl Handler {
|
||||
### 编译器不能捕获的问题
|
||||
|
||||
**业务逻辑正确性**
|
||||
|
||||
- [ ] 边界条件处理正确
|
||||
- [ ] 状态机转换完整
|
||||
- [ ] 并发场景下的竞态条件
|
||||
|
||||
**API 设计**
|
||||
|
||||
- [ ] 公共 API 难以误用
|
||||
- [ ] 类型签名清晰表达意图
|
||||
- [ ] 错误类型粒度合适
|
||||
|
||||
+50
-28
@@ -5,6 +5,7 @@ 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
|
||||
@@ -14,6 +15,7 @@ Security-focused code review checklist based on OWASP Top 10 and best practices.
|
||||
- [ ] Session timeout implemented
|
||||
|
||||
### Authorization
|
||||
|
||||
- [ ] Authorization checks on every request
|
||||
- [ ] Principle of least privilege applied
|
||||
- [ ] Role-based access control (RBAC) properly implemented
|
||||
@@ -22,6 +24,7 @@ Security-focused code review checklist based on OWASP Top 10 and best practices.
|
||||
- [ ] API endpoints protected appropriately
|
||||
|
||||
### JWT Security
|
||||
|
||||
```typescript
|
||||
// ❌ Insecure JWT configuration
|
||||
jwt.sign(payload, 'weak-secret');
|
||||
@@ -31,23 +34,24 @@ jwt.sign(payload, process.env.JWT_SECRET, {
|
||||
algorithm: 'RS256',
|
||||
expiresIn: '15m',
|
||||
issuer: 'your-app',
|
||||
audience: 'your-api'
|
||||
audience: 'your-api',
|
||||
});
|
||||
|
||||
// ❌ Not verifying JWT properly
|
||||
const decoded = jwt.decode(token); // No signature verification!
|
||||
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'
|
||||
audience: 'your-api',
|
||||
});
|
||||
```
|
||||
|
||||
## Input Validation
|
||||
|
||||
### SQL Injection Prevention
|
||||
|
||||
```python
|
||||
# ❌ Vulnerable to SQL injection
|
||||
query = f"SELECT * FROM users WHERE id = {user_id}"
|
||||
@@ -60,6 +64,7 @@ User.objects.filter(id=user_id)
|
||||
```
|
||||
|
||||
### XSS Prevention
|
||||
|
||||
```typescript
|
||||
// ❌ Vulnerable to XSS
|
||||
element.innerHTML = userInput;
|
||||
@@ -76,6 +81,7 @@ return <div dangerouslySetInnerHTML={{__html: userInput}} />; // Dangerous!
|
||||
```
|
||||
|
||||
### Command Injection Prevention
|
||||
|
||||
```python
|
||||
# ❌ Vulnerable to command injection
|
||||
os.system(f"convert {filename} output.png")
|
||||
@@ -89,6 +95,7 @@ safe_filename = shlex.quote(filename)
|
||||
```
|
||||
|
||||
### Path Traversal Prevention
|
||||
|
||||
```typescript
|
||||
// ❌ Vulnerable to path traversal
|
||||
const filePath = `./uploads/${req.params.filename}`;
|
||||
@@ -107,6 +114,7 @@ if (!filePath.startsWith(path.resolve('./uploads'))) {
|
||||
## Data Protection
|
||||
|
||||
### Sensitive Data Handling
|
||||
|
||||
- [ ] No secrets in source code
|
||||
- [ ] Secrets stored in environment variables or secret manager
|
||||
- [ ] Sensitive data encrypted at rest
|
||||
@@ -116,6 +124,7 @@ if (!filePath.startsWith(path.resolve('./uploads'))) {
|
||||
- [ ] Secure data deletion when required
|
||||
|
||||
### Configuration Security
|
||||
|
||||
```yaml
|
||||
# ❌ Secrets in config files
|
||||
database:
|
||||
@@ -127,6 +136,7 @@ database:
|
||||
```
|
||||
|
||||
### Error Messages
|
||||
|
||||
```typescript
|
||||
// ❌ Leaking sensitive information
|
||||
catch (error) {
|
||||
@@ -148,45 +158,53 @@ catch (error) {
|
||||
## 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
|
||||
}));
|
||||
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' }
|
||||
}));
|
||||
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
|
||||
@@ -194,6 +212,7 @@ app.use(helmet({
|
||||
- [ ] Secure key storage (HSM, KMS)
|
||||
|
||||
### Common Mistakes
|
||||
|
||||
```typescript
|
||||
// ❌ Weak random generation
|
||||
const token = Math.random().toString(36);
|
||||
@@ -213,6 +232,7 @@ 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
|
||||
@@ -221,6 +241,7 @@ const hash = await bcrypt.hash(password, 12);
|
||||
- [ ] License compliance verified
|
||||
|
||||
### Audit Commands
|
||||
|
||||
```bash
|
||||
# Node.js
|
||||
npm audit
|
||||
@@ -240,6 +261,7 @@ snyk test
|
||||
## Logging & Monitoring
|
||||
|
||||
### Secure Logging
|
||||
|
||||
- [ ] No sensitive data in logs (passwords, tokens, PII)
|
||||
- [ ] Logs protected from tampering
|
||||
- [ ] Appropriate log retention
|
||||
@@ -256,10 +278,10 @@ 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 |
|
||||
| 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 |
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
```typescript
|
||||
// ❌ Using any defeats type safety
|
||||
function processData(data: any) {
|
||||
return data.value; // 无类型检查,运行时可能崩溃
|
||||
return data.value; // 无类型检查,运行时可能崩溃
|
||||
}
|
||||
|
||||
// ✅ Use proper types
|
||||
@@ -47,7 +47,7 @@ function processUnknown(data: unknown) {
|
||||
```typescript
|
||||
// ❌ 不安全的类型断言
|
||||
function getLength(value: string | string[]) {
|
||||
return (value as string[]).length; // 如果是 string 会出错
|
||||
return (value as string[]).length; // 如果是 string 会出错
|
||||
}
|
||||
|
||||
// ✅ 使用类型守卫
|
||||
@@ -59,8 +59,12 @@ function getLength(value: string | string[]): number {
|
||||
}
|
||||
|
||||
// ✅ 使用 in 操作符
|
||||
interface Dog { bark(): void }
|
||||
interface Cat { meow(): void }
|
||||
interface Dog {
|
||||
bark(): void;
|
||||
}
|
||||
interface Cat {
|
||||
meow(): void;
|
||||
}
|
||||
|
||||
function speak(animal: Dog | Cat) {
|
||||
if ('bark' in animal) {
|
||||
@@ -117,7 +121,7 @@ function getFirst<T>(arr: T[]): T | undefined {
|
||||
```typescript
|
||||
// ❌ 泛型没有约束,无法访问属性
|
||||
function getProperty<T>(obj: T, key: string) {
|
||||
return obj[key]; // Error: 无法索引
|
||||
return obj[key]; // Error: 无法索引
|
||||
}
|
||||
|
||||
// ✅ 使用 keyof 约束
|
||||
@@ -126,9 +130,9 @@ function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
|
||||
}
|
||||
|
||||
const user = { name: 'Alice', age: 30 };
|
||||
getProperty(user, 'name'); // 返回类型是 string
|
||||
getProperty(user, 'age'); // 返回类型是 number
|
||||
getProperty(user, 'foo'); // Error: 'foo' 不在 keyof User
|
||||
getProperty(user, 'name'); // 返回类型是 string
|
||||
getProperty(user, 'age'); // 返回类型是 number
|
||||
getProperty(user, 'foo'); // Error: 'foo' 不在 keyof User
|
||||
```
|
||||
|
||||
### 泛型默认值
|
||||
@@ -157,13 +161,13 @@ interface User {
|
||||
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 }
|
||||
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 }
|
||||
```
|
||||
|
||||
---
|
||||
@@ -176,13 +180,13 @@ type UserRecord = Record<string, User>; // { [key: string]: User }
|
||||
// ✅ 根据输入类型返回不同类型
|
||||
type IsString<T> = T extends string ? true : false;
|
||||
|
||||
type A = IsString<string>; // true
|
||||
type B = IsString<number>; // 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
|
||||
type Elem = ElementType<string[]>; // string
|
||||
|
||||
// ✅ 提取函数返回类型(内置 ReturnType)
|
||||
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
|
||||
@@ -223,23 +227,21 @@ type HandlerName = `on${Capitalize<EventName>}`;
|
||||
|
||||
// ✅ API 路由类型
|
||||
type ApiRoute = `/api/${string}`;
|
||||
const route: ApiRoute = '/api/users'; // OK
|
||||
const badRoute: ApiRoute = '/users'; // Error
|
||||
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 };
|
||||
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 存在
|
||||
console.log(result.data.name); // TypeScript 知道 data 存在
|
||||
} else {
|
||||
console.log(result.error.message); // TypeScript 知道 error 存在
|
||||
console.log(result.error.message); // TypeScript 知道 error 存在
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,11 +254,11 @@ type Action =
|
||||
function reducer(state: number, action: Action): number {
|
||||
switch (action.type) {
|
||||
case 'INCREMENT':
|
||||
return state + action.payload; // payload 类型已知
|
||||
return state + action.payload; // payload 类型已知
|
||||
case 'DECREMENT':
|
||||
return state - action.payload;
|
||||
case 'RESET':
|
||||
return 0; // 这里没有 payload
|
||||
return 0; // 这里没有 payload
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -296,10 +298,10 @@ function reducer(state: number, action: Action): number {
|
||||
// tsconfig: "noUncheckedIndexedAccess": true
|
||||
|
||||
const arr = [1, 2, 3];
|
||||
const first = arr[0]; // 类型是 number | undefined
|
||||
const first = arr[0]; // 类型是 number | undefined
|
||||
|
||||
// ❌ 直接使用可能出错
|
||||
console.log(first.toFixed(2)); // Error: 可能是 undefined
|
||||
console.log(first.toFixed(2)); // Error: 可能是 undefined
|
||||
|
||||
// ✅ 先检查
|
||||
if (first !== undefined) {
|
||||
@@ -320,7 +322,7 @@ console.log(arr[0]!.toFixed(2));
|
||||
// ❌ Not handling async errors
|
||||
async function fetchUser(id: string) {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
return response.json(); // 网络错误未处理
|
||||
return response.json(); // 网络错误未处理
|
||||
}
|
||||
|
||||
// ✅ Handle errors properly
|
||||
@@ -346,7 +348,7 @@ async function fetchUser(id: string): Promise<User> {
|
||||
// ❌ Promise.all 一个失败全部失败
|
||||
async function fetchAllUsers(ids: string[]) {
|
||||
const users = await Promise.all(ids.map(fetchUser));
|
||||
return users; // 一个失败就全部失败
|
||||
return users; // 一个失败就全部失败
|
||||
}
|
||||
|
||||
// ✅ Promise.allSettled 获取所有结果
|
||||
@@ -378,8 +380,8 @@ function useSearch() {
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/search?q=${query}`)
|
||||
.then(r => r.json())
|
||||
.then(setResults); // 旧请求可能后返回!
|
||||
.then((r) => r.json())
|
||||
.then(setResults); // 旧请求可能后返回!
|
||||
}, [query]);
|
||||
}
|
||||
|
||||
@@ -392,9 +394,9 @@ function useSearch() {
|
||||
const controller = new AbortController();
|
||||
|
||||
fetch(`/api/search?q=${query}`, { signal: controller.signal })
|
||||
.then(r => r.json())
|
||||
.then((r) => r.json())
|
||||
.then(setResults)
|
||||
.catch(e => {
|
||||
.catch((e) => {
|
||||
if (e.name !== 'AbortError') throw e;
|
||||
});
|
||||
|
||||
@@ -412,7 +414,7 @@ function useSearch() {
|
||||
```typescript
|
||||
// ❌ 可变参数可能被意外修改
|
||||
function processUsers(users: User[]) {
|
||||
users.sort((a, b) => a.name.localeCompare(b.name)); // 修改了原数组!
|
||||
users.sort((a, b) => a.name.localeCompare(b.name)); // 修改了原数组!
|
||||
return users;
|
||||
}
|
||||
|
||||
@@ -452,7 +454,7 @@ module.exports = {
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
||||
'plugin:@typescript-eslint/strict'
|
||||
'plugin:@typescript-eslint/strict',
|
||||
],
|
||||
rules: {
|
||||
// ✅ 类型安全
|
||||
@@ -471,8 +473,8 @@ module.exports = {
|
||||
// ✅ 代码风格
|
||||
'@typescript-eslint/consistent-type-imports': 'error',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'error',
|
||||
'@typescript-eslint/prefer-optional-chain': 'error'
|
||||
}
|
||||
'@typescript-eslint/prefer-optional-chain': 'error',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
@@ -509,6 +511,7 @@ await Promise.all(items.map(processItem));
|
||||
## Review Checklist
|
||||
|
||||
### 类型系统
|
||||
|
||||
- [ ] 没有使用 `any`(使用 `unknown` + 类型守卫代替)
|
||||
- [ ] 接口和类型定义完整且有意义的命名
|
||||
- [ ] 使用泛型提高代码复用性
|
||||
@@ -516,16 +519,19 @@ await Promise.all(items.map(processItem));
|
||||
- [ ] 善用工具类型(Partial、Pick、Omit 等)
|
||||
|
||||
### 泛型
|
||||
|
||||
- [ ] 泛型有适当的约束(extends)
|
||||
- [ ] 泛型参数有合理的默认值
|
||||
- [ ] 避免过度泛型化(KISS 原则)
|
||||
|
||||
### Strict 模式
|
||||
|
||||
- [ ] tsconfig.json 启用了 strict: true
|
||||
- [ ] 启用了 noUncheckedIndexedAccess
|
||||
- [ ] 没有使用 @ts-ignore(改用 @ts-expect-error)
|
||||
|
||||
### 异步代码
|
||||
|
||||
- [ ] async 函数有错误处理
|
||||
- [ ] Promise rejection 被正确处理
|
||||
- [ ] 没有 floating promises(未处理的 Promise)
|
||||
@@ -533,11 +539,13 @@ await Promise.all(items.map(processItem));
|
||||
- [ ] 竞态条件使用 AbortController 处理
|
||||
|
||||
### 不可变性
|
||||
|
||||
- [ ] 不直接修改函数参数
|
||||
- [ ] 使用 spread 操作符创建新对象/数组
|
||||
- [ ] 考虑使用 readonly 修饰符
|
||||
|
||||
### ESLint
|
||||
|
||||
- [ ] 使用 @typescript-eslint/recommended
|
||||
- [ ] 没有 ESLint 警告或错误
|
||||
- [ ] 使用 consistent-type-imports
|
||||
|
||||
@@ -22,11 +22,11 @@
|
||||
```vue
|
||||
<!-- ✅ 基本类型用 ref -->
|
||||
<script setup lang="ts">
|
||||
const count = ref(0)
|
||||
const name = ref('Vue')
|
||||
const count = ref(0);
|
||||
const name = ref('Vue');
|
||||
|
||||
// ref 需要 .value 访问
|
||||
count.value++
|
||||
count.value++;
|
||||
</script>
|
||||
|
||||
<!-- ✅ 对象/数组用 reactive(可选)-->
|
||||
@@ -34,18 +34,18 @@ count.value++
|
||||
const state = reactive({
|
||||
user: null,
|
||||
loading: false,
|
||||
error: null
|
||||
})
|
||||
error: null,
|
||||
});
|
||||
|
||||
// reactive 直接访问
|
||||
state.loading = true
|
||||
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)
|
||||
const user = ref<User | null>(null);
|
||||
const loading = ref(false);
|
||||
const error = ref<Error | null>(null);
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -54,17 +54,17 @@ const error = ref<Error | null>(null)
|
||||
```vue
|
||||
<!-- ❌ 解构 reactive 会丢失响应性 -->
|
||||
<script setup lang="ts">
|
||||
const state = reactive({ count: 0, name: 'Vue' })
|
||||
const { count, name } = state // 丢失响应性!
|
||||
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) // 保持响应性
|
||||
const state = reactive({ count: 0, name: 'Vue' });
|
||||
const { count, name } = toRefs(state); // 保持响应性
|
||||
// 或者直接使用 ref
|
||||
const count = ref(0)
|
||||
const name = ref('Vue')
|
||||
const count = ref(0);
|
||||
const name = ref('Vue');
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -74,21 +74,21 @@ const name = ref('Vue')
|
||||
<!-- ❌ computed 中产生副作用 -->
|
||||
<script setup lang="ts">
|
||||
const fullName = computed(() => {
|
||||
console.log('Computing...') // 副作用!
|
||||
otherRef.value = 'changed' // 修改其他状态!
|
||||
return `${firstName.value} ${lastName.value}`
|
||||
})
|
||||
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}`
|
||||
})
|
||||
return `${firstName.value} ${lastName.value}`;
|
||||
});
|
||||
// 副作用放在 watch 或事件处理中
|
||||
watch(fullName, (name) => {
|
||||
console.log('Name changed:', name)
|
||||
})
|
||||
console.log('Name changed:', name);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -97,25 +97,25 @@ watch(fullName, (name) => {
|
||||
```vue
|
||||
<!-- ❌ 大型对象使用 ref 会深度转换 -->
|
||||
<script setup lang="ts">
|
||||
const largeData = ref(hugeNestedObject) // 深度响应式,性能开销大
|
||||
const largeData = ref(hugeNestedObject); // 深度响应式,性能开销大
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 shallowRef 避免深度转换 -->
|
||||
<script setup lang="ts">
|
||||
const largeData = shallowRef(hugeNestedObject)
|
||||
const largeData = shallowRef(hugeNestedObject);
|
||||
|
||||
// 整体替换才会触发更新
|
||||
function updateData(newData) {
|
||||
largeData.value = newData // ✅ 触发更新
|
||||
largeData.value = newData; // ✅ 触发更新
|
||||
}
|
||||
|
||||
// ❌ 修改嵌套属性不会触发更新
|
||||
// largeData.value.nested.prop = 'new'
|
||||
|
||||
// 需要手动触发时使用 triggerRef
|
||||
import { triggerRef } from 'vue'
|
||||
largeData.value.nested.prop = 'new'
|
||||
triggerRef(largeData)
|
||||
import { triggerRef } from 'vue';
|
||||
largeData.value.nested.prop = 'new';
|
||||
triggerRef(largeData);
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -128,17 +128,17 @@ triggerRef(largeData)
|
||||
```vue
|
||||
<!-- ❌ 直接修改 props -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ user: User }>()
|
||||
props.user.name = 'New Name' // 永远不要直接修改 props!
|
||||
const props = defineProps<{ user: User }>();
|
||||
props.user.name = 'New Name'; // 永远不要直接修改 props!
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 emit 通知父组件更新 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ user: User }>()
|
||||
const props = defineProps<{ user: User }>();
|
||||
const emit = defineEmits<{
|
||||
update: [name: string]
|
||||
}>()
|
||||
const updateName = (name: string) => emit('update', name)
|
||||
update: [name: string];
|
||||
}>();
|
||||
const updateName = (name: string) => emit('update', name);
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -147,20 +147,20 @@ const updateName = (name: string) => emit('update', name)
|
||||
```vue
|
||||
<!-- ❌ defineProps 缺少类型声明 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps(['title', 'count']) // 无类型检查
|
||||
const props = defineProps(['title', 'count']); // 无类型检查
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用类型声明 + withDefaults -->
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
title: string
|
||||
count?: number
|
||||
items?: string[]
|
||||
title: string;
|
||||
count?: number;
|
||||
items?: string[];
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
count: 0,
|
||||
items: () => [] // 对象/数组默认值需要工厂函数
|
||||
})
|
||||
items: () => [], // 对象/数组默认值需要工厂函数
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -169,21 +169,21 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
```vue
|
||||
<!-- ❌ defineEmits 缺少类型 -->
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits(['update', 'delete']) // 无类型检查
|
||||
emit('update', someValue) // 参数类型不安全
|
||||
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]
|
||||
}>()
|
||||
update: [id: number, value: string];
|
||||
delete: [id: number];
|
||||
'custom-event': [payload: CustomPayload];
|
||||
}>();
|
||||
|
||||
// 现在有完整的类型检查
|
||||
emit('update', 1, 'new value') // ✅
|
||||
emit('update', 'wrong') // ❌ TypeScript 报错
|
||||
emit('update', 1, 'new value'); // ✅
|
||||
emit('update', 'wrong'); // ❌ TypeScript 报错
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -196,22 +196,25 @@ emit('update', 'wrong') // ❌ TypeScript 报错
|
||||
```vue
|
||||
<!-- Vue 3.5 之前:解构会丢失响应性 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ count: number }>()
|
||||
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: number;
|
||||
name?: string;
|
||||
}>();
|
||||
|
||||
// count 和 name 自动保持响应性!
|
||||
// 可以直接在模板和 watch 中使用
|
||||
watch(() => count, (newCount) => {
|
||||
console.log('Count changed:', newCount)
|
||||
})
|
||||
watch(
|
||||
() => count,
|
||||
(newCount) => {
|
||||
console.log('Count changed:', newCount);
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<!-- ✅ 配合默认值使用 -->
|
||||
@@ -219,12 +222,12 @@ watch(() => count, (newCount) => {
|
||||
const {
|
||||
title,
|
||||
count = 0,
|
||||
items = () => [] // 函数作为默认值(对象/数组)
|
||||
items = () => [], // 函数作为默认值(对象/数组)
|
||||
} = defineProps<{
|
||||
title: string
|
||||
count?: number
|
||||
items?: () => string[]
|
||||
}>()
|
||||
title: string;
|
||||
count?: number;
|
||||
items?: () => string[];
|
||||
}>();
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -233,23 +236,23 @@ const {
|
||||
```vue
|
||||
<!-- ❌ 传统 v-model 实现:冗长 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ modelValue: string }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
|
||||
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)
|
||||
})
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- ✅ defineModel:简洁的 v-model 实现 -->
|
||||
<script setup lang="ts">
|
||||
// 自动处理 props 和 emit
|
||||
const model = defineModel<string>()
|
||||
const model = defineModel<string>();
|
||||
|
||||
// 直接使用
|
||||
model.value = 'new value' // 自动 emit
|
||||
model.value = 'new value'; // 自动 emit
|
||||
</script>
|
||||
<template>
|
||||
<input v-model="model" />
|
||||
@@ -258,19 +261,19 @@ model.value = 'new value' // 自动 emit
|
||||
<!-- ✅ 命名 v-model -->
|
||||
<script setup lang="ts">
|
||||
// v-model:title 的实现
|
||||
const title = defineModel<string>('title')
|
||||
const title = defineModel<string>('title');
|
||||
|
||||
// 带默认值和选项
|
||||
const count = defineModel<number>('count', {
|
||||
default: 0,
|
||||
required: false
|
||||
})
|
||||
required: false,
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- ✅ 多个 v-model -->
|
||||
<script setup lang="ts">
|
||||
const firstName = defineModel<string>('firstName')
|
||||
const lastName = defineModel<string>('lastName')
|
||||
const firstName = defineModel<string>('firstName');
|
||||
const lastName = defineModel<string>('lastName');
|
||||
</script>
|
||||
<template>
|
||||
<!-- 父组件使用:<MyInput v-model:first-name="first" v-model:last-name="last" /> -->
|
||||
@@ -278,7 +281,7 @@ const lastName = defineModel<string>('lastName')
|
||||
|
||||
<!-- ✅ v-model 修饰符 -->
|
||||
<script setup lang="ts">
|
||||
const [model, modifiers] = defineModel<string>()
|
||||
const [model, modifiers] = defineModel<string>();
|
||||
|
||||
// 检查修饰符
|
||||
if (modifiers.capitalize) {
|
||||
@@ -292,7 +295,7 @@ if (modifiers.capitalize) {
|
||||
```vue
|
||||
<!-- 传统方式:ref 属性与变量同名 -->
|
||||
<script setup lang="ts">
|
||||
const inputRef = ref<HTMLInputElement | null>(null)
|
||||
const inputRef = ref<HTMLInputElement | null>(null);
|
||||
</script>
|
||||
<template>
|
||||
<input ref="inputRef" />
|
||||
@@ -300,13 +303,13 @@ const inputRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
<!-- ✅ useTemplateRef:更清晰的模板引用 -->
|
||||
<script setup lang="ts">
|
||||
import { useTemplateRef } from 'vue'
|
||||
import { useTemplateRef } from 'vue';
|
||||
|
||||
const input = useTemplateRef<HTMLInputElement>('my-input')
|
||||
const input = useTemplateRef<HTMLInputElement>('my-input');
|
||||
|
||||
onMounted(() => {
|
||||
input.value?.focus()
|
||||
})
|
||||
input.value?.focus();
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<input ref="my-input" />
|
||||
@@ -314,8 +317,8 @@ onMounted(() => {
|
||||
|
||||
<!-- ✅ 动态 ref -->
|
||||
<script setup lang="ts">
|
||||
const refKey = ref('input-a')
|
||||
const dynamicInput = useTemplateRef<HTMLInputElement>(refKey)
|
||||
const refKey = ref('input-a');
|
||||
const dynamicInput = useTemplateRef<HTMLInputElement>(refKey);
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -324,14 +327,14 @@ const dynamicInput = useTemplateRef<HTMLInputElement>(refKey)
|
||||
```vue
|
||||
<!-- ❌ 手动生成 ID 可能冲突 -->
|
||||
<script setup lang="ts">
|
||||
const id = `input-${Math.random()}` // SSR 不一致!
|
||||
const id = `input-${Math.random()}`; // SSR 不一致!
|
||||
</script>
|
||||
|
||||
<!-- ✅ useId:SSR 安全的唯一 ID -->
|
||||
<script setup lang="ts">
|
||||
import { useId } from 'vue'
|
||||
import { useId } from 'vue';
|
||||
|
||||
const id = useId() // 例如:'v-0'
|
||||
const id = useId(); // 例如:'v-0'
|
||||
</script>
|
||||
<template>
|
||||
<label :for="id">Name</label>
|
||||
@@ -340,15 +343,12 @@ const id = useId() // 例如:'v-0'
|
||||
|
||||
<!-- ✅ 表单组件中使用 -->
|
||||
<script setup lang="ts">
|
||||
const inputId = useId()
|
||||
const errorId = useId()
|
||||
const inputId = useId();
|
||||
const errorId = useId();
|
||||
</script>
|
||||
<template>
|
||||
<label :for="inputId">Email</label>
|
||||
<input
|
||||
:id="inputId"
|
||||
:aria-describedby="errorId"
|
||||
/>
|
||||
<input :id="inputId" :aria-describedby="errorId" />
|
||||
<span :id="errorId" class="error">{{ error }}</span>
|
||||
</template>
|
||||
```
|
||||
@@ -359,28 +359,28 @@ const errorId = useId()
|
||||
<!-- 传统方式:watch 第三个参数 -->
|
||||
<script setup lang="ts">
|
||||
watch(source, async (value, oldValue, onCleanup) => {
|
||||
const controller = new AbortController()
|
||||
onCleanup(() => controller.abort())
|
||||
const controller = new AbortController();
|
||||
onCleanup(() => controller.abort());
|
||||
// ...
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- ✅ onWatcherCleanup:更灵活的清理 -->
|
||||
<script setup lang="ts">
|
||||
import { onWatcherCleanup } from 'vue'
|
||||
import { onWatcherCleanup } from 'vue';
|
||||
|
||||
watch(source, async (value) => {
|
||||
const controller = new AbortController()
|
||||
onWatcherCleanup(() => controller.abort())
|
||||
const controller = new AbortController();
|
||||
onWatcherCleanup(() => controller.abort());
|
||||
|
||||
// 可以在任意位置调用,不限于回调开头
|
||||
if (someCondition) {
|
||||
const anotherResource = createResource()
|
||||
onWatcherCleanup(() => anotherResource.dispose())
|
||||
const anotherResource = createResource();
|
||||
onWatcherCleanup(() => anotherResource.dispose());
|
||||
}
|
||||
|
||||
await fetchData(value, controller.signal)
|
||||
})
|
||||
await fetchData(value, controller.signal);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -415,15 +415,15 @@ watch(source, async (value) => {
|
||||
watch(
|
||||
() => props.userId,
|
||||
async (userId) => {
|
||||
user.value = await fetchUser(userId)
|
||||
}
|
||||
)
|
||||
user.value = await fetchUser(userId);
|
||||
},
|
||||
);
|
||||
|
||||
// ✅ watchEffect:自动收集依赖,立即执行
|
||||
watchEffect(async () => {
|
||||
// 自动追踪 props.userId
|
||||
user.value = await fetchUser(props.userId)
|
||||
})
|
||||
user.value = await fetchUser(props.userId);
|
||||
});
|
||||
|
||||
// 💡 选择指南:
|
||||
// - 需要旧值?用 watch
|
||||
@@ -438,30 +438,30 @@ watchEffect(async () => {
|
||||
<!-- ❌ watch 缺少清理函数,可能内存泄漏 -->
|
||||
<script setup lang="ts">
|
||||
watch(searchQuery, async (query) => {
|
||||
const controller = new AbortController()
|
||||
const controller = new AbortController();
|
||||
const data = await fetch(`/api/search?q=${query}`, {
|
||||
signal: controller.signal
|
||||
})
|
||||
results.value = await data.json()
|
||||
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()) // 取消旧请求
|
||||
const controller = new AbortController();
|
||||
onCleanup(() => controller.abort()); // 取消旧请求
|
||||
|
||||
try {
|
||||
const data = await fetch(`/api/search?q=${query}`, {
|
||||
signal: controller.signal
|
||||
})
|
||||
results.value = await data.json()
|
||||
signal: controller.signal,
|
||||
});
|
||||
results.value = await data.json();
|
||||
} catch (e) {
|
||||
if (e.name !== 'AbortError') throw e
|
||||
if (e.name !== 'AbortError') throw e;
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -473,19 +473,19 @@ watch(searchQuery, async (query, _, onCleanup) => {
|
||||
watch(
|
||||
userId,
|
||||
async (id) => {
|
||||
user.value = await fetchUser(id)
|
||||
user.value = await fetchUser(id);
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// ✅ deep:深度监听(性能开销大,谨慎使用)
|
||||
watch(
|
||||
state,
|
||||
(newState) => {
|
||||
console.log('State changed deeply')
|
||||
console.log('State changed deeply');
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// ✅ flush: 'post':DOM 更新后执行
|
||||
watch(
|
||||
@@ -494,17 +494,17 @@ watch(
|
||||
// 可以安全访问更新后的 DOM
|
||||
// nextTick 不再需要
|
||||
},
|
||||
{ flush: 'post' }
|
||||
)
|
||||
{ flush: 'post' },
|
||||
);
|
||||
|
||||
// ✅ once: true (Vue 3.4+):只执行一次
|
||||
watch(
|
||||
source,
|
||||
(value) => {
|
||||
console.log('只会执行一次:', value)
|
||||
console.log('只会执行一次:', value);
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
{ once: true },
|
||||
);
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -513,20 +513,17 @@ watch(
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// ✅ 监听多个 ref
|
||||
watch(
|
||||
[firstName, lastName],
|
||||
([newFirst, newLast], [oldFirst, oldLast]) => {
|
||||
console.log(`Name changed from ${oldFirst} ${oldLast} to ${newFirst} ${newLast}`)
|
||||
}
|
||||
)
|
||||
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}`)
|
||||
}
|
||||
)
|
||||
console.log(`count: ${count}, name: ${name}`);
|
||||
},
|
||||
);
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -571,9 +568,7 @@ watch(
|
||||
|
||||
<!-- ✅ 使用 computed 过滤 -->
|
||||
<script setup lang="ts">
|
||||
const activeUsers = computed(() =>
|
||||
users.value.filter(user => user.active)
|
||||
)
|
||||
const activeUsers = computed(() => users.value.filter((user) => user.active));
|
||||
</script>
|
||||
<template>
|
||||
<li v-for="user in activeUsers" :key="user.id">
|
||||
@@ -596,7 +591,12 @@ const activeUsers = computed(() =>
|
||||
```vue
|
||||
<!-- ❌ 内联复杂逻辑 -->
|
||||
<template>
|
||||
<button @click="items = items.filter(i => i.id !== item.id); count--">
|
||||
<button
|
||||
@click="
|
||||
items = items.filter((i) => i.id !== item.id);
|
||||
count--;
|
||||
"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</template>
|
||||
@@ -604,9 +604,9 @@ const activeUsers = computed(() =>
|
||||
<!-- ✅ 使用方法 -->
|
||||
<script setup lang="ts">
|
||||
const deleteItem = (id: number) => {
|
||||
items.value = items.value.filter(i => i.id !== id)
|
||||
count.value--
|
||||
}
|
||||
items.value = items.value.filter((i) => i.id !== id);
|
||||
count.value--;
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<button @click="deleteItem(item.id)">Delete</button>
|
||||
@@ -637,27 +637,27 @@ const deleteItem = (id: number) => {
|
||||
```typescript
|
||||
// ✅ 好的 composable 设计
|
||||
export function useCounter(initialValue = 0) {
|
||||
const count = ref(initialValue)
|
||||
const count = ref(initialValue);
|
||||
|
||||
const increment = () => count.value++
|
||||
const decrement = () => count.value--
|
||||
const reset = () => count.value = initialValue
|
||||
const increment = () => count.value++;
|
||||
const decrement = () => count.value--;
|
||||
const reset = () => (count.value = initialValue);
|
||||
|
||||
// 返回响应式引用和方法
|
||||
return {
|
||||
count: readonly(count), // 只读防止外部修改
|
||||
count: readonly(count), // 只读防止外部修改
|
||||
increment,
|
||||
decrement,
|
||||
reset
|
||||
}
|
||||
reset,
|
||||
};
|
||||
}
|
||||
|
||||
// ❌ 不要返回 .value
|
||||
export function useBadCounter() {
|
||||
const count = ref(0)
|
||||
const count = ref(0);
|
||||
return {
|
||||
count: count.value // ❌ 丢失响应性!
|
||||
}
|
||||
count: count.value, // ❌ 丢失响应性!
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
@@ -666,21 +666,21 @@ export function useBadCounter() {
|
||||
```vue
|
||||
<!-- ❌ 传递 props 到 composable 丢失响应性 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ userId: string }>()
|
||||
const { user } = useUser(props.userId) // 丢失响应性!
|
||||
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) // 保持响应性
|
||||
const props = defineProps<{ userId: string }>();
|
||||
const userIdRef = toRef(props, 'userId');
|
||||
const { user } = useUser(userIdRef); // 保持响应性
|
||||
// 或使用 computed
|
||||
const { user } = useUser(computed(() => props.userId))
|
||||
const { user } = useUser(computed(() => props.userId));
|
||||
|
||||
// ✅ Vue 3.5+:直接解构使用
|
||||
const { userId } = defineProps<{ userId: string }>()
|
||||
const { user } = useUser(() => userId) // getter 函数
|
||||
const { userId } = defineProps<{ userId: string }>();
|
||||
const { user } = useUser(() => userId); // getter 函数
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -689,43 +689,43 @@ const { user } = useUser(() => userId) // getter 函数
|
||||
```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 data = ref<T | null>(null);
|
||||
const error = ref<Error | null>(null);
|
||||
const loading = ref(false);
|
||||
|
||||
const execute = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await fetch(toValue(url))
|
||||
const response = await fetch(toValue(url));
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
data.value = await response.json()
|
||||
data.value = await response.json();
|
||||
} catch (e) {
|
||||
error.value = e as Error
|
||||
error.value = e as Error;
|
||||
} finally {
|
||||
loading.value = false
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 响应式 URL 时自动重新获取
|
||||
watchEffect(() => {
|
||||
toValue(url) // 追踪依赖
|
||||
execute()
|
||||
})
|
||||
toValue(url); // 追踪依赖
|
||||
execute();
|
||||
});
|
||||
|
||||
return {
|
||||
data: readonly(data),
|
||||
error: readonly(error),
|
||||
loading: readonly(loading),
|
||||
refetch: execute
|
||||
}
|
||||
refetch: execute,
|
||||
};
|
||||
}
|
||||
|
||||
// 使用
|
||||
const { data, loading, error, refetch } = useFetch<User[]>('/api/users')
|
||||
const { data, loading, error, refetch } = useFetch<User[]>('/api/users');
|
||||
```
|
||||
|
||||
### 生命周期与清理
|
||||
@@ -735,34 +735,40 @@ const { data, loading, error, refetch } = useFetch<User[]>('/api/users')
|
||||
export function useEventListener(
|
||||
target: MaybeRefOrGetter<EventTarget>,
|
||||
event: string,
|
||||
handler: EventListener
|
||||
handler: EventListener,
|
||||
) {
|
||||
// 组件挂载后添加
|
||||
onMounted(() => {
|
||||
toValue(target).addEventListener(event, handler)
|
||||
})
|
||||
toValue(target).addEventListener(event, handler);
|
||||
});
|
||||
|
||||
// 组件卸载时移除
|
||||
onUnmounted(() => {
|
||||
toValue(target).removeEventListener(event, handler)
|
||||
})
|
||||
toValue(target).removeEventListener(event, handler);
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 使用 effectScope 管理副作用
|
||||
export function useFeature() {
|
||||
const scope = effectScope()
|
||||
const scope = effectScope();
|
||||
|
||||
scope.run(() => {
|
||||
// 所有响应式效果都在这个 scope 内
|
||||
const state = ref(0)
|
||||
watch(state, () => { /* ... */ })
|
||||
watchEffect(() => { /* ... */ })
|
||||
})
|
||||
const state = ref(0);
|
||||
watch(state, () => {
|
||||
/* ... */
|
||||
});
|
||||
watchEffect(() => {
|
||||
/* ... */
|
||||
});
|
||||
});
|
||||
|
||||
// 清理所有效果
|
||||
onUnmounted(() => scope.stop())
|
||||
onUnmounted(() => scope.stop());
|
||||
|
||||
return { /* ... */ }
|
||||
return {
|
||||
/* ... */
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
@@ -783,11 +789,7 @@ export function useFeature() {
|
||||
|
||||
<!-- ✅ 配合 v-for 使用 -->
|
||||
<template>
|
||||
<div
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
v-memo="[item.name, item.status]"
|
||||
>
|
||||
<div v-for="item in list" :key="item.id" v-memo="[item.name, item.status]">
|
||||
<!-- 只有 name 或 status 变化时重新渲染 -->
|
||||
</div>
|
||||
</template>
|
||||
@@ -797,21 +799,19 @@ export function useFeature() {
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
// ✅ 懒加载组件
|
||||
const HeavyChart = defineAsyncComponent(() =>
|
||||
import('./components/HeavyChart.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 // 超时时间
|
||||
})
|
||||
delay: 200, // 延迟显示 loading(避免闪烁)
|
||||
timeout: 3000, // 超时时间
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -839,13 +839,13 @@ const AsyncModal = defineAsyncComponent({
|
||||
// KeepAlive 组件的生命周期钩子
|
||||
onActivated(() => {
|
||||
// 组件被激活时(从缓存恢复)
|
||||
refreshData()
|
||||
})
|
||||
refreshData();
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
// 组件被停用时(进入缓存)
|
||||
pauseTimers()
|
||||
})
|
||||
pauseTimers();
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -854,12 +854,9 @@ onDeactivated(() => {
|
||||
```vue
|
||||
<!-- ✅ 大型列表使用虚拟滚动 -->
|
||||
<script setup lang="ts">
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
import { useVirtualList } from '@vueuse/core';
|
||||
|
||||
const { list, containerProps, wrapperProps } = useVirtualList(
|
||||
items,
|
||||
{ itemHeight: 50 }
|
||||
)
|
||||
const { list, containerProps, wrapperProps } = useVirtualList(items, { itemHeight: 50 });
|
||||
</script>
|
||||
<template>
|
||||
<div v-bind="containerProps" style="height: 400px; overflow: auto">
|
||||
@@ -877,6 +874,7 @@ const { list, containerProps, wrapperProps } = useVirtualList(
|
||||
## Review Checklist
|
||||
|
||||
### 响应性系统
|
||||
|
||||
- [ ] ref 用于基本类型,reactive 用于对象(或统一用 ref)
|
||||
- [ ] 没有解构 reactive 对象(或使用了 toRefs)
|
||||
- [ ] props 传递给 composable 时保持了响应性
|
||||
@@ -884,6 +882,7 @@ const { list, containerProps, wrapperProps } = useVirtualList(
|
||||
- [ ] computed 中没有副作用
|
||||
|
||||
### Props & Emits
|
||||
|
||||
- [ ] defineProps 使用 TypeScript 类型声明
|
||||
- [ ] 复杂默认值使用 withDefaults + 工厂函数
|
||||
- [ ] defineEmits 有完整的类型定义
|
||||
@@ -891,12 +890,14 @@ const { list, containerProps, wrapperProps } = useVirtualList(
|
||||
- [ ] 考虑使用 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
|
||||
@@ -904,12 +905,14 @@ const { list, containerProps, wrapperProps } = useVirtualList(
|
||||
- [ ] 考虑 once: true 用于一次性监听
|
||||
|
||||
### 模板
|
||||
|
||||
- [ ] v-for 使用唯一且稳定的 key
|
||||
- [ ] v-if 和 v-for 没有在同一元素上
|
||||
- [ ] 事件处理使用方法而非内联复杂逻辑
|
||||
- [ ] 大型列表使用虚拟滚动
|
||||
|
||||
### Composables
|
||||
|
||||
- [ ] 相关逻辑提取到 composables
|
||||
- [ ] composables 返回响应式引用(不是 .value)
|
||||
- [ ] 纯函数不要包装成 composable
|
||||
@@ -917,6 +920,7 @@ const { list, containerProps, wrapperProps } = useVirtualList(
|
||||
- [ ] 使用 effectScope 管理复杂副作用
|
||||
|
||||
### 性能
|
||||
|
||||
- [ ] 大型组件拆分为小组件
|
||||
- [ ] 使用 defineAsyncComponent 懒加载
|
||||
- [ ] 避免不必要的响应式转换
|
||||
|
||||
Reference in New Issue
Block a user