mirror of
https://github.com/awesome-skills/code-review-skill.git
synced 2026-03-22 10:28:31 +08:00
refactor: 模块化 skill 结构,支持按需加载
- 将 SKILL.md 从 1774 行精简到 ~180 行 - 新增 references/react.md:React 19、RSC、TanStack Query v5 - 新增 references/vue.md:Vue 3 Composition API - 新增 references/rust.md:所有权、unsafe、异步 - 新增 references/typescript.md:类型安全、async/await - 新增 references/python.md:常见陷阱 - 更新 README.md:说明按需加载机制 优化效果: - 初始加载:~40K tokens → ~4K tokens - 支持 Progressive Disclosure 按需加载
This commit is contained in:
105
README.md
105
README.md
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## English
|
## English
|
||||||
|
|
||||||
> A comprehensive code review skill for Claude Code, covering React 19, Vue 3, Rust, TypeScript, and more.
|
> A modular code review skill for Claude Code, covering React 19, Vue 3, Rust, TypeScript, Python, and more.
|
||||||
|
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
@@ -14,10 +14,11 @@
|
|||||||
|
|
||||||
This is a Claude Code skill designed to help developers conduct effective code reviews. It provides:
|
This is a Claude Code skill designed to help developers conduct effective code reviews. It provides:
|
||||||
|
|
||||||
- **Language-specific patterns** for React 19, Vue 3, Rust, TypeScript/JavaScript, Python, SQL
|
- **Language-specific patterns** for React 19, Vue 3, Rust, TypeScript/JavaScript, Python
|
||||||
- **Modern framework support** including React Server Components, TanStack Query v5, Suspense & Streaming
|
- **Modern framework support** including React Server Components, TanStack Query v5, Suspense & Streaming
|
||||||
- **Comprehensive checklists** for security, performance, and code quality
|
- **Comprehensive checklists** for security, performance, and code quality
|
||||||
- **Best practices** for giving constructive feedback
|
- **Best practices** for giving constructive feedback
|
||||||
|
- **Modular structure** for on-demand loading (reduces context usage)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
@@ -33,29 +34,36 @@ This is a Claude Code skill designed to help developers conduct effective code r
|
|||||||
|
|
||||||
#### Content Statistics
|
#### Content Statistics
|
||||||
|
|
||||||
- **SKILL.md**: ~1,800 lines of review guidelines and code examples
|
| File | Lines | Description |
|
||||||
- **common-bugs-checklist.md**: ~1,200 lines of bug patterns and checklists
|
|------|-------|-------------|
|
||||||
- Additional reference files for security review, PR templates, and more
|
| **SKILL.md** | ~180 | Core principles + index (loads on skill activation) |
|
||||||
|
| **references/react.md** | ~650 | React/Next.js patterns (on-demand) |
|
||||||
|
| **references/vue.md** | ~200 | Vue 3 patterns (on-demand) |
|
||||||
|
| **references/rust.md** | ~200 | Rust patterns (on-demand) |
|
||||||
|
| **references/typescript.md** | ~100 | TypeScript/JS patterns (on-demand) |
|
||||||
|
| **references/python.md** | ~60 | Python patterns (on-demand) |
|
||||||
|
|
||||||
|
**Total: ~2,000+ lines** of review guidelines and code examples, loaded on-demand per language.
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
#### For Claude Code Users
|
#### For Claude Code Users
|
||||||
|
|
||||||
Copy the skill to your Claude Code plugins directory:
|
Copy the skill to your Claude Code skills directory:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone the repository
|
# Clone the repository
|
||||||
git clone https://github.com/tt-a1i/ai-code-review-guide.git
|
git clone https://github.com/tt-a1i/ai-code-review-guide.git
|
||||||
|
|
||||||
# Copy to Claude Code skills directory
|
# Copy to Claude Code skills directory
|
||||||
cp -r ai-code-review-guide ~/.claude/commands/code-review-excellence
|
cp -r ai-code-review-guide ~/.claude/skills/code-review-excellence
|
||||||
```
|
```
|
||||||
|
|
||||||
Or add to your existing Claude Code plugin:
|
Or add to your existing Claude Code plugin:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp -r ai-code-review-guide/SKILL.md ~/.claude/plugins/your-plugin/skills/code-review/
|
# Copy the entire directory structure
|
||||||
cp -r ai-code-review-guide/references ~/.claude/plugins/your-plugin/skills/code-review/
|
cp -r ai-code-review-guide ~/.claude/plugins/your-plugin/skills/code-review/
|
||||||
```
|
```
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
@@ -72,11 +80,16 @@ Or reference it in your custom commands.
|
|||||||
|
|
||||||
```
|
```
|
||||||
ai-code-review-guide/
|
ai-code-review-guide/
|
||||||
├── SKILL.md # Main skill definition
|
├── SKILL.md # Core skill (loads immediately)
|
||||||
├── README.md # This file
|
├── README.md # This file
|
||||||
├── LICENSE # MIT License
|
├── LICENSE # MIT License
|
||||||
├── CONTRIBUTING.md # Contribution guidelines
|
├── CONTRIBUTING.md # Contribution guidelines
|
||||||
├── references/
|
├── references/
|
||||||
|
│ ├── react.md # React/Next.js patterns (on-demand)
|
||||||
|
│ ├── vue.md # Vue 3 patterns (on-demand)
|
||||||
|
│ ├── rust.md # Rust patterns (on-demand)
|
||||||
|
│ ├── typescript.md # TypeScript/JS patterns (on-demand)
|
||||||
|
│ ├── python.md # Python patterns (on-demand)
|
||||||
│ ├── common-bugs-checklist.md # Language-specific bug patterns
|
│ ├── common-bugs-checklist.md # Language-specific bug patterns
|
||||||
│ ├── security-review-guide.md # Security review checklist
|
│ ├── security-review-guide.md # Security review checklist
|
||||||
│ └── code-review-best-practices.md
|
│ └── code-review-best-practices.md
|
||||||
@@ -87,6 +100,16 @@ ai-code-review-guide/
|
|||||||
└── pr-analyzer.py # PR complexity analyzer
|
└── pr-analyzer.py # PR complexity analyzer
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### On-Demand Loading
|
||||||
|
|
||||||
|
This skill uses **Progressive Disclosure** to minimize context usage:
|
||||||
|
|
||||||
|
1. **SKILL.md** (~180 lines) loads when the skill is activated
|
||||||
|
2. **Language-specific files** load only when reviewing that language
|
||||||
|
3. **Reference files** load only when explicitly needed
|
||||||
|
|
||||||
|
This means reviewing a React PR only loads SKILL.md + react.md, not Vue/Rust/Python content.
|
||||||
|
|
||||||
### Key Topics Covered
|
### Key Topics Covered
|
||||||
|
|
||||||
#### React 19
|
#### React 19
|
||||||
@@ -143,7 +166,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|||||||
|
|
||||||
## 中文
|
## 中文
|
||||||
|
|
||||||
> 一个全面的 Claude Code 代码审查技能,覆盖 React 19、Vue 3、Rust、TypeScript 等。
|
> 一个模块化的 Claude Code 代码审查技能,覆盖 React 19、Vue 3、Rust、TypeScript、Python 等。
|
||||||
|
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
@@ -151,10 +174,11 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|||||||
|
|
||||||
这是一个为 Claude Code 设计的代码审查技能,旨在帮助开发者进行高效的代码审查。它提供:
|
这是一个为 Claude Code 设计的代码审查技能,旨在帮助开发者进行高效的代码审查。它提供:
|
||||||
|
|
||||||
- **语言特定模式**:覆盖 React 19、Vue 3、Rust、TypeScript/JavaScript、Python、SQL
|
- **语言特定模式**:覆盖 React 19、Vue 3、Rust、TypeScript/JavaScript、Python
|
||||||
- **现代框架支持**:包括 React Server Components、TanStack Query v5、Suspense & Streaming
|
- **现代框架支持**:包括 React Server Components、TanStack Query v5、Suspense & Streaming
|
||||||
- **全面的检查清单**:安全、性能和代码质量检查
|
- **全面的检查清单**:安全、性能和代码质量检查
|
||||||
- **最佳实践**:如何提供建设性的反馈
|
- **最佳实践**:如何提供建设性的反馈
|
||||||
|
- **模块化结构**:按需加载,减少上下文占用
|
||||||
|
|
||||||
### 特性
|
### 特性
|
||||||
|
|
||||||
@@ -170,29 +194,36 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|||||||
|
|
||||||
#### 内容统计
|
#### 内容统计
|
||||||
|
|
||||||
- **SKILL.md**:约 1,800 行审查指南和代码示例
|
| 文件 | 行数 | 描述 |
|
||||||
- **common-bugs-checklist.md**:约 1,200 行错误模式和检查清单
|
|------|------|------|
|
||||||
- 额外的安全审查、PR 模板等参考文件
|
| **SKILL.md** | ~180 | 核心原则 + 索引(技能激活时加载)|
|
||||||
|
| **references/react.md** | ~650 | React/Next.js 模式(按需加载)|
|
||||||
|
| **references/vue.md** | ~200 | Vue 3 模式(按需加载)|
|
||||||
|
| **references/rust.md** | ~200 | Rust 模式(按需加载)|
|
||||||
|
| **references/typescript.md** | ~100 | TypeScript/JS 模式(按需加载)|
|
||||||
|
| **references/python.md** | ~60 | Python 模式(按需加载)|
|
||||||
|
|
||||||
|
**总计:2,000+ 行**审查指南和代码示例,按语言按需加载。
|
||||||
|
|
||||||
### 安装
|
### 安装
|
||||||
|
|
||||||
#### Claude Code 用户
|
#### Claude Code 用户
|
||||||
|
|
||||||
将技能复制到 Claude Code 插件目录:
|
将技能复制到 Claude Code skills 目录:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 克隆仓库
|
# 克隆仓库
|
||||||
git clone https://github.com/tt-a1i/ai-code-review-guide.git
|
git clone https://github.com/tt-a1i/ai-code-review-guide.git
|
||||||
|
|
||||||
# 复制到 Claude Code skills 目录
|
# 复制到 Claude Code skills 目录
|
||||||
cp -r ai-code-review-guide ~/.claude/commands/code-review-excellence
|
cp -r ai-code-review-guide ~/.claude/skills/code-review-excellence
|
||||||
```
|
```
|
||||||
|
|
||||||
或添加到现有的 Claude Code 插件:
|
或添加到现有的 Claude Code 插件:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp -r ai-code-review-guide/SKILL.md ~/.claude/plugins/your-plugin/skills/code-review/
|
# 复制整个目录结构
|
||||||
cp -r ai-code-review-guide/references ~/.claude/plugins/your-plugin/skills/code-review/
|
cp -r ai-code-review-guide ~/.claude/plugins/your-plugin/skills/code-review/
|
||||||
```
|
```
|
||||||
|
|
||||||
### 使用方法
|
### 使用方法
|
||||||
@@ -209,11 +240,16 @@ cp -r ai-code-review-guide/references ~/.claude/plugins/your-plugin/skills/code-
|
|||||||
|
|
||||||
```
|
```
|
||||||
ai-code-review-guide/
|
ai-code-review-guide/
|
||||||
├── SKILL.md # 主技能定义
|
├── SKILL.md # 核心技能(立即加载)
|
||||||
├── README.md # 本文件
|
├── README.md # 本文件
|
||||||
├── LICENSE # MIT 许可证
|
├── LICENSE # MIT 许可证
|
||||||
├── CONTRIBUTING.md # 贡献指南
|
├── CONTRIBUTING.md # 贡献指南
|
||||||
├── references/
|
├── references/
|
||||||
|
│ ├── react.md # React/Next.js 模式(按需加载)
|
||||||
|
│ ├── vue.md # Vue 3 模式(按需加载)
|
||||||
|
│ ├── rust.md # Rust 模式(按需加载)
|
||||||
|
│ ├── typescript.md # TypeScript/JS 模式(按需加载)
|
||||||
|
│ ├── python.md # Python 模式(按需加载)
|
||||||
│ ├── common-bugs-checklist.md # 语言特定的错误模式
|
│ ├── common-bugs-checklist.md # 语言特定的错误模式
|
||||||
│ ├── security-review-guide.md # 安全审查清单
|
│ ├── security-review-guide.md # 安全审查清单
|
||||||
│ └── code-review-best-practices.md
|
│ └── code-review-best-practices.md
|
||||||
@@ -224,6 +260,16 @@ ai-code-review-guide/
|
|||||||
└── pr-analyzer.py # PR 复杂度分析器
|
└── pr-analyzer.py # PR 复杂度分析器
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 按需加载机制
|
||||||
|
|
||||||
|
此技能使用 **Progressive Disclosure(渐进式披露)** 来最小化上下文占用:
|
||||||
|
|
||||||
|
1. **SKILL.md**(~180 行)在技能激活时加载
|
||||||
|
2. **语言特定文件** 仅在审查该语言时加载
|
||||||
|
3. **参考文件** 仅在明确需要时加载
|
||||||
|
|
||||||
|
这意味着审查 React PR 时只加载 SKILL.md + react.md,不会加载 Vue/Rust/Python 内容。
|
||||||
|
|
||||||
### 核心内容
|
### 核心内容
|
||||||
|
|
||||||
#### React 19
|
#### React 19
|
||||||
@@ -269,13 +315,6 @@ ai-code-review-guide/
|
|||||||
|
|
||||||
本项目采用 MIT 许可证 - 详见 [LICENSE](LICENSE) 文件。
|
本项目采用 MIT 许可证 - 详见 [LICENSE](LICENSE) 文件。
|
||||||
|
|
||||||
### 致谢
|
|
||||||
|
|
||||||
- 灵感来自软件工程社区的代码审查最佳实践
|
|
||||||
- React 19 特性来自 React 官方文档
|
|
||||||
- TanStack Query 文档
|
|
||||||
- TkDodo 的 React Query 最佳实践博客
|
|
||||||
|
|
||||||
### 参考资料
|
### 参考资料
|
||||||
|
|
||||||
- [React v19 官方文档](https://react.dev/blog/2024/12/05/react-19)
|
- [React v19 官方文档](https://react.dev/blog/2024/12/05/react-19)
|
||||||
|
|||||||
77
references/python.md
Normal file
77
references/python.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Python Code Review Guide
|
||||||
|
|
||||||
|
> Python 代码审查指南,覆盖常见陷阱和最佳实践。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见陷阱
|
||||||
|
|
||||||
|
### 可变默认参数
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ❌ Mutable default arguments
|
||||||
|
def add_item(item, items=[]): # Bug! Shared across calls
|
||||||
|
items.append(item)
|
||||||
|
return items
|
||||||
|
|
||||||
|
# ✅ Use None as default
|
||||||
|
def add_item(item, items=None):
|
||||||
|
if items is None:
|
||||||
|
items = []
|
||||||
|
items.append(item)
|
||||||
|
return items
|
||||||
|
```
|
||||||
|
|
||||||
|
### 异常捕获过宽
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ❌ Catching too broad
|
||||||
|
try:
|
||||||
|
result = risky_operation()
|
||||||
|
except: # Catches everything, even KeyboardInterrupt!
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ✅ Catch specific exceptions
|
||||||
|
try:
|
||||||
|
result = risky_operation()
|
||||||
|
except ValueError as e:
|
||||||
|
logger.error(f"Invalid value: {e}")
|
||||||
|
raise
|
||||||
|
```
|
||||||
|
|
||||||
|
### 可变类属性
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ❌ Using mutable class attributes
|
||||||
|
class User:
|
||||||
|
permissions = [] # Shared across all instances!
|
||||||
|
|
||||||
|
# ✅ Initialize in __init__
|
||||||
|
class User:
|
||||||
|
def __init__(self):
|
||||||
|
self.permissions = []
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Python Review Checklist
|
||||||
|
|
||||||
|
### 数据结构
|
||||||
|
- [ ] 没有使用可变默认参数(list、dict、set)
|
||||||
|
- [ ] 类属性不是可变对象
|
||||||
|
- [ ] 理解浅拷贝和深拷贝的区别
|
||||||
|
|
||||||
|
### 异常处理
|
||||||
|
- [ ] 捕获特定异常类型,不使用裸 `except:`
|
||||||
|
- [ ] 异常信息有意义,便于调试
|
||||||
|
- [ ] 必要时重新抛出异常(`raise`)
|
||||||
|
|
||||||
|
### 性能
|
||||||
|
- [ ] 大数据集使用生成器而非列表
|
||||||
|
- [ ] 避免循环中重复创建对象
|
||||||
|
- [ ] 使用 `collections` 模块的高效数据结构
|
||||||
|
|
||||||
|
### 代码风格
|
||||||
|
- [ ] 遵循 PEP 8 风格指南
|
||||||
|
- [ ] 使用类型注解(type hints)
|
||||||
|
- [ ] 函数和类有 docstring
|
||||||
799
references/react.md
Normal file
799
references/react.md
Normal file
@@ -0,0 +1,799 @@
|
|||||||
|
# React Code Review Guide
|
||||||
|
|
||||||
|
React 审查重点:Hooks 规则、性能优化的适度性、组件设计、以及现代 React 19/RSC 模式。
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
- [基础 Hooks 规则](#基础-hooks-规则)
|
||||||
|
- [useEffect 模式](#useeffect-模式)
|
||||||
|
- [useMemo / useCallback](#usememo--usecallback)
|
||||||
|
- [组件设计](#组件设计)
|
||||||
|
- [Error Boundaries & Suspense](#error-boundaries--suspense)
|
||||||
|
- [Server Components (RSC)](#server-components-rsc)
|
||||||
|
- [React 19 Actions & Forms](#react-19-actions--forms)
|
||||||
|
- [Suspense & Streaming SSR](#suspense--streaming-ssr)
|
||||||
|
- [TanStack Query v5](#tanstack-query-v5)
|
||||||
|
- [Review Checklists](#review-checklists)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 基础 Hooks 规则
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ 条件调用 Hooks — 违反 Hooks 规则
|
||||||
|
function BadComponent({ isLoggedIn }) {
|
||||||
|
if (isLoggedIn) {
|
||||||
|
const [user, setUser] = useState(null); // Error!
|
||||||
|
}
|
||||||
|
return <div>...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Hooks 必须在组件顶层调用
|
||||||
|
function GoodComponent({ isLoggedIn }) {
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
if (!isLoggedIn) return <LoginPrompt />;
|
||||||
|
return <div>{user?.name}</div>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## useEffect 模式
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ 依赖数组缺失或不完整
|
||||||
|
function BadEffect({ userId }) {
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUser(userId).then(setUser);
|
||||||
|
}, []); // 缺少 userId 依赖!
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 完整的依赖数组
|
||||||
|
function GoodEffect({ userId }) {
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
fetchUser(userId).then(data => {
|
||||||
|
if (!cancelled) setUser(data);
|
||||||
|
});
|
||||||
|
return () => { cancelled = true; }; // 清理函数
|
||||||
|
}, [userId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ useEffect 用于派生状态(反模式)
|
||||||
|
function BadDerived({ items }) {
|
||||||
|
const [filteredItems, setFilteredItems] = useState([]);
|
||||||
|
useEffect(() => {
|
||||||
|
setFilteredItems(items.filter(i => i.active));
|
||||||
|
}, [items]); // 不必要的 effect + 额外渲染
|
||||||
|
return <List items={filteredItems} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 直接在渲染时计算,或用 useMemo
|
||||||
|
function GoodDerived({ items }) {
|
||||||
|
const filteredItems = useMemo(
|
||||||
|
() => items.filter(i => i.active),
|
||||||
|
[items]
|
||||||
|
);
|
||||||
|
return <List items={filteredItems} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ useEffect 用于事件响应
|
||||||
|
function BadEventEffect() {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
useEffect(() => {
|
||||||
|
if (query) {
|
||||||
|
analytics.track('search', { query }); // 应该在事件处理器中
|
||||||
|
}
|
||||||
|
}, [query]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 在事件处理器中执行副作用
|
||||||
|
function GoodEvent() {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
const handleSearch = (q: string) => {
|
||||||
|
setQuery(q);
|
||||||
|
analytics.track('search', { query: q });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## useMemo / useCallback
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ 过度优化 — 常量不需要 useMemo
|
||||||
|
function OverOptimized() {
|
||||||
|
const config = useMemo(() => ({ timeout: 5000 }), []); // 无意义
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
console.log('clicked');
|
||||||
|
}, []); // 如果不传给 memo 组件,无意义
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 只在需要时优化
|
||||||
|
function ProperlyOptimized() {
|
||||||
|
const config = { timeout: 5000 }; // 简单对象直接定义
|
||||||
|
const handleClick = () => console.log('clicked');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ useCallback 依赖总是变化
|
||||||
|
function BadCallback({ data }) {
|
||||||
|
// data 每次渲染都是新对象,useCallback 无效
|
||||||
|
const process = useCallback(() => {
|
||||||
|
return data.map(transform);
|
||||||
|
}, [data]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ useMemo + useCallback 配合 React.memo 使用
|
||||||
|
const MemoizedChild = React.memo(function Child({ onClick, items }) {
|
||||||
|
return <div onClick={onClick}>{items.length}</div>;
|
||||||
|
});
|
||||||
|
|
||||||
|
function Parent({ rawItems }) {
|
||||||
|
const items = useMemo(() => processItems(rawItems), [rawItems]);
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
console.log(items.length);
|
||||||
|
}, [items]);
|
||||||
|
return <MemoizedChild onClick={handleClick} items={items} />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 组件设计
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ 在组件内定义组件 — 每次渲染都创建新组件
|
||||||
|
function BadParent() {
|
||||||
|
function ChildComponent() { // 每次渲染都是新函数!
|
||||||
|
return <div>child</div>;
|
||||||
|
}
|
||||||
|
return <ChildComponent />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 组件定义在外部
|
||||||
|
function ChildComponent() {
|
||||||
|
return <div>child</div>;
|
||||||
|
}
|
||||||
|
function GoodParent() {
|
||||||
|
return <ChildComponent />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Props 总是新对象引用
|
||||||
|
function BadProps() {
|
||||||
|
return (
|
||||||
|
<MemoizedComponent
|
||||||
|
style={{ color: 'red' }} // 每次渲染新对象
|
||||||
|
onClick={() => {}} // 每次渲染新函数
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 稳定的引用
|
||||||
|
const style = { color: 'red' };
|
||||||
|
function GoodProps() {
|
||||||
|
const handleClick = useCallback(() => {}, []);
|
||||||
|
return <MemoizedComponent style={style} onClick={handleClick} />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Boundaries & Suspense
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ 没有错误边界
|
||||||
|
function BadApp() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<Loading />}>
|
||||||
|
<DataComponent /> {/* 错误会导致整个应用崩溃 */}
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Error Boundary 包裹 Suspense
|
||||||
|
function GoodApp() {
|
||||||
|
return (
|
||||||
|
<ErrorBoundary fallback={<ErrorUI />}>
|
||||||
|
<Suspense fallback={<Loading />}>
|
||||||
|
<DataComponent />
|
||||||
|
</Suspense>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Server Components (RSC)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ 在 Server Component 中使用客户端特性
|
||||||
|
// app/page.tsx (Server Component by default)
|
||||||
|
function BadServerComponent() {
|
||||||
|
const [count, setCount] = useState(0); // Error! No hooks in RSC
|
||||||
|
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 交互逻辑提取到 Client Component
|
||||||
|
// app/counter.tsx
|
||||||
|
'use client';
|
||||||
|
function Counter() {
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// app/page.tsx (Server Component)
|
||||||
|
function GoodServerComponent() {
|
||||||
|
const data = await fetchData(); // 可以直接 await
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>{data.title}</h1>
|
||||||
|
<Counter /> {/* 客户端组件 */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ 'use client' 放置不当 — 整个树都变成客户端
|
||||||
|
// layout.tsx
|
||||||
|
'use client'; // 这会让所有子组件都成为客户端组件
|
||||||
|
export default function Layout({ children }) { ... }
|
||||||
|
|
||||||
|
// ✅ 只在需要交互的组件使用 'use client'
|
||||||
|
// 将客户端逻辑隔离到叶子组件
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## React 19 Actions & Forms
|
||||||
|
|
||||||
|
React 19 引入了 Actions 系统和新的表单处理 Hooks,简化异步操作和乐观更新。
|
||||||
|
|
||||||
|
### useActionState
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ 传统方式:多个状态变量
|
||||||
|
function OldForm() {
|
||||||
|
const [isPending, setIsPending] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [data, setData] = useState(null);
|
||||||
|
|
||||||
|
const handleSubmit = async (formData: FormData) => {
|
||||||
|
setIsPending(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const result = await submitForm(formData);
|
||||||
|
setData(result);
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message);
|
||||||
|
} finally {
|
||||||
|
setIsPending(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ React 19: useActionState 统一管理
|
||||||
|
import { useActionState } from 'react';
|
||||||
|
|
||||||
|
function NewForm() {
|
||||||
|
const [state, formAction, isPending] = useActionState(
|
||||||
|
async (prevState, formData: FormData) => {
|
||||||
|
try {
|
||||||
|
const result = await submitForm(formData);
|
||||||
|
return { success: true, data: result };
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, error: e.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ success: false, data: null, error: null }
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form action={formAction}>
|
||||||
|
<input name="email" />
|
||||||
|
<button disabled={isPending}>
|
||||||
|
{isPending ? 'Submitting...' : 'Submit'}
|
||||||
|
</button>
|
||||||
|
{state.error && <p className="error">{state.error}</p>}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### useFormStatus
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ Props 透传表单状态
|
||||||
|
function BadSubmitButton({ isSubmitting }) {
|
||||||
|
return <button disabled={isSubmitting}>Submit</button>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ useFormStatus 访问父 <form> 状态(无需 props)
|
||||||
|
import { useFormStatus } from 'react-dom';
|
||||||
|
|
||||||
|
function SubmitButton() {
|
||||||
|
const { pending, data, method, action } = useFormStatus();
|
||||||
|
// 注意:必须在 <form> 内部的子组件中使用
|
||||||
|
return (
|
||||||
|
<button disabled={pending}>
|
||||||
|
{pending ? 'Submitting...' : 'Submit'}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ useFormStatus 在 form 同级组件中调用——不工作
|
||||||
|
function BadForm() {
|
||||||
|
const { pending } = useFormStatus(); // 这里无法获取状态!
|
||||||
|
return (
|
||||||
|
<form action={action}>
|
||||||
|
<button disabled={pending}>Submit</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ useFormStatus 必须在 form 的子组件中
|
||||||
|
function GoodForm() {
|
||||||
|
return (
|
||||||
|
<form action={action}>
|
||||||
|
<SubmitButton /> {/* useFormStatus 在这里面调用 */}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### useOptimistic
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ 等待服务器响应再更新 UI
|
||||||
|
function SlowLike({ postId, likes }) {
|
||||||
|
const [likeCount, setLikeCount] = useState(likes);
|
||||||
|
const [isPending, setIsPending] = useState(false);
|
||||||
|
|
||||||
|
const handleLike = async () => {
|
||||||
|
setIsPending(true);
|
||||||
|
const newCount = await likePost(postId); // 等待...
|
||||||
|
setLikeCount(newCount);
|
||||||
|
setIsPending(false);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ useOptimistic 即时反馈,失败自动回滚
|
||||||
|
import { useOptimistic } from 'react';
|
||||||
|
|
||||||
|
function FastLike({ postId, likes }) {
|
||||||
|
const [optimisticLikes, addOptimisticLike] = useOptimistic(
|
||||||
|
likes,
|
||||||
|
(currentLikes, increment: number) => currentLikes + increment
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleLike = async () => {
|
||||||
|
addOptimisticLike(1); // 立即更新 UI
|
||||||
|
try {
|
||||||
|
await likePost(postId); // 后台同步
|
||||||
|
} catch {
|
||||||
|
// React 自动回滚到 likes 原值
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <button onClick={handleLike}>{optimisticLikes} likes</button>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Actions (Next.js 15+)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ 客户端调用 API
|
||||||
|
'use client';
|
||||||
|
function ClientForm() {
|
||||||
|
const handleSubmit = async (formData: FormData) => {
|
||||||
|
const res = await fetch('/api/submit', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Server Action + useActionState
|
||||||
|
// actions.ts
|
||||||
|
'use server';
|
||||||
|
export async function createPost(prevState: any, formData: FormData) {
|
||||||
|
const title = formData.get('title');
|
||||||
|
await db.posts.create({ title });
|
||||||
|
revalidatePath('/posts');
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// form.tsx
|
||||||
|
'use client';
|
||||||
|
import { createPost } from './actions';
|
||||||
|
|
||||||
|
function PostForm() {
|
||||||
|
const [state, formAction, isPending] = useActionState(createPost, null);
|
||||||
|
return (
|
||||||
|
<form action={formAction}>
|
||||||
|
<input name="title" />
|
||||||
|
<SubmitButton />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Suspense & Streaming SSR
|
||||||
|
|
||||||
|
Suspense 和 Streaming 是 React 18+ 的核心特性,在 2025 年的 Next.js 15 等框架中广泛使用。
|
||||||
|
|
||||||
|
### 基础 Suspense
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ 传统加载状态管理
|
||||||
|
function OldComponent() {
|
||||||
|
const [data, setData] = useState(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData().then(setData).finally(() => setIsLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (isLoading) return <Spinner />;
|
||||||
|
return <DataView data={data} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Suspense 声明式加载状态
|
||||||
|
function NewComponent() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<Spinner />}>
|
||||||
|
<DataView /> {/* 内部使用 use() 或支持 Suspense 的数据获取 */}
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 多个独立 Suspense 边界
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ 单一边界——所有内容一起加载
|
||||||
|
function BadLayout() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<FullPageSpinner />}>
|
||||||
|
<Header />
|
||||||
|
<MainContent /> {/* 慢 */}
|
||||||
|
<Sidebar /> {/* 快 */}
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 独立边界——各部分独立流式传输
|
||||||
|
function GoodLayout() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header /> {/* 立即显示 */}
|
||||||
|
<div className="flex">
|
||||||
|
<Suspense fallback={<ContentSkeleton />}>
|
||||||
|
<MainContent /> {/* 独立加载 */}
|
||||||
|
</Suspense>
|
||||||
|
<Suspense fallback={<SidebarSkeleton />}>
|
||||||
|
<Sidebar /> {/* 独立加载 */}
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Next.js 15 Streaming
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// app/page.tsx - 自动 Streaming
|
||||||
|
export default async function Page() {
|
||||||
|
// 这个 await 不会阻塞整个页面
|
||||||
|
const data = await fetchSlowData();
|
||||||
|
return <div>{data}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// app/loading.tsx - 自动 Suspense 边界
|
||||||
|
export default function Loading() {
|
||||||
|
return <Skeleton />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### use() Hook (React 19)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ✅ 在组件中读取 Promise
|
||||||
|
import { use } from 'react';
|
||||||
|
|
||||||
|
function Comments({ commentsPromise }) {
|
||||||
|
const comments = use(commentsPromise); // 自动触发 Suspense
|
||||||
|
return (
|
||||||
|
<ul>
|
||||||
|
{comments.map(c => <li key={c.id}>{c.text}</li>)}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 父组件创建 Promise,子组件消费
|
||||||
|
function Post({ postId }) {
|
||||||
|
const commentsPromise = fetchComments(postId); // 不 await
|
||||||
|
return (
|
||||||
|
<article>
|
||||||
|
<PostContent id={postId} />
|
||||||
|
<Suspense fallback={<CommentsSkeleton />}>
|
||||||
|
<Comments commentsPromise={commentsPromise} />
|
||||||
|
</Suspense>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TanStack Query v5
|
||||||
|
|
||||||
|
TanStack Query 是 React 生态中最流行的数据获取库,v5 是当前稳定版本。
|
||||||
|
|
||||||
|
### 基础配置
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ 不正确的默认配置
|
||||||
|
const queryClient = new QueryClient(); // 默认配置可能不适合
|
||||||
|
|
||||||
|
// ✅ 生产环境推荐配置
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 1000 * 60 * 5, // 5 分钟内数据视为新鲜
|
||||||
|
gcTime: 1000 * 60 * 30, // 30 分钟后垃圾回收(v5 重命名)
|
||||||
|
retry: 3,
|
||||||
|
refetchOnWindowFocus: false, // 根据需求决定
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### queryOptions (v5 新增)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ 重复定义 queryKey 和 queryFn
|
||||||
|
function Component1() {
|
||||||
|
const { data } = useQuery({
|
||||||
|
queryKey: ['users', userId],
|
||||||
|
queryFn: () => fetchUser(userId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function prefetchUser(queryClient, userId) {
|
||||||
|
queryClient.prefetchQuery({
|
||||||
|
queryKey: ['users', userId], // 重复!
|
||||||
|
queryFn: () => fetchUser(userId), // 重复!
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ queryOptions 统一定义,类型安全
|
||||||
|
import { queryOptions } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
const userQueryOptions = (userId: string) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ['users', userId],
|
||||||
|
queryFn: () => fetchUser(userId),
|
||||||
|
});
|
||||||
|
|
||||||
|
function Component1({ userId }) {
|
||||||
|
const { data } = useQuery(userQueryOptions(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
function prefetchUser(queryClient, userId) {
|
||||||
|
queryClient.prefetchQuery(userQueryOptions(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// getQueryData 也是类型安全的
|
||||||
|
const user = queryClient.getQueryData(userQueryOptions(userId).queryKey);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常见陷阱
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ staleTime 为 0 导致过度请求
|
||||||
|
useQuery({
|
||||||
|
queryKey: ['data'],
|
||||||
|
queryFn: fetchData,
|
||||||
|
// staleTime 默认为 0,每次组件挂载都会 refetch
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ 设置合理的 staleTime
|
||||||
|
useQuery({
|
||||||
|
queryKey: ['data'],
|
||||||
|
queryFn: fetchData,
|
||||||
|
staleTime: 1000 * 60, // 1 分钟内不会重新请求
|
||||||
|
});
|
||||||
|
|
||||||
|
// ❌ 在 queryFn 中使用不稳定的引用
|
||||||
|
function BadQuery({ filters }) {
|
||||||
|
useQuery({
|
||||||
|
queryKey: ['items'], // queryKey 没有包含 filters!
|
||||||
|
queryFn: () => fetchItems(filters), // filters 变化不会触发重新请求
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ queryKey 包含所有影响数据的参数
|
||||||
|
function GoodQuery({ filters }) {
|
||||||
|
useQuery({
|
||||||
|
queryKey: ['items', filters], // filters 是 queryKey 的一部分
|
||||||
|
queryFn: () => fetchItems(filters),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### useSuspenseQuery
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ 使用 useQuery + enabled 实现条件查询
|
||||||
|
function BadSuspenseQuery({ userId }) {
|
||||||
|
const { data } = useSuspenseQuery({
|
||||||
|
queryKey: ['user', userId],
|
||||||
|
queryFn: () => fetchUser(userId),
|
||||||
|
enabled: !!userId, // useSuspenseQuery 不支持 enabled!
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 组件组合实现条件渲染
|
||||||
|
function GoodSuspenseQuery({ userId }) {
|
||||||
|
// useSuspenseQuery 保证 data 是 T 不是 T | undefined
|
||||||
|
const { data } = useSuspenseQuery({
|
||||||
|
queryKey: ['user', userId],
|
||||||
|
queryFn: () => fetchUser(userId),
|
||||||
|
});
|
||||||
|
return <UserProfile user={data} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Parent({ userId }) {
|
||||||
|
if (!userId) return <NoUserSelected />;
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<UserSkeleton />}>
|
||||||
|
<GoodSuspenseQuery userId={userId} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 乐观更新 (v5 简化)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ 手动管理缓存的乐观更新(复杂)
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: updateTodo,
|
||||||
|
onMutate: async (newTodo) => {
|
||||||
|
await queryClient.cancelQueries({ queryKey: ['todos'] });
|
||||||
|
const previousTodos = queryClient.getQueryData(['todos']);
|
||||||
|
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
|
||||||
|
return { previousTodos };
|
||||||
|
},
|
||||||
|
onError: (err, newTodo, context) => {
|
||||||
|
queryClient.setQueryData(['todos'], context.previousTodos);
|
||||||
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['todos'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ v5 简化:使用 variables 进行乐观 UI
|
||||||
|
function TodoList() {
|
||||||
|
const { data: todos } = useQuery(todosQueryOptions);
|
||||||
|
const { mutate, variables, isPending } = useMutation({
|
||||||
|
mutationFn: addTodo,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['todos'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul>
|
||||||
|
{todos?.map(todo => <TodoItem key={todo.id} todo={todo} />)}
|
||||||
|
{/* 乐观显示正在添加的 todo */}
|
||||||
|
{isPending && <TodoItem todo={variables} isOptimistic />}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### v5 状态字段变化
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// v4: isLoading 表示首次加载或后续获取
|
||||||
|
// v5: isPending 表示没有数据,isLoading = isPending && isFetching
|
||||||
|
|
||||||
|
const { data, isPending, isFetching, isLoading } = useQuery({...});
|
||||||
|
|
||||||
|
// isPending: 缓存中没有数据(首次加载)
|
||||||
|
// isFetching: 正在请求中(包括后台刷新)
|
||||||
|
// isLoading: isPending && isFetching(首次加载中)
|
||||||
|
|
||||||
|
// ❌ v4 代码直接迁移
|
||||||
|
if (isLoading) return <Spinner />; // v5 中行为可能不同
|
||||||
|
|
||||||
|
// ✅ 明确意图
|
||||||
|
if (isPending) return <Spinner />; // 没有数据时显示加载
|
||||||
|
// 或
|
||||||
|
if (isLoading) return <Spinner />; // 首次加载中
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Review Checklists
|
||||||
|
|
||||||
|
### Hooks 规则
|
||||||
|
|
||||||
|
- [ ] Hooks 在组件/自定义 Hook 顶层调用
|
||||||
|
- [ ] 没有条件/循环中调用 Hooks
|
||||||
|
- [ ] useEffect 依赖数组完整
|
||||||
|
- [ ] useEffect 有清理函数(订阅/定时器/请求)
|
||||||
|
- [ ] 没有用 useEffect 计算派生状态
|
||||||
|
|
||||||
|
### 性能优化(适度原则)
|
||||||
|
|
||||||
|
- [ ] useMemo/useCallback 只用于真正需要的场景
|
||||||
|
- [ ] React.memo 配合稳定的 props 引用
|
||||||
|
- [ ] 没有在组件内定义子组件
|
||||||
|
- [ ] 没有在 JSX 中创建新对象/函数(除非传给非 memo 组件)
|
||||||
|
- [ ] 长列表使用虚拟化(react-window/react-virtual)
|
||||||
|
|
||||||
|
### 组件设计
|
||||||
|
|
||||||
|
- [ ] 组件职责单一,不超过 200 行
|
||||||
|
- [ ] 逻辑与展示分离(Custom Hooks)
|
||||||
|
- [ ] Props 接口清晰,使用 TypeScript
|
||||||
|
- [ ] 避免 Props Drilling(考虑 Context 或组合)
|
||||||
|
|
||||||
|
### 状态管理
|
||||||
|
|
||||||
|
- [ ] 状态就近原则(最小必要范围)
|
||||||
|
- [ ] 复杂状态用 useReducer
|
||||||
|
- [ ] 全局状态用 Context 或状态库
|
||||||
|
- [ ] 避免不必要的状态(派生 > 存储)
|
||||||
|
|
||||||
|
### 错误处理
|
||||||
|
|
||||||
|
- [ ] 关键区域有 Error Boundary
|
||||||
|
- [ ] Suspense 配合 Error Boundary 使用
|
||||||
|
- [ ] 异步操作有错误处理
|
||||||
|
|
||||||
|
### Server Components (RSC)
|
||||||
|
|
||||||
|
- [ ] 'use client' 只用于需要交互的组件
|
||||||
|
- [ ] Server Component 不使用 Hooks/事件处理
|
||||||
|
- [ ] 客户端组件尽量放在叶子节点
|
||||||
|
- [ ] 数据获取在 Server Component 中进行
|
||||||
|
|
||||||
|
### React 19 Forms
|
||||||
|
|
||||||
|
- [ ] 使用 useActionState 替代多个 useState
|
||||||
|
- [ ] useFormStatus 在 form 子组件中调用
|
||||||
|
- [ ] useOptimistic 不用于关键业务(支付等)
|
||||||
|
- [ ] Server Action 正确标记 'use server'
|
||||||
|
|
||||||
|
### Suspense & Streaming
|
||||||
|
|
||||||
|
- [ ] 按用户体验需求划分 Suspense 边界
|
||||||
|
- [ ] 每个 Suspense 有对应的 Error Boundary
|
||||||
|
- [ ] 提供有意义的 fallback(骨架屏 > Spinner)
|
||||||
|
- [ ] 避免在 layout 层级 await 慢数据
|
||||||
|
|
||||||
|
### TanStack Query
|
||||||
|
|
||||||
|
- [ ] queryKey 包含所有影响数据的参数
|
||||||
|
- [ ] 设置合理的 staleTime(不是默认 0)
|
||||||
|
- [ ] useSuspenseQuery 不使用 enabled
|
||||||
|
- [ ] Mutation 成功后 invalidate 相关查询
|
||||||
|
- [ ] 理解 isPending vs isLoading 区别
|
||||||
|
|
||||||
|
### 测试
|
||||||
|
|
||||||
|
- [ ] 使用 @testing-library/react
|
||||||
|
- [ ] 用 screen 查询元素
|
||||||
|
- [ ] 用 userEvent 代替 fireEvent
|
||||||
|
- [ ] 优先使用 *ByRole 查询
|
||||||
|
- [ ] 测试行为而非实现细节
|
||||||
256
references/rust.md
Normal file
256
references/rust.md
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
# Rust Code Review Guide
|
||||||
|
|
||||||
|
> Rust 代码审查指南。编译器能捕获内存安全问题,但审查者需要关注编译器无法检测的问题——业务逻辑、API 设计、性能和可维护性。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 所有权与借用
|
||||||
|
|
||||||
|
### 避免不必要的 clone()
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// ❌ clone() 是"Rust 的胶带"——用于绕过借用检查器
|
||||||
|
fn bad_process(data: &Data) -> Result<()> {
|
||||||
|
let owned = data.clone(); // 为什么需要 clone?
|
||||||
|
expensive_operation(owned)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 审查时问:clone 是否必要?能否用借用?
|
||||||
|
fn good_process(data: &Data) -> Result<()> {
|
||||||
|
expensive_operation(data) // 传递引用
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arc<Mutex<T>> 的使用
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// ❌ Arc<Mutex<T>> 可能隐藏不必要的共享状态
|
||||||
|
struct BadService {
|
||||||
|
cache: Arc<Mutex<HashMap<String, Data>>>, // 真的需要共享?
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 考虑是否需要共享,或者设计可以避免
|
||||||
|
struct GoodService {
|
||||||
|
cache: HashMap<String, Data>, // 单一所有者
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Unsafe 代码审查(最关键!)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// ❌ unsafe 没有安全文档——这是红旗
|
||||||
|
unsafe fn bad_transmute<T, U>(t: T) -> U {
|
||||||
|
std::mem::transmute(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 每个 unsafe 必须解释:为什么安全?什么不变量?
|
||||||
|
/// Transmutes `T` to `U`.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// - `T` and `U` must have the same size and alignment
|
||||||
|
/// - `T` must be a valid bit pattern for `U`
|
||||||
|
/// - The caller ensures no references to `t` exist after this call
|
||||||
|
unsafe fn documented_transmute<T, U>(t: T) -> U {
|
||||||
|
// SAFETY: Caller guarantees size/alignment match and bit validity
|
||||||
|
std::mem::transmute(t)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 异步代码
|
||||||
|
|
||||||
|
### 避免阻塞操作
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// ❌ 在 async 上下文中阻塞——会饿死其他任务
|
||||||
|
async fn bad_async() {
|
||||||
|
let data = std::fs::read_to_string("file.txt").unwrap(); // 阻塞!
|
||||||
|
std::thread::sleep(Duration::from_secs(1)); // 阻塞!
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 使用异步 API
|
||||||
|
async fn good_async() {
|
||||||
|
let data = tokio::fs::read_to_string("file.txt").await?;
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mutex 和 .await
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// ❌ 跨 .await 持有 std::sync::Mutex——可能死锁
|
||||||
|
async fn bad_lock(mutex: &std::sync::Mutex<Data>) {
|
||||||
|
let guard = mutex.lock().unwrap();
|
||||||
|
async_operation().await; // 持锁等待!
|
||||||
|
process(&guard);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 最小化锁范围,或使用 tokio::sync::Mutex
|
||||||
|
async fn good_lock(mutex: &std::sync::Mutex<Data>) {
|
||||||
|
let data = mutex.lock().unwrap().clone(); // 立即释放
|
||||||
|
async_operation().await;
|
||||||
|
process(&data);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
### 库 vs 应用的错误类型
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// ❌ 库代码用 anyhow——调用者无法 match 错误
|
||||||
|
pub fn parse_config(s: &str) -> anyhow::Result<Config> { ... }
|
||||||
|
|
||||||
|
// ✅ 库用 thiserror,应用用 anyhow
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum ConfigError {
|
||||||
|
#[error("invalid syntax at line {line}: {message}")]
|
||||||
|
Syntax { line: usize, message: String },
|
||||||
|
#[error("missing required field: {0}")]
|
||||||
|
MissingField(String),
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_config(s: &str) -> Result<Config, ConfigError> { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 保留错误上下文
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// ❌ 吞掉错误上下文
|
||||||
|
fn bad_error() -> Result<()> {
|
||||||
|
operation().map_err(|_| anyhow!("failed"))?; // 原始错误丢失
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 使用 context 保留错误链
|
||||||
|
fn good_error() -> Result<()> {
|
||||||
|
operation().context("failed to perform operation")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 性能
|
||||||
|
|
||||||
|
### 避免不必要的 collect()
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// ❌ 不必要的 collect——中间分配
|
||||||
|
fn bad_sum(items: &[i32]) -> i32 {
|
||||||
|
items.iter()
|
||||||
|
.filter(|x| **x > 0)
|
||||||
|
.collect::<Vec<_>>() // 不必要!
|
||||||
|
.iter()
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 惰性迭代
|
||||||
|
fn good_sum(items: &[i32]) -> i32 {
|
||||||
|
items.iter().filter(|x| **x > 0).sum()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 字符串拼接
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// ❌ 字符串拼接在循环中重复分配
|
||||||
|
fn bad_concat(items: &[&str]) -> String {
|
||||||
|
let mut s = String::new();
|
||||||
|
for item in items {
|
||||||
|
s = s + item; // 每次都重新分配!
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 预分配或用 join
|
||||||
|
fn good_concat(items: &[&str]) -> String {
|
||||||
|
items.join("") // 或用 with_capacity
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Trait 设计
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// ❌ 过度抽象——不是 Java,不需要 Interface 一切
|
||||||
|
trait Processor { fn process(&self); }
|
||||||
|
trait Handler { fn handle(&self); }
|
||||||
|
trait Manager { fn manage(&self); } // Trait 过多
|
||||||
|
|
||||||
|
// ✅ 只在需要多态时创建 trait
|
||||||
|
// 具体类型通常更简单、更快
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rust Review Checklist
|
||||||
|
|
||||||
|
### 编译器不能捕获的问题
|
||||||
|
|
||||||
|
**业务逻辑正确性**
|
||||||
|
- [ ] 边界条件处理正确
|
||||||
|
- [ ] 状态机转换完整
|
||||||
|
- [ ] 并发场景下的竞态条件
|
||||||
|
|
||||||
|
**API 设计**
|
||||||
|
- [ ] 公共 API 难以误用
|
||||||
|
- [ ] 类型签名清晰表达意图
|
||||||
|
- [ ] 错误类型粒度合适
|
||||||
|
|
||||||
|
### 所有权与借用
|
||||||
|
|
||||||
|
- [ ] clone() 是有意为之,文档说明了原因
|
||||||
|
- [ ] Arc<Mutex<T>> 真的需要共享状态吗?
|
||||||
|
- [ ] RefCell 的使用有正当理由
|
||||||
|
- [ ] 生命周期不过度复杂
|
||||||
|
|
||||||
|
### Unsafe 代码(最重要)
|
||||||
|
|
||||||
|
- [ ] 每个 unsafe 块有 SAFETY 注释
|
||||||
|
- [ ] unsafe fn 有 # Safety 文档节
|
||||||
|
- [ ] 解释了为什么是安全的,不只是做什么
|
||||||
|
- [ ] 列出了必须维护的不变量
|
||||||
|
- [ ] unsafe 边界尽可能小
|
||||||
|
- [ ] 考虑过是否有 safe 替代方案
|
||||||
|
|
||||||
|
### 异步/并发
|
||||||
|
|
||||||
|
- [ ] 没有在 async 中阻塞(std::fs、thread::sleep)
|
||||||
|
- [ ] 没有跨 .await 持有 std::sync 锁
|
||||||
|
- [ ] spawn 的任务满足 'static
|
||||||
|
- [ ] 锁的获取顺序一致
|
||||||
|
- [ ] Channel 缓冲区大小合理
|
||||||
|
|
||||||
|
### 错误处理
|
||||||
|
|
||||||
|
- [ ] 库:thiserror 定义结构化错误
|
||||||
|
- [ ] 应用:anyhow + context
|
||||||
|
- [ ] 没有生产代码 unwrap/expect
|
||||||
|
- [ ] 错误消息对调试有帮助
|
||||||
|
- [ ] must_use 返回值被处理
|
||||||
|
|
||||||
|
### 性能
|
||||||
|
|
||||||
|
- [ ] 避免不必要的 collect()
|
||||||
|
- [ ] 大数据传引用
|
||||||
|
- [ ] 字符串用 with_capacity 或 write!
|
||||||
|
- [ ] impl Trait vs Box<dyn Trait> 选择合理
|
||||||
|
- [ ] 热路径避免分配
|
||||||
|
|
||||||
|
### 代码质量
|
||||||
|
|
||||||
|
- [ ] cargo clippy 零警告
|
||||||
|
- [ ] cargo fmt 格式化
|
||||||
|
- [ ] 文档注释完整
|
||||||
|
- [ ] 测试覆盖边界条件
|
||||||
|
- [ ] 公共 API 有文档示例
|
||||||
101
references/typescript.md
Normal file
101
references/typescript.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# TypeScript/JavaScript Code Review Guide
|
||||||
|
|
||||||
|
> TypeScript/JavaScript 通用代码审查指南,覆盖类型安全、异步处理、常见陷阱。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 类型安全
|
||||||
|
|
||||||
|
### 避免使用 any
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Using any defeats type safety
|
||||||
|
function processData(data: any) { // Avoid any
|
||||||
|
return data.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Use proper types
|
||||||
|
interface DataPayload {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
function processData(data: DataPayload) {
|
||||||
|
return data.value;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 异步处理
|
||||||
|
|
||||||
|
### 错误处理
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Not handling async errors
|
||||||
|
async function fetchUser(id: string) {
|
||||||
|
const response = await fetch(`/api/users/${id}`);
|
||||||
|
return response.json(); // What if network fails?
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Handle errors properly
|
||||||
|
async function fetchUser(id: string): Promise<User> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/users/${id}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch user:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 不可变性
|
||||||
|
|
||||||
|
### 避免直接修改 Props/参数
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Mutation of props
|
||||||
|
function UserProfile({ user }: Props) {
|
||||||
|
user.lastViewed = new Date(); // Mutating prop!
|
||||||
|
return <div>{user.name}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Don't mutate props
|
||||||
|
function UserProfile({ user, onView }: Props) {
|
||||||
|
useEffect(() => {
|
||||||
|
onView(user.id); // Notify parent to update
|
||||||
|
}, [user.id]);
|
||||||
|
return <div>{user.name}</div>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TypeScript Review Checklist
|
||||||
|
|
||||||
|
### 类型系统
|
||||||
|
- [ ] 没有使用 `any`(使用 `unknown` 代替未知类型)
|
||||||
|
- [ ] 接口和类型定义完整
|
||||||
|
- [ ] 使用泛型提高代码复用性
|
||||||
|
- [ ] 联合类型有正确的类型收窄
|
||||||
|
|
||||||
|
### 异步代码
|
||||||
|
- [ ] async 函数有错误处理
|
||||||
|
- [ ] Promise rejection 被正确处理
|
||||||
|
- [ ] 避免 callback hell,使用 async/await
|
||||||
|
- [ ] 并发请求使用 `Promise.all` 或 `Promise.allSettled`
|
||||||
|
|
||||||
|
### 不可变性
|
||||||
|
- [ ] 不直接修改函数参数
|
||||||
|
- [ ] 使用 spread 操作符创建新对象/数组
|
||||||
|
- [ ] 考虑使用 `readonly` 修饰符
|
||||||
|
|
||||||
|
### 代码质量
|
||||||
|
- [ ] 启用 strict 模式
|
||||||
|
- [ ] ESLint/TSLint 无警告
|
||||||
|
- [ ] 函数有返回类型注解
|
||||||
|
- [ ] 避免类型断言(`as`),除非确实必要
|
||||||
240
references/vue.md
Normal file
240
references/vue.md
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
# Vue 3 Code Review Guide
|
||||||
|
|
||||||
|
> Vue 3 Composition API 代码审查指南,覆盖响应性系统、Props/Emits、Watchers、Composables 等核心主题。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 响应性系统
|
||||||
|
|
||||||
|
### 解构 reactive 对象
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ❌ 解构 reactive 会丢失响应性 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
const state = reactive({ count: 0, name: 'Vue' })
|
||||||
|
const { count, name } = state // 丢失响应性!
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- ✅ 使用 toRefs 保持响应性 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
const state = reactive({ count: 0, name: 'Vue' })
|
||||||
|
const { count, name } = toRefs(state) // 保持响应性
|
||||||
|
// 或者直接使用 ref
|
||||||
|
const count = ref(0)
|
||||||
|
const name = ref('Vue')
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### computed 副作用
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ❌ computed 中产生副作用 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
const fullName = computed(() => {
|
||||||
|
console.log('Computing...') // 副作用!
|
||||||
|
otherRef.value = 'changed' // 修改其他状态!
|
||||||
|
return `${firstName.value} ${lastName.value}`
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- ✅ computed 只用于派生状态 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
const fullName = computed(() => {
|
||||||
|
return `${firstName.value} ${lastName.value}`
|
||||||
|
})
|
||||||
|
// 副作用放在 watch 或事件处理中
|
||||||
|
watch(fullName, (name) => {
|
||||||
|
console.log('Name changed:', name)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Props & Emits
|
||||||
|
|
||||||
|
### 直接修改 props
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ❌ 直接修改 props -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{ user: User }>()
|
||||||
|
props.user.name = 'New Name' // 永远不要直接修改 props!
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- ✅ 使用 emit 通知父组件更新 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{ user: User }>()
|
||||||
|
const emit = defineEmits<{
|
||||||
|
update: [name: string]
|
||||||
|
}>()
|
||||||
|
const updateName = (name: string) => emit('update', name)
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### defineProps 类型声明
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ❌ defineProps 缺少类型声明 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps(['title', 'count']) // 无类型检查
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- ✅ 使用类型声明 + withDefaults -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
title: string
|
||||||
|
count?: number
|
||||||
|
items?: string[]
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
count: 0,
|
||||||
|
items: () => [] // 对象/数组默认值需要工厂函数
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Watchers
|
||||||
|
|
||||||
|
### watch 清理函数
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ❌ watch 缺少清理函数,可能内存泄漏 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
watch(searchQuery, async (query) => {
|
||||||
|
const controller = new AbortController()
|
||||||
|
const data = await fetch(`/api/search?q=${query}`, {
|
||||||
|
signal: controller.signal
|
||||||
|
})
|
||||||
|
results.value = await data.json()
|
||||||
|
// 如果 query 快速变化,旧请求不会被取消!
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- ✅ 使用 onCleanup 清理副作用 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
watch(searchQuery, async (query, _, onCleanup) => {
|
||||||
|
const controller = new AbortController()
|
||||||
|
onCleanup(() => controller.abort()) // 取消旧请求
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await fetch(`/api/search?q=${query}`, {
|
||||||
|
signal: controller.signal
|
||||||
|
})
|
||||||
|
results.value = await data.json()
|
||||||
|
} catch (e) {
|
||||||
|
if (e.name !== 'AbortError') throw e
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 模板最佳实践
|
||||||
|
|
||||||
|
### v-for 的 key
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ❌ v-for 中使用 index 作为 key -->
|
||||||
|
<template>
|
||||||
|
<li v-for="(item, index) in items" :key="index">
|
||||||
|
{{ item.name }}
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ✅ 使用唯一标识作为 key -->
|
||||||
|
<template>
|
||||||
|
<li v-for="item in items" :key="item.id">
|
||||||
|
{{ item.name }}
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### v-if 和 v-for 优先级
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ❌ v-if 和 v-for 同时使用 -->
|
||||||
|
<template>
|
||||||
|
<li v-for="user in users" v-if="user.active" :key="user.id">
|
||||||
|
{{ user.name }}
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ✅ 使用 computed 过滤 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
const activeUsers = computed(() =>
|
||||||
|
users.value.filter(user => user.active)
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<li v-for="user in activeUsers" :key="user.id">
|
||||||
|
{{ user.name }}
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Composables
|
||||||
|
|
||||||
|
### Props 传递给 composable
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ❌ 传递 props 到 composable 丢失响应性 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{ userId: string }>()
|
||||||
|
const { user } = useUser(props.userId) // 丢失响应性!
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- ✅ 使用 toRef 或 computed 保持响应性 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{ userId: string }>()
|
||||||
|
const userIdRef = toRef(props, 'userId')
|
||||||
|
const { user } = useUser(userIdRef) // 保持响应性
|
||||||
|
// 或使用 computed
|
||||||
|
const { user } = useUser(computed(() => props.userId))
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vue 3 Review Checklist
|
||||||
|
|
||||||
|
### 响应性系统
|
||||||
|
- [ ] ref 用于基本类型,reactive 用于对象
|
||||||
|
- [ ] 没有解构 reactive 对象(或使用了 toRefs)
|
||||||
|
- [ ] props 传递给 composable 时保持了响应性
|
||||||
|
- [ ] shallowRef/shallowReactive 用于大型对象优化
|
||||||
|
|
||||||
|
### Props & Emits
|
||||||
|
- [ ] defineProps 使用 TypeScript 类型声明
|
||||||
|
- [ ] 复杂默认值使用 withDefaults + 工厂函数
|
||||||
|
- [ ] defineEmits 有完整的类型定义
|
||||||
|
- [ ] 没有直接修改 props
|
||||||
|
|
||||||
|
### Watchers
|
||||||
|
- [ ] watch/watchEffect 有适当的清理函数
|
||||||
|
- [ ] 异步 watch 处理了竞态条件
|
||||||
|
- [ ] flush: 'post' 用于 DOM 操作的 watcher
|
||||||
|
- [ ] 避免过度使用 watcher(优先用 computed)
|
||||||
|
|
||||||
|
### 模板
|
||||||
|
- [ ] v-for 使用唯一且稳定的 key
|
||||||
|
- [ ] v-if 和 v-for 没有在同一元素上
|
||||||
|
- [ ] 事件处理使用 kebab-case
|
||||||
|
- [ ] 大型列表使用虚拟滚动
|
||||||
|
|
||||||
|
### Composables
|
||||||
|
- [ ] 相关逻辑提取到 composables
|
||||||
|
- [ ] composables 返回响应式引用(不是 .value)
|
||||||
|
- [ ] 纯函数不要包装成 composable
|
||||||
|
- [ ] 副作用在组件卸载时清理
|
||||||
|
|
||||||
|
### 性能
|
||||||
|
- [ ] 大型组件拆分为小组件
|
||||||
|
- [ ] 使用 defineAsyncComponent 懒加载
|
||||||
|
- [ ] 避免不必要的响应式转换
|
||||||
|
- [ ] v-memo 用于昂贵的列表渲染
|
||||||
Reference in New Issue
Block a user