diff --git a/.clawhub/lock.json b/.clawhub/lock.json index 9374d84..d93480b 100644 --- a/.clawhub/lock.json +++ b/.clawhub/lock.json @@ -44,6 +44,14 @@ "openclaw-agent-browser": { "version": "1.0.0", "installedAt": 1779234569458 + }, + "next-best-practices": { + "version": "0.1.0", + "installedAt": 1779235116283 + }, + "vue-composition-api-best-practices": { + "version": "1.0.0", + "installedAt": 1779235185182 } } } diff --git a/.learnings/LEARNINGS.md b/.learnings/LEARNINGS.md index ece85c6..aa62bfb 100644 --- a/.learnings/LEARNINGS.md +++ b/.learnings/LEARNINGS.md @@ -51,3 +51,35 @@ Fazer múltiplas searches com termos curtos e depois filtrar manualmente. --- + +## [LRN-20260519-003] biblioteca-compartilhada-libs + +**Logged**: 2026-05-19T21:30:00-03:00 +**Priority**: medium +**Status**: pending +**Area**: config + +### Summary +Criar biblioteca inteligente compartilhada em `libs/` para reuso entre projetos, com conhecimento extraído de todas as skills instaladas. + +### Details +Toda skill que instalamos tem conhecimento valioso (padrões, gotchas, templates). +Ao invés de cada agente lembrar de cor, centralize em `libs//`: +- skills são extraídas e promovidas para arquivos .md limpos na biblioteca +- novos projetos copiam `libs/` como template de padrões +- o próprio agente consulte `libs/` antes de implementar qualquer coisa + +### Suggested Action +Quando instalar nova skill: +1. Ler o SKILL.md +2. Extrair o conhecimento útil +3. Promover para `libs//` apropriado +4. Atualizar `libs/INDEX.md` + +### Metadata +- Source: best_practice +- Tags: biblioteca, reuso, padroes, compartilhamento +- Pattern-Key: libs.shared_knowledge_base +- Recurrence-Count: 1 + +--- diff --git a/.learnings/PATTERN_COUNTER.md b/.learnings/PATTERN_COUNTER.md index ae2627b..cecdcfc 100644 --- a/.learnings/PATTERN_COUNTER.md +++ b/.learnings/PATTERN_COUNTER.md @@ -6,6 +6,8 @@ _Rastreia quantas vezes cada abordagem funcionou para identificar skills candida |---|---|---|---| | clawhub.cli_path | 1 | 2026-05-19 | tracking | | clawhub.search_strategy | 1 | 2026-05-19 | tracking | +| libs.shared_knowledge_base | 1 | 2026-05-19 | tracking | +| skill.extra | 3 | 2026-05-19 | tracking | _Quando Count >= 3 e visto em >= 2 tarefas distintas em 30 dias → promover para AGENTS.md_ diff --git a/MEMORY.md b/MEMORY.md index 48f0213..5d0980b 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -58,3 +58,18 @@ IA → nova-self-improver (auto-melhoria contínua) ## 🔑 Comandos Linux rápidos (referência) Ver AGENTS.md — seção Linux Analyst para a lista completa. + + +## 📚 Biblioteca Inteligente — libs/ +Biblioteca compartilhada de padrões criada para ser usada em todos os projetos: +- `libs/typescript/` — TS safe patterns + generics/utility gotchas +- `libs/react/` — Next.js App Router + Vite config +- `libs/vue/` — Vue 3 Composition API + Pinia +- `libs/linux/` — Diagnóstico completo do sistema +- `libs/database/` — PostgreSQL + MySQL prático +- `libs/browser/` — Chromium CLI + E2E testing +- `libs/security/` — SAST audit guide (OWASP Top 10) +- `libs/best-practices/` — Clean Code + SOLID + Clean Architecture +- `libs/deploy/` — Docker multi-stack + OpenClaw ops +- `libs/docs/` — Templates de documentação +Ver `libs/INDEX.md` para índice completo. diff --git a/SESSION-STATE.md b/SESSION-STATE.md index fe8d511..32f96b3 100644 --- a/SESSION-STATE.md +++ b/SESSION-STATE.md @@ -17,7 +17,12 @@ Expansão completa do agente: skills, Linux analyst, browser automation, TOOLS/A - [x] Expandir AGENTS.md com Linux analyst + full-stack strategy - [x] Configurar HEARTBEAT.md com tarefas úteis - [ ] Ler skills instaladas gradualmente quando for usá-las -- [ ] Depois de usar as skills, fazer os primeiros logs em .learnings/ + +- [x] Instalar next, vite, uncle-bob, clean-code-review, vue +- [x] Criar biblioteca compartilhada em libs/ (10 domínios) +- [x] Extrair e promover conhecimento das skills +- [ ] Logar LRN-20260519-003 para "biblioteca compartilhada em libs/" +- [ ] Log LRN para std de cada domínio (TS, React, Linux, DB, etc.) ## Skills — resumo rápido | Skill | Quando usar | diff --git a/libs/INDEX.md b/libs/INDEX.md new file mode 100644 index 0000000..6e4fd0c --- /dev/null +++ b/libs/INDEX.md @@ -0,0 +1,93 @@ +# 📚 Biblioteca Inteligente — Índice Completo + +> Biblioteca dinâmica para compartilhar entre todos os projetos. Conhecimento extraído +> automaticamente das skills instaladas, organizado por domínio. + +--- + +## 🗂️ Estrutura + +``` +libs/ +├── README.md ← Este arquivo +├── typescript/ ← TS seguro: narrowing, generics, satisfies +├── react/ ← Next.js, Vite, App Router, RSC, Server Actions +├── vue/ ← Vue 3 Composition API, Pinia, Router +├── linux/ ← Diagnóstico de sistema, logs, rede, SSH +├── database/ ← PostgreSQL, MySQL — schemas, queries, EXPLAIN +├── browser/ ← Agent-browser (Chromium) + E2E testing +├── security/ ← SAST: OWASP Top 10, prompt injection, secrets +├── best-practices/ ← Clean Code, SOLID, Clean Architecture +├── deploy/ ← Docker multi-stack, xCloud, OpenClaw Gateway +└── docs/ ← Templates de documentação +``` + +--- + +## 📁 Typescript — 2 arquivos +| Arquivo | Conteúdo | +|---------|----------| +| `typescript/TYPESCRIPT_SAFE_PATTERNS.md` | narrowing, satisfies, discriminated unions, literais | +| `typescript/GENERICS_UTILITY_GOTCHAS.md` | Armadilhas de generics & utility types `Partial`, `Omit`, `Pick` | + +## 📁 React — 2 arquivos +| Arquivo | Conteúdo | +|---------|----------| +| `react/NEXTJS_BEST_PRACTICES.md` | App Router, RSC, Server Actions, caching, data fetching | +| `react/VITE_CONFIG.md` | Env vars, path aliases, Dev Server proxy, CJS compat | + +## 📁 Vue — 1 arquivo +| Arquivo | Conteúdo | +|---------|----------| +| `vue/VUE3_COMPOSITION_API.md` | ` + + +``` + +## Composables — `useXxx` Pattern + +```ts +// composables/usePagination.ts +export function usePagination(items: T[], pageSize = 20) { + const page = ref(1) + const totalPages = computed(() => Math.ceil(items.value.length / pageSize)) + const paginated = computed(() => + items.value.slice((page.value - 1) * pageSize, page.value * pageSize) + ) + return { page, totalPages, paginated } +} +``` + +## + + + +``` + +## State Management — Pinia +```ts +// stores/counter.ts +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' + +export const useCounterStore = defineStore('counter', () => { + const count = ref(0) + const double = computed(() => count.value * 2) + function increment() { count.value++ } + return { count, double, increment } +}) +``` + +## Reactividade — Principais Armadilhas +- `ref` vs `reactive`: use `ref` por padrão (tipagem simples); `reactive` para objetos grandes +- Perda de reatividade ao desestruturar — usar `toRefs()` ou `storeToRefs()` +- `watch` vs `watchEffect`: `watch` é mais controlado; `watchEffect` é automático mas menos previsível +- `v-if` vs `v-show`: `v-if` remove do DOM; `v-show` togglea `display` + +## Type-Safe Vue +```vue + +``` + +## Vue Router Traps +- `useRoute()` para rota atual — reativa, usar em setup +- `useRouter()` para navegação — `router.push('/path')` +- Guards: `beforeEach`, `beforeResolve`, `afterEach` — retornar `false` cancela +- `` com named views — múltiplas views por rota + +## Common Mistakes +- `key` em `v-for` é obrigatório — `v-for="item in items" :key="item.id"` +- Ordem de event modifiers importa — `.prevent.stop` ≠ `.stop.prevent` +- `Teleport` para modais — `` renderiza fora da árvore diff --git a/skills/clean-code-review/.clawhub/origin.json b/skills/clean-code-review/.clawhub/origin.json new file mode 100644 index 0000000..70851cd --- /dev/null +++ b/skills/clean-code-review/.clawhub/origin.json @@ -0,0 +1,8 @@ +{ + "version": 1, + "registry": "https://clawhub.ai", + "slug": "clean-code-review", + "installedVersion": "1.0.0", + "installedAt": 1779235184537, + "fingerprint": "4b346d3428567b279a1b0e9bd1554f217fd666b5db431ab418bc26f1b1bd56e5" +} diff --git a/skills/clean-code-review/README.md b/skills/clean-code-review/README.md new file mode 100644 index 0000000..8c3458b --- /dev/null +++ b/skills/clean-code-review/README.md @@ -0,0 +1,77 @@ +# Clean Code + +Pragmatic coding standards for writing clean, maintainable code — naming, functions, structure, anti-patterns, and pre-edit safety checks. Every name reveals intent, every function does one thing, and every abstraction earns its place. + +## What's Inside + +- Core principles (SRP, DRY, KISS, YAGNI, Boy Scout Rule) +- Naming rules and naming anti-patterns +- Function rules (small, one thing, few args, guard clauses, parameter objects) +- Code structure patterns (composition, colocation, extract function) +- Return type consistency with discriminated unions +- Anti-patterns catalog (21 common mistakes) +- Pre-edit safety check (dependency impact analysis) +- Reference guides for code smells, anti-patterns, and refactoring catalog + +## When to Use + +- Writing new code and wanting to follow best practices +- Refactoring existing code for clarity and maintainability +- Reviewing code quality in pull requests +- Establishing coding standards for a team + +## Installation + +```bash +npx add https://github.com/wpank/ai/tree/main/skills/testing/clean-code +``` + +### OpenClaw / Moltbot / Clawbot + +```bash +npx clawhub@latest install clean-code-review +``` + +### Manual Installation + +#### Cursor (per-project) + +From your project root: + +```bash +mkdir -p .cursor/skills +cp -r ~/.ai-skills/skills/testing/clean-code .cursor/skills/clean-code +``` + +#### Cursor (global) + +```bash +mkdir -p ~/.cursor/skills +cp -r ~/.ai-skills/skills/testing/clean-code ~/.cursor/skills/clean-code +``` + +#### Claude Code (per-project) + +From your project root: + +```bash +mkdir -p .claude/skills +cp -r ~/.ai-skills/skills/testing/clean-code .claude/skills/clean-code +``` + +#### Claude Code (global) + +```bash +mkdir -p ~/.claude/skills +cp -r ~/.ai-skills/skills/testing/clean-code ~/.claude/skills/clean-code +``` + +## Related Skills + +- [code-review](../code-review/) — Structured code review checklists +- [reducing-entropy](../reducing-entropy/) — Minimize codebase size through simplification +- [testing-patterns](../testing-patterns/) — Testing patterns for verifying clean code + +--- + +Part of the [Testing](..) skill category. diff --git a/skills/clean-code-review/SKILL.md b/skills/clean-code-review/SKILL.md new file mode 100644 index 0000000..ae644dd --- /dev/null +++ b/skills/clean-code-review/SKILL.md @@ -0,0 +1,267 @@ +--- +name: clean-code +model: standard +category: testing +description: Pragmatic coding standards for writing clean, maintainable code — naming, functions, structure, anti-patterns, and pre-edit safety checks. Use when writing new code, refactoring existing code, reviewing code quality, or establishing coding standards. +version: 2.0 +--- + +# Clean Code + +> Be **concise, direct, and solution-focused**. Clean code reads like well-written prose — every name reveals intent, every function does one thing, and every abstraction earns its place. + + +## Installation + +### OpenClaw / Moltbot / Clawbot + +```bash +npx clawhub@latest install clean-code +``` + + +--- + +## Core Principles + +| Principle | Rule | Practical Test | +|-----------|------|----------------| +| **SRP** | Single Responsibility — each function/class does ONE thing | "Can I describe what this does without using 'and'?" | +| **DRY** | Don't Repeat Yourself — extract duplicates, reuse | "Have I written this logic before?" | +| **KISS** | Keep It Simple — simplest solution that works | "Is there a simpler way to achieve this?" | +| **YAGNI** | You Aren't Gonna Need It — don't build unused features | "Does anyone need this right now?" | +| **Boy Scout** | Leave code cleaner than you found it | "Is this file better after my change?" | + +--- + +## Naming Rules + +Names are the most important documentation. A good name eliminates the need for a comment. + +| Element | Convention | Bad | Good | +|---------|------------|-----|------| +| **Variables** | Reveal intent | `n`, `d`, `tmp` | `userCount`, `elapsed`, `activeUsers` | +| **Functions** | Verb + noun | `user()`, `calc()` | `getUserById()`, `calculateTotal()` | +| **Booleans** | Question form | `active`, `flag` | `isActive`, `hasPermission`, `canEdit` | +| **Constants** | SCREAMING_SNAKE | `max`, `timeout` | `MAX_RETRY_COUNT`, `REQUEST_TIMEOUT_MS` | +| **Classes** | Noun, singular | `Manager`, `Data` | `UserRepository`, `OrderService` | +| **Enums** | PascalCase values | `'pending'` string | `Status.Pending` | + +> **Rule:** If you need a comment to explain a name, rename it. + +### Naming Anti-Patterns + +| Anti-Pattern | Problem | Fix | +|--------------|---------|-----| +| Cryptic abbreviations (`usrMgr`, `cfg`) | Unreadable in 6 months | Spell it out — IDE autocomplete makes long names free | +| Generic names (`data`, `info`, `item`, `handler`) | Says nothing about purpose | Use domain-specific names that reveal intent | +| Misleading names (`getUserList` returns one user) | Actively deceives readers | Match name to behavior, or change the behavior | +| Hungarian notation (`strName`, `nCount`, `IUser`) | Redundant with type system | Let TypeScript/IDE show types; names describe purpose | + +--- + +## Function Rules + +| Rule | Guideline | Why | +|------|-----------|-----| +| **Small** | Max 20 lines, ideally 5-10 | Fits in your head | +| **One Thing** | Does one thing, does it well | Testable and nameable | +| **One Level** | One level of abstraction per function | Readable top to bottom | +| **Few Args** | Max 3 arguments, prefer 0-2 | Easy to call correctly | +| **No Side Effects** | Don't mutate inputs unexpectedly | Predictable behavior | + +### Guard Clauses + +Flatten nested conditionals with early returns. Never nest deeper than 2 levels. + +```typescript +// BAD — 5 levels deep +function processOrder(order: Order) { + if (order) { + if (order.items.length > 0) { + if (order.customer) { + if (order.customer.isVerified) { + return submitOrder(order); + } + } + } + } + throw new Error('Invalid order'); +} + +// GOOD — guard clauses flatten the structure +function processOrder(order: Order) { + if (!order) throw new Error('No order'); + if (!order.items.length) throw new Error('No items'); + if (!order.customer) throw new Error('No customer'); + if (!order.customer.isVerified) throw new Error('Customer not verified'); + + return submitOrder(order); +} +``` + +### Parameter Objects + +When a function needs more than 3 arguments, use an options object. + +```typescript +// BAD — too many parameters, order matters +createUser('John', 'Doe', 'john@example.com', 'secret', 'admin', 'Engineering'); + +// GOOD — self-documenting options object +createUser({ + firstName: 'John', + lastName: 'Doe', + email: 'john@example.com', + password: 'secret', + role: 'admin', + department: 'Engineering', +}); +``` + +--- + +## Code Structure Patterns + +| Pattern | When to Apply | Benefit | +|---------|--------------|---------| +| **Guard Clauses** | Edge cases at function start | Flat, readable flow | +| **Flat > Nested** | Any nesting beyond 2 levels | Reduced cognitive load | +| **Composition** | Complex operations | Small, testable pieces | +| **Colocation** | Related code across files | Easier to find and change | +| **Extract Function** | Comments separating "sections" | Self-documenting code | + +### Composition Over God Functions + +```typescript +// BAD — god function doing everything +async function processOrder(order: Order) { + // Validate... (15 lines) + // Calculate totals... (15 lines) + // Process payment... (10 lines) + // Send notifications... (10 lines) + // Update inventory... (10 lines) + return { success: true }; +} + +// GOOD — composed of small, focused functions +async function processOrder(order: Order) { + validateOrder(order); + const totals = calculateOrderTotals(order); + const payment = await processPayment(order.customer, totals); + await sendOrderConfirmation(order, payment); + await updateInventory(order.items); + return { success: true, orderId: payment.orderId }; +} +``` + +--- + +## Return Type Consistency + +Functions should return consistent types. Use discriminated unions for multiple outcomes. + +```typescript +// BAD — returns different types +function getUser(id: string) { + const user = database.find(id); + if (!user) return false; // boolean + if (user.isDeleted) return null; // null + return user; // User +} + +// GOOD — discriminated union +type GetUserResult = + | { status: 'found'; user: User } + | { status: 'not_found' } + | { status: 'deleted' }; + +function getUser(id: string): GetUserResult { + const user = database.find(id); + if (!user) return { status: 'not_found' }; + if (user.isDeleted) return { status: 'deleted' }; + return { status: 'found', user }; +} +``` + +--- + +## Anti-Patterns + +| Anti-Pattern | Problem | Fix | +|--------------|---------|-----| +| Comment every line | Noise obscures signal | Delete obvious comments; comment *why*, not *what* | +| Helper for one-liner | Unnecessary indirection | Inline the code | +| Factory for 2 objects | Over-engineering | Direct instantiation | +| `utils.ts` with 1 function | Junk drawer file | Put code where it's used | +| Deep nesting | Unreadable flow | Guard clauses and early returns | +| Magic numbers | Unclear intent | Named constants | +| God functions | Untestable, unreadable | Split by responsibility | +| Commented-out code | Dead code confusion | Delete it; git remembers | +| TODO sprawl | Never gets done | Track in issue tracker, not code | +| Premature abstraction | Wrong abstraction is worse than none | Wait for 3+ duplicates before abstracting | +| Copy-paste programming | Duplicated bugs | Extract shared logic | +| Exception-driven control flow | Slow and confusing | Use explicit conditionals | +| Stringly-typed code | Typos and missed cases | Use enums or union types | +| Callback hell | Pyramid of doom | Use async/await | + +--- + +## Pre-Edit Safety Check + +Before changing any file, answer these questions to avoid cascading breakage: + +| Question | Why | +|----------|-----| +| **What imports this file?** | Dependents might break on interface changes | +| **What does this file import?** | You might need to update the contract | +| **What tests cover this?** | Tests might fail — update them alongside code | +| **Is this a shared component?** | Multiple consumers means wider blast radius | + +``` +File to edit: UserService.ts +├── Who imports this? → UserController.ts, AuthController.ts +├── Do they need changes too? → Check function signatures +└── What tests cover this? → UserService.test.ts +``` + +> **Rule:** Edit the file + all dependent files in the SAME task. Never leave broken imports or missing updates. + +--- + +## Self-Check Before Completing + +Before marking any task complete, verify: + +| Check | Question | +|-------|----------| +| **Goal met?** | Did I do exactly what was asked? | +| **Files edited?** | Did I modify all necessary files, including dependents? | +| **Code works?** | Did I verify the change compiles and runs? | +| **No errors?** | Do lint and type checks pass? | +| **Nothing forgotten?** | Any edge cases or dependent files missed? | + +--- + +## NEVER Do + +1. **NEVER add comments that restate the code** — if the code needs a comment to explain *what* it does, rename things until it doesn't +2. **NEVER create abstractions for fewer than 3 use cases** — premature abstraction is worse than duplication +3. **NEVER leave commented-out code in the codebase** — delete it; version control exists for history +4. **NEVER write functions longer than 20 lines** — extract sub-functions until each does one thing +5. **NEVER nest deeper than 2 levels** — use guard clauses, early returns, or extract functions +6. **NEVER use magic numbers or strings** — define named constants with clear semantics +7. **NEVER edit a file without checking what depends on it** — broken imports and missing updates are the most common source of bugs in multi-file changes +8. **NEVER leave a task with failing lint or type checks** — fix all errors before marking complete + +--- + +## References + +Detailed guides for specific clean code topics: + +| Reference | Description | +|-----------|-------------| +| [Anti-Patterns](references/anti-patterns.md) | 21 common mistakes with bad/good code examples across naming, functions, structure, and comments | +| [Code Smells](references/code-smells.md) | Classic code smells catalog with detection patterns — Bloaters, OO Abusers, Change Preventers, Dispensables, Couplers | +| [Refactoring Catalog](references/refactoring-catalog.md) | Essential refactoring patterns with before/after examples and step-by-step mechanics | diff --git a/skills/clean-code-review/_meta.json b/skills/clean-code-review/_meta.json new file mode 100644 index 0000000..2334af3 --- /dev/null +++ b/skills/clean-code-review/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn77z49xfssappp65hpybb9gx180x56e", + "slug": "clean-code-review", + "version": "1.0.0", + "publishedAt": 1770729831880 +} \ No newline at end of file diff --git a/skills/clean-code-review/references/anti-patterns.md b/skills/clean-code-review/references/anti-patterns.md new file mode 100644 index 0000000..7a122d1 --- /dev/null +++ b/skills/clean-code-review/references/anti-patterns.md @@ -0,0 +1,961 @@ +# Anti-Patterns Gallery + +Common coding mistakes with explanations and fixes. Each pattern includes bad/good examples to make code review and refactoring actionable. + +--- + +## Naming Anti-Patterns + +### 1. Cryptic Abbreviations + +**Problem**: Abbreviations save keystrokes but cost readability. Future readers (including yourself) won't remember what `usrMgr` means. + +```typescript +// ❌ Bad +const usrMgr = new UsrMgr(); +const cfg = loadCfg(); +const btn = document.getElementById('sbmt'); +const val = calc(x, y, z); +``` + +```typescript +// ✅ Good +const userManager = new UserManager(); +const config = loadConfig(); +const submitButton = document.getElementById('submit'); +const totalPrice = calculateTotalPrice(quantity, unitPrice, taxRate); +``` + +**Rule**: Spell it out. IDE autocomplete makes long names free. + +--- + +### 2. Generic Names + +**Problem**: Names like `data`, `info`, `item`, `thing`, `manager`, `handler` tell you nothing about what the code does. + +```typescript +// ❌ Bad +function processData(data: any) { + const info = getData(); + const result = handle(info); + return result; +} + +class Manager { + items: any[] = []; + process() { /* ... */ } +} +``` + +```typescript +// ✅ Good +function validateUserRegistration(registration: UserRegistration) { + const existingUser = findUserByEmail(registration.email); + const validationResult = checkEmailAvailability(existingUser); + return validationResult; +} + +class ShoppingCart { + lineItems: CartLineItem[] = []; + calculateTotal() { /* ... */ } +} +``` + +**Rule**: Names should reveal intent. Ask "what does this actually do?" + +--- + +### 3. Misleading Names + +**Problem**: Names that lie are worse than bad names. They actively deceive readers. + +```typescript +// ❌ Bad - name lies about what it does +function getUserList() { + // Actually returns a single user, not a list + return this.currentUser; +} + +const isValid = checkDate(date); // Returns the date, not a boolean + +class AccountList extends Map { } // It's a Map, not a List +``` + +```typescript +// ✅ Good - names match behavior +function getCurrentUser() { + return this.currentUser; +} + +const normalizedDate = normalizeDate(date); + +class AccountRegistry extends Map { } +``` + +**Rule**: If the name doesn't match the behavior, change the name (or the behavior). + +--- + +### 4. Hungarian Notation + +**Problem**: Encoding types in names was useful in weakly-typed languages. TypeScript makes it redundant and noisy. + +```typescript +// ❌ Bad +const strName: string = 'Alice'; +const nCount: number = 42; +const arrUsers: User[] = []; +const bIsActive: boolean = true; +interface IUser { } // "I" prefix for interfaces +type TUserRole = 'admin' | 'user'; // "T" prefix for types +``` + +```typescript +// ✅ Good +const name: string = 'Alice'; +const count: number = 42; +const users: User[] = []; +const isActive: boolean = true; +interface User { } +type UserRole = 'admin' | 'user'; +``` + +**Rule**: Let the type system handle types. Names should describe purpose. + +--- + +## Function Anti-Patterns + +### 5. God Functions + +**Problem**: Functions over 20 lines are hard to understand, test, and modify. They usually do too many things. + +```typescript +// ❌ Bad - 50+ line function doing everything +async function processOrder(order: Order) { + // Validate order + if (!order.items.length) throw new Error('Empty order'); + if (!order.customer) throw new Error('No customer'); + // ... 10 more validation lines + + // Calculate totals + let subtotal = 0; + for (const item of order.items) { + subtotal += item.price * item.quantity; + // ... discount logic + } + // ... 15 more calculation lines + + // Process payment + const paymentResult = await stripe.charge(/* ... */); + // ... 10 more payment lines + + // Send notifications + await sendEmail(/* ... */); + await sendSMS(/* ... */); + // ... more notification logic + + // Update inventory + // ... 10 more lines + + return { success: true }; +} +``` + +```typescript +// ✅ Good - composed of small, focused functions +async function processOrder(order: Order) { + validateOrder(order); + const totals = calculateOrderTotals(order); + const payment = await processPayment(order.customer, totals); + await sendOrderConfirmation(order, payment); + await updateInventory(order.items); + return { success: true, orderId: payment.orderId }; +} + +function validateOrder(order: Order): void { + if (!order.items.length) throw new Error('Empty order'); + if (!order.customer) throw new Error('No customer'); +} + +function calculateOrderTotals(order: Order): OrderTotals { + const subtotal = order.items.reduce( + (sum, item) => sum + item.price * item.quantity, 0 + ); + return { subtotal, tax: subtotal * 0.1, total: subtotal * 1.1 }; +} +``` + +**Rule**: Extract until each function does exactly one thing. + +--- + +### 6. Too Many Parameters + +**Problem**: Functions with 4+ parameters are hard to call correctly and often indicate the function does too much. + +```typescript +// ❌ Bad - too many parameters, order matters +function createUser( + firstName: string, + lastName: string, + email: string, + password: string, + role: string, + department: string, + startDate: Date, + managerId: string | null, + isActive: boolean +) { + // ... +} + +// Callers must remember order +createUser('John', 'Doe', 'john@example.com', 'secret', 'admin', 'Engineering', new Date(), null, true); +``` + +```typescript +// ✅ Good - use an options object +interface CreateUserOptions { + firstName: string; + lastName: string; + email: string; + password: string; + role: UserRole; + department: string; + startDate?: Date; + managerId?: string; + isActive?: boolean; +} + +function createUser(options: CreateUserOptions) { + const { firstName, lastName, email, role, isActive = true } = options; + // ... +} + +// Callers have self-documenting code +createUser({ + firstName: 'John', + lastName: 'Doe', + email: 'john@example.com', + password: 'secret', + role: 'admin', + department: 'Engineering', +}); +``` + +**Rule**: More than 3 parameters? Use an options object. + +--- + +### 7. Boolean Flag Parameters + +**Problem**: Boolean parameters hide branching logic and make function calls unreadable. + +```typescript +// ❌ Bad - what does `true` mean here? +renderButton('Submit', true, false, true); + +function renderButton( + label: string, + isPrimary: boolean, + isDisabled: boolean, + isLoading: boolean +) { + // Complex branching based on booleans +} +``` + +```typescript +// ✅ Good - use options object or separate functions +renderButton({ + label: 'Submit', + variant: 'primary', + state: 'loading', +}); + +// Or separate functions for distinct behaviors +renderPrimaryButton('Submit'); +renderLoadingButton('Submit'); + +// Or use enums +type ButtonVariant = 'primary' | 'secondary' | 'ghost'; +type ButtonState = 'default' | 'loading' | 'disabled'; +``` + +**Rule**: Boolean parameters should be in an options object with named properties. + +--- + +### 8. Side Effects in Getters + +**Problem**: Getters that modify state violate the principle of least surprise. Readers expect getters to be pure. + +```typescript +// ❌ Bad - getter with hidden side effect +class ShoppingCart { + private _items: CartItem[] = []; + private _lastAccessed: Date; + + get items() { + this._lastAccessed = new Date(); // Side effect! + this.logAccess(); // Another side effect! + return this._items; + } + + get totalPrice() { + this.recalculateDiscounts(); // Mutation! + return this._items.reduce((sum, i) => sum + i.price, 0); + } +} +``` + +```typescript +// ✅ Good - getters are pure, side effects are explicit +class ShoppingCart { + private _items: CartItem[] = []; + private _lastAccessed: Date; + + get items() { + return this._items; + } + + get totalPrice() { + return this._items.reduce((sum, i) => sum + i.price, 0); + } + + recordAccess() { + this._lastAccessed = new Date(); + this.logAccess(); + } + + applyDiscounts() { + this.recalculateDiscounts(); + } +} +``` + +**Rule**: Getters should be pure. Make side effects explicit with verbs. + +--- + +### 9. Returning Different Types + +**Problem**: Functions that return different types based on conditions make code unpredictable and hard to type. + +```typescript +// ❌ Bad - return type depends on runtime condition +function getUser(id: string) { + const user = database.find(id); + if (!user) { + return false; // boolean + } + if (user.isDeleted) { + return null; // null + } + return user; // User object +} + +// Caller must handle all cases +const result = getUser('123'); +if (result === false) { /* not found */ } +else if (result === null) { /* deleted */ } +else { /* use result.name */ } +``` + +```typescript +// ✅ Good - consistent return type with discriminated union +type GetUserResult = + | { status: 'found'; user: User } + | { status: 'not_found' } + | { status: 'deleted' }; + +function getUser(id: string): GetUserResult { + const user = database.find(id); + if (!user) { + return { status: 'not_found' }; + } + if (user.isDeleted) { + return { status: 'deleted' }; + } + return { status: 'found', user }; +} + +// Caller has type-safe handling +const result = getUser('123'); +if (result.status === 'found') { + console.log(result.user.name); // TypeScript knows user exists +} +``` + +**Rule**: Return consistent types. Use discriminated unions for multiple outcomes. + +--- + +## Structure Anti-Patterns + +### 10. Deep Nesting (Pyramid of Doom) + +**Problem**: Deeply nested code is hard to follow and usually indicates missing abstractions. + +```typescript +// ❌ Bad - 5 levels deep +function processOrder(order: Order) { + if (order) { + if (order.items.length > 0) { + if (order.customer) { + if (order.customer.isVerified) { + if (order.paymentMethod) { + // Finally, the actual logic buried 5 levels deep + return submitOrder(order); + } else { + throw new Error('No payment method'); + } + } else { + throw new Error('Customer not verified'); + } + } else { + throw new Error('No customer'); + } + } else { + throw new Error('No items'); + } + } else { + throw new Error('No order'); + } +} +``` + +```typescript +// ✅ Good - guard clauses flatten the structure +function processOrder(order: Order) { + if (!order) throw new Error('No order'); + if (!order.items.length) throw new Error('No items'); + if (!order.customer) throw new Error('No customer'); + if (!order.customer.isVerified) throw new Error('Customer not verified'); + if (!order.paymentMethod) throw new Error('No payment method'); + + return submitOrder(order); +} +``` + +**Rule**: Use guard clauses for early returns. Max 2 levels of nesting. + +--- + +### 11. Premature Abstraction + +**Problem**: Creating abstractions before you understand the problem leads to wrong abstractions that are hard to change. + +```typescript +// ❌ Bad - abstraction created for one use case +interface DataFetcher { + fetch(): Promise; + cache(): void; + invalidate(): void; +} + +class GenericRepository implements DataFetcher { + constructor(private adapter: StorageAdapter) {} + // ... complex implementation +} + +// Used exactly once: +const userRepo = new GenericRepository(new UserAdapter()); +``` + +```typescript +// ✅ Good - start simple, abstract when patterns emerge +// First implementation: just fetch users +async function fetchUsers(): Promise { + return await db.query('SELECT * FROM users'); +} + +// Later, if you need caching, add it: +async function fetchUsersWithCache(): Promise { + const cached = cache.get('users'); + if (cached) return cached; + + const users = await db.query('SELECT * FROM users'); + cache.set('users', users); + return users; +} + +// Abstract only when you see the SAME pattern 3+ times +``` + +**Rule**: "Duplication is far cheaper than the wrong abstraction." - Sandi Metz + +--- + +### 12. Over-Engineering + +**Problem**: Building for hypothetical future requirements adds complexity without value. + +```typescript +// ❌ Bad - enterprise FizzBuzz +interface FizzBuzzStrategy { + applies(n: number): boolean; + execute(n: number): string; +} + +class FizzStrategy implements FizzBuzzStrategy { + applies(n: number) { return n % 3 === 0; } + execute() { return 'Fizz'; } +} + +class BuzzStrategy implements FizzBuzzStrategy { + applies(n: number) { return n % 5 === 0; } + execute() { return 'Buzz'; } +} + +class FizzBuzzProcessor { + constructor(private strategies: FizzBuzzStrategy[]) {} + process(n: number): string { + return this.strategies + .filter(s => s.applies(n)) + .map(s => s.execute(n)) + .join('') || String(n); + } +} + +const processor = new FizzBuzzProcessor([ + new FizzStrategy(), + new BuzzStrategy(), +]); +``` + +```typescript +// ✅ Good - solve the actual problem +function fizzBuzz(n: number): string { + if (n % 15 === 0) return 'FizzBuzz'; + if (n % 3 === 0) return 'Fizz'; + if (n % 5 === 0) return 'Buzz'; + return String(n); +} +``` + +**Rule**: Solve today's problem. Refactor when requirements actually change. + +--- + +### 13. Copy-Paste Programming + +**Problem**: Duplicated code means duplicated bugs and maintenance burden. + +```typescript +// ❌ Bad - same validation logic copied +function createUser(data: UserInput) { + if (!data.email) throw new Error('Email required'); + if (!data.email.includes('@')) throw new Error('Invalid email'); + if (data.email.length > 255) throw new Error('Email too long'); + // ... create user +} + +function updateUser(id: string, data: UserInput) { + if (!data.email) throw new Error('Email required'); + if (!data.email.includes('@')) throw new Error('Invalid email'); + if (data.email.length > 255) throw new Error('Email too long'); + // ... update user +} + +function inviteUser(data: UserInput) { + if (!data.email) throw new Error('Email required'); + if (!data.email.includes('@')) throw new Error('Invalid email'); + if (data.email.length > 255) throw new Error('Email too long'); + // ... invite user +} +``` + +```typescript +// ✅ Good - extract shared logic +function validateEmail(email: string): void { + if (!email) throw new Error('Email required'); + if (!email.includes('@')) throw new Error('Invalid email'); + if (email.length > 255) throw new Error('Email too long'); +} + +function createUser(data: UserInput) { + validateEmail(data.email); + // ... create user +} + +function updateUser(id: string, data: UserInput) { + validateEmail(data.email); + // ... update user +} + +function inviteUser(data: UserInput) { + validateEmail(data.email); + // ... invite user +} +``` + +**Rule**: If you copy-paste, you're probably missing an abstraction. + +--- + +### 14. God Objects + +**Problem**: Classes that know too much and do too much become unmaintainable. + +```typescript +// ❌ Bad - class does everything +class ApplicationManager { + users: User[] = []; + orders: Order[] = []; + products: Product[] = []; + + // User operations + createUser() { /* ... */ } + deleteUser() { /* ... */ } + authenticateUser() { /* ... */ } + + // Order operations + createOrder() { /* ... */ } + cancelOrder() { /* ... */ } + refundOrder() { /* ... */ } + + // Product operations + addProduct() { /* ... */ } + updateInventory() { /* ... */ } + + // Reporting + generateSalesReport() { /* ... */ } + generateUserReport() { /* ... */ } + + // Notifications + sendEmail() { /* ... */ } + sendSMS() { /* ... */ } +} +``` + +```typescript +// ✅ Good - separate concerns +class UserService { + createUser() { /* ... */ } + deleteUser() { /* ... */ } +} + +class AuthService { + authenticate() { /* ... */ } +} + +class OrderService { + createOrder() { /* ... */ } + cancelOrder() { /* ... */ } +} + +class NotificationService { + sendEmail() { /* ... */ } + sendSMS() { /* ... */ } +} +``` + +**Rule**: Each class should have one reason to change. + +--- + +## Comment Anti-Patterns + +### 15. Commented-Out Code + +**Problem**: Commented code is dead code. It confuses readers and never gets cleaned up. + +```typescript +// ❌ Bad +function calculateTotal(items: Item[]) { + let total = 0; + for (const item of items) { + total += item.price; + // total += item.price * item.quantity; // Old calculation + // if (item.discount) { + // total -= item.discount; + // } + } + // return total * 1.1; // With tax + // return total * 1.08; // Old tax rate + return total; +} +``` + +```typescript +// ✅ Good - delete it, git remembers +function calculateTotal(items: Item[]) { + return items.reduce((total, item) => total + item.price, 0); +} +``` + +**Rule**: Delete commented code. Use version control for history. + +--- + +### 16. Obvious Comments + +**Problem**: Comments that restate the code add noise without value. + +```typescript +// ❌ Bad - comments that add nothing +// Increment counter +counter++; + +// Check if user is null +if (user === null) { + // Return early + return; +} + +// Loop through all items +for (const item of items) { + // Add item price to total + total += item.price; +} +``` + +```typescript +// ✅ Good - code is self-documenting, comments explain WHY +counter++; // No comment needed + +if (!user) return; + +const total = items.reduce((sum, item) => sum + item.price, 0); + +// Business rule: Premium members get early access 48 hours before launch +if (user.isPremium && hoursUntilLaunch < 48) { + showEarlyAccess(); +} +``` + +**Rule**: Don't comment WHAT, comment WHY (if not obvious). + +--- + +### 17. TODO Sprawl + +**Problem**: TODOs accumulate and never get done. They become invisible noise. + +```typescript +// ❌ Bad - TODO graveyard +function processPayment(amount: number) { + // TODO: Add retry logic + // TODO: Handle currency conversion + // TODO: Add logging + // TODO: Optimize this (added 2019) + // FIXME: This is broken sometimes + // HACK: Temporary fix, remove later + // XXX: Why does this work? + return charge(amount); +} +``` + +```typescript +// ✅ Good - TODOs are tracked in issues, not code +function processPayment(amount: number) { + // See JIRA-1234 for planned retry logic + return charge(amount); +} + +// Or just fix it now: +async function processPayment(amount: number) { + return await retry(() => charge(amount), { attempts: 3 }); +} +``` + +**Rule**: TODOs belong in your issue tracker, not your code. + +--- + +### 18. Outdated Comments + +**Problem**: Comments that contradict the code are actively harmful. + +```typescript +// ❌ Bad - comment lies about the code +// Returns the user's full name (first + last) +function getUserName(user: User): string { + return user.email; // Actually returns email! +} + +// Validates and saves the user +function processUser(user: User) { + // No validation, just saves + database.save(user); +} + +// This function is deprecated, use newFunction() instead +function oldFunction() { + // Still actively used throughout codebase +} +``` + +```typescript +// ✅ Good - update or remove outdated comments +function getUserEmail(user: User): string { + return user.email; +} + +function saveUser(user: User) { + database.save(user); +} + +/** @deprecated Use {@link newFunction} instead */ +function oldFunction() { + console.warn('oldFunction is deprecated'); + return newFunction(); +} +``` + +**Rule**: When you change code, update or delete related comments. + +--- + +## Control Flow Anti-Patterns + +### 19. Exception-Driven Control Flow + +**Problem**: Using exceptions for normal control flow is slow and hard to follow. + +```typescript +// ❌ Bad - exceptions for expected cases +function findUser(id: string): User { + try { + return database.getUser(id); + } catch { + try { + return cache.getUser(id); + } catch { + try { + return createDefaultUser(id); + } catch { + throw new Error('Cannot get user'); + } + } + } +} +``` + +```typescript +// ✅ Good - explicit control flow +function findUser(id: string): User | null { + const dbUser = database.getUser(id); + if (dbUser) return dbUser; + + const cachedUser = cache.getUser(id); + if (cachedUser) return cachedUser; + + return createDefaultUser(id); +} +``` + +**Rule**: Exceptions are for exceptional situations, not control flow. + +--- + +### 20. Stringly-Typed Code + +**Problem**: Using strings where enums or types would be safer leads to typos and missing cases. + +```typescript +// ❌ Bad - magic strings everywhere +function handleStatus(status: string) { + if (status === 'pending') { /* ... */ } + else if (status === 'Pending') { /* ... */ } // Typo variant + else if (status === 'active') { /* ... */ } + else if (status === 'actve') { /* ... */ } // Typo! +} + +user.role = 'admni'; // Typo, no error! +``` + +```typescript +// ✅ Good - type-safe enums or unions +type Status = 'pending' | 'active' | 'completed' | 'cancelled'; +type UserRole = 'admin' | 'user' | 'guest'; + +function handleStatus(status: Status) { + switch (status) { + case 'pending': /* ... */ break; + case 'active': /* ... */ break; + case 'completed': /* ... */ break; + case 'cancelled': /* ... */ break; + // TypeScript ensures exhaustive handling + } +} + +user.role = 'admni'; // TypeScript error! +``` + +**Rule**: Use types instead of strings for fixed sets of values. + +--- + +### 21. Callback Hell + +**Problem**: Nested callbacks create unreadable, hard-to-debug code. + +```typescript +// ❌ Bad - callback pyramid +getUser(userId, (err, user) => { + if (err) return handleError(err); + getOrders(user.id, (err, orders) => { + if (err) return handleError(err); + getOrderDetails(orders[0].id, (err, details) => { + if (err) return handleError(err); + processDetails(details, (err, result) => { + if (err) return handleError(err); + sendNotification(result, (err) => { + if (err) return handleError(err); + console.log('Done!'); + }); + }); + }); + }); +}); +``` + +```typescript +// ✅ Good - async/await +async function processUserOrder(userId: string) { + try { + const user = await getUser(userId); + const orders = await getOrders(user.id); + const details = await getOrderDetails(orders[0].id); + const result = await processDetails(details); + await sendNotification(result); + console.log('Done!'); + } catch (error) { + handleError(error); + } +} +``` + +**Rule**: Use async/await for asynchronous code. + +--- + +## Quick Reference + +| Anti-Pattern | Fix | +|--------------|-----| +| Cryptic abbreviations | Spell it out | +| Generic names | Reveal intent | +| Misleading names | Match name to behavior | +| Hungarian notation | Let types handle types | +| God functions | Extract smaller functions | +| Too many parameters | Use options object | +| Boolean flags | Named options or separate functions | +| Side effects in getters | Make mutations explicit | +| Different return types | Use discriminated unions | +| Deep nesting | Guard clauses | +| Premature abstraction | Wait for patterns to emerge | +| Over-engineering | Solve today's problem | +| Copy-paste | Extract shared logic | +| God objects | Single responsibility | +| Commented-out code | Delete it | +| Obvious comments | Let code self-document | +| TODO sprawl | Use issue tracker | +| Outdated comments | Update or delete | +| Exception control flow | Explicit conditionals | +| Stringly-typed | Use enums/unions | +| Callback hell | async/await | diff --git a/skills/clean-code-review/references/code-smells.md b/skills/clean-code-review/references/code-smells.md new file mode 100644 index 0000000..8fa09c7 --- /dev/null +++ b/skills/clean-code-review/references/code-smells.md @@ -0,0 +1,1076 @@ +# Code Smells Catalog + +Code smells are symptoms that indicate deeper problems. They're not bugs—the code works—but they signal design issues that make code harder to understand, change, and maintain. + +Based on Martin Fowler's refactoring catalog with TypeScript examples. + +--- + +## Bloaters + +Code that has grown too large to work with effectively. + +### Long Method + +**Symptoms**: +- Function exceeds 20 lines +- Multiple levels of abstraction in one function +- Comments separating "sections" within a function +- Difficult to name—does too many things + +```typescript +// ❌ Smell: Long method with multiple responsibilities +async function processCheckout(cart: Cart, user: User) { + // Validate cart + if (!cart.items.length) throw new Error('Empty cart'); + for (const item of cart.items) { + const product = await getProduct(item.productId); + if (product.stock < item.quantity) { + throw new Error(`Insufficient stock for ${product.name}`); + } + } + + // Calculate totals + let subtotal = 0; + for (const item of cart.items) { + const product = await getProduct(item.productId); + subtotal += product.price * item.quantity; + } + const tax = subtotal * 0.1; + const shipping = subtotal > 100 ? 0 : 10; + const total = subtotal + tax + shipping; + + // Process payment + const paymentIntent = await stripe.paymentIntents.create({ + amount: Math.round(total * 100), + currency: 'usd', + }); + + // Create order + const order = await db.orders.create({ + userId: user.id, + items: cart.items, + total, + paymentIntentId: paymentIntent.id, + }); + + // Send confirmation + await sendEmail(user.email, 'Order Confirmed', `Order #${order.id}`); + + // Update inventory + for (const item of cart.items) { + await db.products.update(item.productId, { + stock: { decrement: item.quantity }, + }); + } + + return order; +} +``` + +**Refactoring**: Extract Method + +```typescript +// ✅ Refactored: Each function does one thing +async function processCheckout(cart: Cart, user: User) { + await validateCart(cart); + const totals = await calculateTotals(cart); + const payment = await processPayment(totals.total); + const order = await createOrder(user, cart, totals, payment); + await sendOrderConfirmation(user, order); + await updateInventory(cart.items); + return order; +} + +async function validateCart(cart: Cart): Promise { + if (!cart.items.length) throw new Error('Empty cart'); + for (const item of cart.items) { + const product = await getProduct(item.productId); + if (product.stock < item.quantity) { + throw new Error(`Insufficient stock for ${product.name}`); + } + } +} + +async function calculateTotals(cart: Cart): Promise { + const subtotal = await calculateSubtotal(cart.items); + const tax = subtotal * 0.1; + const shipping = subtotal > 100 ? 0 : 10; + return { subtotal, tax, shipping, total: subtotal + tax + shipping }; +} +``` + +**Prevention**: If you're adding a comment to separate sections, extract a function instead. + +--- + +### Large Class + +**Symptoms**: +- Class has 10+ methods +- Class has 10+ fields +- Class name includes "Manager", "Processor", "Handler" doing everything +- You can't summarize what the class does in one sentence + +```typescript +// ❌ Smell: God class that does everything +class UserManager { + // User CRUD + createUser(data: UserInput) { /* ... */ } + updateUser(id: string, data: UserInput) { /* ... */ } + deleteUser(id: string) { /* ... */ } + getUser(id: string) { /* ... */ } + listUsers(filters: UserFilters) { /* ... */ } + + // Authentication + login(email: string, password: string) { /* ... */ } + logout(userId: string) { /* ... */ } + resetPassword(email: string) { /* ... */ } + verifyEmail(token: string) { /* ... */ } + + // Authorization + checkPermission(userId: string, resource: string) { /* ... */ } + assignRole(userId: string, role: string) { /* ... */ } + + // Profile + updateProfile(userId: string, profile: Profile) { /* ... */ } + uploadAvatar(userId: string, file: File) { /* ... */ } + + // Notifications + sendWelcomeEmail(userId: string) { /* ... */ } + sendPasswordResetEmail(email: string) { /* ... */ } + + // Analytics + trackLogin(userId: string) { /* ... */ } + getLoginHistory(userId: string) { /* ... */ } +} +``` + +**Refactoring**: Extract Class + +```typescript +// ✅ Refactored: Single Responsibility +class UserRepository { + create(data: UserInput) { /* ... */ } + update(id: string, data: UserInput) { /* ... */ } + delete(id: string) { /* ... */ } + findById(id: string) { /* ... */ } + findMany(filters: UserFilters) { /* ... */ } +} + +class AuthService { + login(email: string, password: string) { /* ... */ } + logout(userId: string) { /* ... */ } + resetPassword(email: string) { /* ... */ } + verifyEmail(token: string) { /* ... */ } +} + +class AuthorizationService { + checkPermission(userId: string, resource: string) { /* ... */ } + assignRole(userId: string, role: string) { /* ... */ } +} + +class ProfileService { + updateProfile(userId: string, profile: Profile) { /* ... */ } + uploadAvatar(userId: string, file: File) { /* ... */ } +} + +class UserNotificationService { + sendWelcomeEmail(userId: string) { /* ... */ } + sendPasswordResetEmail(email: string) { /* ... */ } +} +``` + +**Prevention**: Each class should have one reason to change. + +--- + +### Long Parameter List + +**Symptoms**: +- Function has 4+ parameters +- Parameters are often passed together +- Hard to remember parameter order +- Boolean parameters with unclear meaning + +```typescript +// ❌ Smell: Too many parameters +function createReport( + title: string, + startDate: Date, + endDate: Date, + format: string, + includeCharts: boolean, + includeTables: boolean, + groupBy: string, + sortBy: string, + sortOrder: string, + limit: number, + userId: string +) { + // ... +} +``` + +**Refactoring**: Introduce Parameter Object + +```typescript +// ✅ Refactored: Options object +interface ReportOptions { + title: string; + dateRange: { + start: Date; + end: Date; + }; + format: 'pdf' | 'excel' | 'csv'; + includes?: { + charts?: boolean; + tables?: boolean; + }; + sorting?: { + field: string; + order: 'asc' | 'desc'; + }; + groupBy?: string; + limit?: number; + userId: string; +} + +function createReport(options: ReportOptions) { + const { title, dateRange, format, includes, sorting, limit = 100 } = options; + // ... +} +``` + +**Prevention**: If parameters travel together, they belong together. + +--- + +### Data Clumps + +**Symptoms**: +- Same 3+ fields appear together in multiple places +- Functions pass the same group of parameters +- Classes have fields that are always used together + +```typescript +// ❌ Smell: Same fields everywhere +function calculateDistance( + startLat: number, startLng: number, + endLat: number, endLng: number +) { /* ... */ } + +function formatAddress( + street: string, city: string, state: string, zip: string +) { /* ... */ } + +class Delivery { + pickupStreet: string; + pickupCity: string; + pickupState: string; + pickupZip: string; + + dropoffStreet: string; + dropoffCity: string; + dropoffState: string; + dropoffZip: string; +} +``` + +**Refactoring**: Extract Class + +```typescript +// ✅ Refactored: Create value objects +interface Coordinates { + latitude: number; + longitude: number; +} + +interface Address { + street: string; + city: string; + state: string; + zip: string; +} + +function calculateDistance(start: Coordinates, end: Coordinates) { /* ... */ } +function formatAddress(address: Address) { /* ... */ } + +class Delivery { + pickup: Address; + dropoff: Address; +} +``` + +**Prevention**: When you see fields traveling together, introduce a class. + +--- + +### Primitive Obsession + +**Symptoms**: +- Using strings/numbers where a class would be clearer +- Validation logic repeated for the same concept +- Magic strings or numbers scattered in code + +```typescript +// ❌ Smell: Primitives everywhere +function processOrder( + userId: string, // Could be UserId + email: string, // Could be Email + phone: string, // Could be PhoneNumber + amount: number, // Could be Money + currency: string, // Could be Currency + status: string // Could be OrderStatus +) { + // Email validation repeated everywhere + if (!email.includes('@')) throw new Error('Invalid email'); + + // Currency logic scattered + if (currency === 'USD') { /* ... */ } + else if (currency === 'EUR') { /* ... */ } + + // Status checks using magic strings + if (status === 'pending') { /* ... */ } +} +``` + +**Refactoring**: Replace Primitive with Object + +```typescript +// ✅ Refactored: Domain types encapsulate rules +class Email { + private constructor(private readonly value: string) {} + + static create(value: string): Email { + if (!value.includes('@')) throw new Error('Invalid email'); + return new Email(value.toLowerCase()); + } + + toString() { return this.value; } +} + +class Money { + constructor( + private readonly amount: number, + private readonly currency: Currency + ) {} + + add(other: Money): Money { + if (this.currency !== other.currency) { + throw new Error('Currency mismatch'); + } + return new Money(this.amount + other.amount, this.currency); + } +} + +type OrderStatus = 'pending' | 'confirmed' | 'shipped' | 'delivered'; + +function processOrder( + userId: UserId, + email: Email, + amount: Money, + status: OrderStatus +) { + // No validation needed—types enforce rules +} +``` + +**Prevention**: If you validate a primitive the same way multiple times, wrap it. + +--- + +## Object-Orientation Abusers + +Incorrect or incomplete application of OO principles. + +### Switch Statements + +**Symptoms**: +- Same switch/if-else chain in multiple places +- Adding a new case requires changes in multiple files +- Switches based on type codes + +```typescript +// ❌ Smell: Type-based switching repeated everywhere +function calculatePay(employee: Employee): number { + switch (employee.type) { + case 'hourly': + return employee.hoursWorked * employee.hourlyRate; + case 'salaried': + return employee.annualSalary / 12; + case 'commissioned': + return employee.baseSalary + employee.sales * employee.commissionRate; + default: + throw new Error('Unknown employee type'); + } +} + +function getTimeOffDays(employee: Employee): number { + switch (employee.type) { + case 'hourly': return 10; + case 'salaried': return 20; + case 'commissioned': return 15; + default: return 0; + } +} +``` + +**Refactoring**: Replace Conditional with Polymorphism + +```typescript +// ✅ Refactored: Polymorphism +interface Employee { + calculatePay(): number; + getTimeOffDays(): number; +} + +class HourlyEmployee implements Employee { + constructor( + private hoursWorked: number, + private hourlyRate: number + ) {} + + calculatePay() { return this.hoursWorked * this.hourlyRate; } + getTimeOffDays() { return 10; } +} + +class SalariedEmployee implements Employee { + constructor(private annualSalary: number) {} + + calculatePay() { return this.annualSalary / 12; } + getTimeOffDays() { return 20; } +} + +class CommissionedEmployee implements Employee { + constructor( + private baseSalary: number, + private sales: number, + private commissionRate: number + ) {} + + calculatePay() { + return this.baseSalary + this.sales * this.commissionRate; + } + getTimeOffDays() { return 15; } +} +``` + +**Prevention**: If switches on type appear in multiple places, use polymorphism. + +--- + +### Temporary Field + +**Symptoms**: +- Fields that are only set in certain situations +- Null checks scattered throughout the class +- Fields used by only some methods + +```typescript +// ❌ Smell: Fields only valid sometimes +class Order { + items: OrderItem[]; + customer: Customer; + + // Only set during discount calculation + discountPercentage?: number; + discountReason?: string; + discountApprover?: string; + + // Only set after shipping + trackingNumber?: string; + carrier?: string; + estimatedDelivery?: Date; + + calculateTotal() { + let total = this.items.reduce((sum, i) => sum + i.price, 0); + // Null check because field might not exist + if (this.discountPercentage) { + total *= (1 - this.discountPercentage / 100); + } + return total; + } +} +``` + +**Refactoring**: Extract Class or Introduce Null Object + +```typescript +// ✅ Refactored: Separate concerns +class Order { + items: OrderItem[]; + customer: Customer; + discount: Discount = Discount.none(); + shipping?: ShippingInfo; + + calculateTotal() { + const subtotal = this.items.reduce((sum, i) => sum + i.price, 0); + return this.discount.applyTo(subtotal); + } +} + +class Discount { + private constructor( + private percentage: number, + private reason: string, + private approver: string | null + ) {} + + static none() { return new Discount(0, '', null); } + + static create(percentage: number, reason: string, approver: string) { + return new Discount(percentage, reason, approver); + } + + applyTo(amount: number) { + return amount * (1 - this.percentage / 100); + } +} + +interface ShippingInfo { + trackingNumber: string; + carrier: string; + estimatedDelivery: Date; +} +``` + +**Prevention**: If fields are only used together, extract them to a class. + +--- + +## Change Preventers + +Code structures that make changes difficult. + +### Divergent Change + +**Symptoms**: +- One class is modified for multiple unrelated reasons +- Changes in different domains affect the same file +- "I need to change X, Y, and Z in this file" + +```typescript +// ❌ Smell: Class changes for different reasons +class ReportGenerator { + // Changes when report format changes + generatePDF(data: ReportData) { /* ... */ } + generateExcel(data: ReportData) { /* ... */ } + generateCSV(data: ReportData) { /* ... */ } + + // Changes when data source changes + fetchFromDatabase(query: string) { /* ... */ } + fetchFromAPI(endpoint: string) { /* ... */ } + + // Changes when business logic changes + calculateTotals(data: ReportData) { /* ... */ } + applyFilters(data: ReportData) { /* ... */ } +} +``` + +**Refactoring**: Extract Class + +```typescript +// ✅ Refactored: Separate by reason for change +class ReportFormatter { + toPDF(data: ReportData) { /* ... */ } + toExcel(data: ReportData) { /* ... */ } + toCSV(data: ReportData) { /* ... */ } +} + +class DataFetcher { + fromDatabase(query: string) { /* ... */ } + fromAPI(endpoint: string) { /* ... */ } +} + +class ReportCalculator { + calculateTotals(data: ReportData) { /* ... */ } + applyFilters(data: ReportData) { /* ... */ } +} + +class ReportGenerator { + constructor( + private fetcher: DataFetcher, + private calculator: ReportCalculator, + private formatter: ReportFormatter + ) {} + + generate(source: DataSource, format: Format): Report { + const data = this.fetcher.fetch(source); + const processed = this.calculator.process(data); + return this.formatter.format(processed, format); + } +} +``` + +**Prevention**: Each class should have one reason to change. + +--- + +### Shotgun Surgery + +**Symptoms**: +- A single change requires editing many files +- Related code is scattered across the codebase +- Easy to miss one of the required changes + +```typescript +// ❌ Smell: Adding a field requires changes everywhere +// user.ts +interface User { name: string; email: string; } + +// api/users.ts +app.post('/users', (req) => { + const { name, email } = req.body; + // ... +}); + +// validation.ts +function validateUser(data: unknown) { + if (!data.name) throw new Error('Name required'); + if (!data.email) throw new Error('Email required'); +} + +// database/user-repo.ts +function createUser(name: string, email: string) { + db.query('INSERT INTO users (name, email) VALUES (?, ?)', [name, email]); +} + +// Adding 'phone' requires changes in 4+ files! +``` + +**Refactoring**: Move Method, Inline Class + +```typescript +// ✅ Refactored: Centralize related logic +// user.ts - Single source of truth +interface User { + name: string; + email: string; + phone?: string; +} + +const UserSchema = z.object({ + name: z.string().min(1), + email: z.string().email(), + phone: z.string().optional(), +}); + +class UserRepository { + create(data: User) { + const validated = UserSchema.parse(data); + return db.users.create(validated); + } +} + +// api/users.ts - Just orchestration +app.post('/users', async (req) => { + const user = await userRepository.create(req.body); + return user; +}); +``` + +**Prevention**: Keep related behavior together. + +--- + +### Feature Envy + +**Symptoms**: +- A method uses more features of another class than its own +- Lots of getter calls on other objects +- Logic that should belong to the data it operates on + +```typescript +// ❌ Smell: OrderPrinter is too interested in Order +class OrderPrinter { + print(order: Order) { + console.log(`Order: ${order.getId()}`); + console.log(`Customer: ${order.getCustomer().getName()}`); + console.log(`Address: ${order.getCustomer().getAddress().getFullAddress()}`); + + let total = 0; + for (const item of order.getItems()) { + const price = item.getProduct().getPrice(); + const qty = item.getQuantity(); + const discount = item.getProduct().getDiscount(); + const lineTotal = price * qty * (1 - discount); + total += lineTotal; + console.log(`${item.getProduct().getName()}: $${lineTotal}`); + } + + console.log(`Total: $${total}`); + } +} +``` + +**Refactoring**: Move Method + +```typescript +// ✅ Refactored: Move logic to where data lives +class Order { + getFormattedSummary(): string { + return [ + `Order: ${this.id}`, + `Customer: ${this.customer.name}`, + `Address: ${this.customer.address.format()}`, + ...this.items.map(item => item.format()), + `Total: $${this.calculateTotal()}`, + ].join('\n'); + } + + calculateTotal(): number { + return this.items.reduce((sum, item) => sum + item.calculateLineTotal(), 0); + } +} + +class OrderItem { + calculateLineTotal(): number { + return this.product.price * this.quantity * (1 - this.product.discount); + } + + format(): string { + return `${this.product.name}: $${this.calculateLineTotal()}`; + } +} + +class OrderPrinter { + print(order: Order) { + console.log(order.getFormattedSummary()); + } +} +``` + +**Prevention**: Put behavior with the data it needs. + +--- + +## Dispensables + +Code that serves no purpose and should be removed. + +### Dead Code + +**Symptoms**: +- Unreachable code after return/throw +- Unused variables, parameters, or functions +- Commented-out code +- Obsolete feature flags + +```typescript +// ❌ Smell: Code that's never executed +function processPayment(amount: number) { + if (amount <= 0) { + throw new Error('Invalid amount'); + console.log('This never runs'); // Dead code + } + + const result = charge(amount); + return result; + + // Everything below is dead code + sendReceipt(result); + updateAnalytics(result); +} + +// Unused function +function legacyPaymentProcessor() { + // Old code nobody uses +} + +// Unused variable +const FEATURE_FLAG_OLD = false; +``` + +**Refactoring**: Remove Dead Code + +```typescript +// ✅ Refactored: Delete it +function processPayment(amount: number) { + if (amount <= 0) { + throw new Error('Invalid amount'); + } + + const result = charge(amount); + sendReceipt(result); + updateAnalytics(result); + return result; +} +``` + +**Prevention**: Use your IDE's "find unused" feature regularly. + +--- + +### Speculative Generality + +**Symptoms**: +- Abstract classes with only one implementation +- Parameters or methods that are never used +- "We might need this someday" code +- Complex framework for simple problem + +```typescript +// ❌ Smell: Over-engineered for imaginary requirements +interface PaymentProcessor { + process(payment: Payment): Promise; + refund(paymentId: string): Promise; + void(paymentId: string): Promise; + partialRefund(paymentId: string, amount: number): Promise; + recurring(subscription: Subscription): Promise; +} + +// Only implementation +class StripeProcessor implements PaymentProcessor { + // We only use process() and refund() + // Other methods throw "not implemented" + process(payment: Payment) { /* ... */ } + refund(paymentId: string) { /* ... */ } + void() { throw new Error('Not implemented'); } + partialRefund() { throw new Error('Not implemented'); } + recurring() { throw new Error('Not implemented'); } +} +``` + +**Refactoring**: Collapse Hierarchy, Remove Parameter + +```typescript +// ✅ Refactored: Only what's actually needed +class PaymentService { + constructor(private stripe: Stripe) {} + + async charge(amount: number, customer: string) { + return this.stripe.charges.create({ amount, customer }); + } + + async refund(chargeId: string) { + return this.stripe.refunds.create({ charge: chargeId }); + } +} +``` + +**Prevention**: YAGNI—You Aren't Gonna Need It. + +--- + +### Duplicate Code + +**Symptoms**: +- Same code structure in multiple places +- Copy-pasted code with minor variations +- Parallel inheritance hierarchies + +```typescript +// ❌ Smell: Same logic in multiple places +class AdminController { + async getUsers(req: Request) { + const page = parseInt(req.query.page as string) || 1; + const limit = parseInt(req.query.limit as string) || 10; + const offset = (page - 1) * limit; + + const users = await db.users.findMany({ skip: offset, take: limit }); + const total = await db.users.count(); + + return { + data: users, + meta: { page, limit, total, pages: Math.ceil(total / limit) } + }; + } +} + +class ProductController { + async getProducts(req: Request) { + const page = parseInt(req.query.page as string) || 1; + const limit = parseInt(req.query.limit as string) || 10; + const offset = (page - 1) * limit; + + const products = await db.products.findMany({ skip: offset, take: limit }); + const total = await db.products.count(); + + return { + data: products, + meta: { page, limit, total, pages: Math.ceil(total / limit) } + }; + } +} +``` + +**Refactoring**: Extract Function, Extract Class + +```typescript +// ✅ Refactored: Shared pagination logic +interface PaginationParams { + page: number; + limit: number; +} + +function parsePagination(query: Record): PaginationParams { + return { + page: parseInt(query.page) || 1, + limit: parseInt(query.limit) || 10, + }; +} + +async function paginate( + query: { findMany: Function; count: Function }, + params: PaginationParams +) { + const { page, limit } = params; + const offset = (page - 1) * limit; + + const [data, total] = await Promise.all([ + query.findMany({ skip: offset, take: limit }), + query.count(), + ]); + + return { + data, + meta: { page, limit, total, pages: Math.ceil(total / limit) } + }; +} + +class AdminController { + async getUsers(req: Request) { + return paginate(db.users, parsePagination(req.query)); + } +} + +class ProductController { + async getProducts(req: Request) { + return paginate(db.products, parsePagination(req.query)); + } +} +``` + +**Prevention**: If you copy-paste, extract immediately. + +--- + +## Couplers + +Code with excessive coupling between classes. + +### Message Chains + +**Symptoms**: +- Long chains of method calls: `a.getB().getC().getD().doSomething()` +- Client depends on navigation structure +- Changes to intermediate objects break clients + +```typescript +// ❌ Smell: Reaching through objects +function getManagerEmail(employee: Employee): string { + return employee + .getDepartment() + .getManager() + .getContactInfo() + .getEmail() + .toLowerCase(); +} + +function sendReport(order: Order) { + const warehouse = order + .getShipment() + .getRoute() + .getDestination() + .getWarehouse(); + + warehouse.receive(order); +} +``` + +**Refactoring**: Hide Delegate + +```typescript +// ✅ Refactored: Ask, don't navigate +class Employee { + getManagerEmail(): string { + return this.department.getManagerEmail(); + } +} + +class Department { + getManagerEmail(): string { + return this.manager.email.toLowerCase(); + } +} + +// Client is simple +function getManagerEmail(employee: Employee): string { + return employee.getManagerEmail(); +} + +// Or use delegation +class Order { + getDestinationWarehouse(): Warehouse { + return this.shipment.getDestinationWarehouse(); + } +} +``` + +**Prevention**: Follow the Law of Demeter—only talk to immediate friends. + +--- + +### Middle Man + +**Symptoms**: +- Class mostly delegates to another class +- No added value, just passing through +- Interface mirrors another class + +```typescript +// ❌ Smell: Pure delegation +class PersonWrapper { + private person: Person; + + getName() { return this.person.getName(); } + setName(name: string) { this.person.setName(name); } + getAge() { return this.person.getAge(); } + setAge(age: number) { this.person.setAge(age); } + getAddress() { return this.person.getAddress(); } + setAddress(addr: Address) { this.person.setAddress(addr); } + // Every method just delegates +} +``` + +**Refactoring**: Remove Middle Man + +```typescript +// ✅ Refactored: Use the class directly +// Delete PersonWrapper, use Person directly + +// If some delegation is valuable, keep only that: +class PersonView { + constructor(private person: Person) {} + + // Only expose what's needed, add value + getDisplayName(): string { + return `${this.person.getName()} (${this.person.getAge()})`; + } +} +``` + +**Prevention**: Only add indirection when it provides value. + +--- + +## Quick Reference + +| Category | Smell | Fix | +|----------|-------|-----| +| **Bloaters** | Long Method | Extract Method | +| | Large Class | Extract Class | +| | Long Parameter List | Parameter Object | +| | Data Clumps | Extract Class | +| | Primitive Obsession | Replace with Object | +| **OO Abusers** | Switch Statements | Polymorphism | +| | Temporary Field | Extract Class | +| **Change Preventers** | Divergent Change | Extract Class | +| | Shotgun Surgery | Move Method | +| | Feature Envy | Move Method | +| **Dispensables** | Dead Code | Delete it | +| | Speculative Generality | Delete it | +| | Duplicate Code | Extract | +| **Couplers** | Message Chains | Hide Delegate | +| | Middle Man | Remove it | diff --git a/skills/clean-code-review/references/refactoring-catalog.md b/skills/clean-code-review/references/refactoring-catalog.md new file mode 100644 index 0000000..3027fc9 --- /dev/null +++ b/skills/clean-code-review/references/refactoring-catalog.md @@ -0,0 +1,959 @@ +# Refactoring Catalog + +Essential refactoring patterns with before/after examples. Each refactoring is a small, reversible transformation that improves code structure without changing behavior. + +Based on Martin Fowler's refactoring catalog with TypeScript examples. + +--- + +## Composing Methods + +Break down large methods into smaller, focused pieces. + +### Extract Function + +**Motivation**: A code fragment that can be grouped together and named. The most common refactoring. + +**When to use**: +- Code block has a comment explaining what it does +- Same code appears in multiple places +- Function is too long (20+ lines) +- Logic is at a different level of abstraction + +```typescript +// Before +function printInvoice(invoice: Invoice) { + console.log('================='); + console.log('=== INVOICE ====='); + console.log('================='); + + // Print details + console.log(`Customer: ${invoice.customer}`); + console.log(`Date: ${invoice.date}`); + + // Calculate total + let total = 0; + for (const item of invoice.items) { + total += item.price * item.quantity; + } + console.log(`Total: $${total}`); + + console.log('================='); +} +``` + +```typescript +// After +function printInvoice(invoice: Invoice) { + printHeader(); + printDetails(invoice); + printTotal(invoice); + printFooter(); +} + +function printHeader() { + console.log('================='); + console.log('=== INVOICE ====='); + console.log('================='); +} + +function printDetails(invoice: Invoice) { + console.log(`Customer: ${invoice.customer}`); + console.log(`Date: ${invoice.date}`); +} + +function printTotal(invoice: Invoice) { + const total = calculateTotal(invoice.items); + console.log(`Total: $${total}`); +} + +function calculateTotal(items: InvoiceItem[]): number { + return items.reduce((sum, item) => sum + item.price * item.quantity, 0); +} + +function printFooter() { + console.log('================='); +} +``` + +**Mechanics**: +1. Create new function named after what it does (not how) +2. Copy the code fragment to the new function +3. Pass any needed variables as parameters +4. Replace the original code with a call to the new function + +--- + +### Inline Function + +**Motivation**: The opposite of Extract Function. The function body is as clear as the name, or the function is only delegating. + +**When to use**: +- Function body is as obvious as its name +- You have a group of badly factored functions and want to re-extract differently +- Too much indirection + +```typescript +// Before +function moreThanFiveOrders(customer: Customer): boolean { + return customer.orders.length > 5; +} + +function getDiscount(customer: Customer): number { + if (moreThanFiveOrders(customer)) { + return 0.1; + } + return 0; +} +``` + +```typescript +// After +function getDiscount(customer: Customer): number { + if (customer.orders.length > 5) { + return 0.1; + } + return 0; +} +``` + +**Mechanics**: +1. Check function isn't polymorphic (overridden in subclasses) +2. Find all callers +3. Replace each call with the function body +4. Delete the function + +--- + +### Replace Temp with Query + +**Motivation**: Temporary variables can be a problem—they make functions longer and block Extract Function. + +**When to use**: +- A temp is assigned once and used multiple times +- The calculation could be a method +- You want to extract part of a function + +```typescript +// Before +function getPrice(order: Order): number { + const basePrice = order.quantity * order.itemPrice; + const discount = Math.max(0, order.quantity - 100) * order.itemPrice * 0.05; + const shipping = Math.min(basePrice * 0.1, 50); + return basePrice - discount + shipping; +} +``` + +```typescript +// After +function getPrice(order: Order): number { + return basePrice(order) - discount(order) + shipping(order); +} + +function basePrice(order: Order): number { + return order.quantity * order.itemPrice; +} + +function discount(order: Order): number { + return Math.max(0, order.quantity - 100) * order.itemPrice * 0.05; +} + +function shipping(order: Order): number { + return Math.min(basePrice(order) * 0.1, 50); +} +``` + +**Mechanics**: +1. Check the temp is only assigned once +2. Extract the assignment into a function +3. Replace references to the temp with function calls +4. Remove the temp declaration + +--- + +### Introduce Explaining Variable + +**Motivation**: Complex expressions are hard to read. Break them into named pieces. + +**When to use**: +- A complex expression that's hard to understand +- Multiple conditions in an if statement +- Mathematical formulas + +```typescript +// Before +function calculatePrice(order: Order): number { + return order.quantity * order.itemPrice - + Math.max(0, order.quantity - 100) * order.itemPrice * 0.05 + + Math.min(order.quantity * order.itemPrice * 0.1, 50); +} + +function isEligibleForDiscount(user: User): boolean { + return user.age >= 65 || + (user.memberSince < new Date('2020-01-01') && user.purchaseCount > 10) || + user.isEmployee; +} +``` + +```typescript +// After +function calculatePrice(order: Order): number { + const basePrice = order.quantity * order.itemPrice; + const quantityDiscount = Math.max(0, order.quantity - 100) * order.itemPrice * 0.05; + const shipping = Math.min(basePrice * 0.1, 50); + return basePrice - quantityDiscount + shipping; +} + +function isEligibleForDiscount(user: User): boolean { + const isSenior = user.age >= 65; + const isLoyalCustomer = user.memberSince < new Date('2020-01-01') && + user.purchaseCount > 10; + const isEmployee = user.isEmployee; + + return isSenior || isLoyalCustomer || isEmployee; +} +``` + +**Mechanics**: +1. Identify a complex expression or sub-expression +2. Create a variable with a meaningful name +3. Assign the expression to the variable +4. Replace the expression with the variable + +--- + +## Moving Features + +Move code to where it belongs. + +### Move Function + +**Motivation**: Functions should live with the data they use most. + +**When to use**: +- Function uses more features of another class +- Function is in the wrong module +- Coupling would be reduced by moving + +```typescript +// Before - calculateInterest uses account data, not the calculator's +class InterestCalculator { + calculateInterest(account: Account, days: number): number { + const balance = account.getBalance(); + const rate = account.getInterestRate(); + const type = account.getType(); + + if (type === 'savings') { + return balance * rate * days / 365; + } else if (type === 'checking') { + return balance * (rate - 0.01) * days / 365; + } + return 0; + } +} +``` + +```typescript +// After - method moved to Account where the data lives +class Account { + private balance: number; + private interestRate: number; + private type: 'savings' | 'checking'; + + calculateInterest(days: number): number { + if (this.type === 'savings') { + return this.balance * this.interestRate * days / 365; + } else if (this.type === 'checking') { + return this.balance * (this.interestRate - 0.01) * days / 365; + } + return 0; + } +} +``` + +**Mechanics**: +1. Look at what the function references—does it use more from elsewhere? +2. Check if it should be a method on one of its arguments +3. Move the function +4. Update all callers + +--- + +### Extract Class + +**Motivation**: A class is doing too much. Split it based on responsibilities. + +**When to use**: +- Class has too many fields +- Class has too many methods +- Subsets of data/methods are used together +- Class name includes "And" or "Manager" + +```typescript +// Before - Person has phone-related responsibilities mixed in +class Person { + name: string; + + // These belong together + officeAreaCode: string; + officeNumber: string; + + // And these + homeAreaCode: string; + homeNumber: string; + + getOfficePhone(): string { + return `(${this.officeAreaCode}) ${this.officeNumber}`; + } + + getHomePhone(): string { + return `(${this.homeAreaCode}) ${this.homeNumber}`; + } +} +``` + +```typescript +// After - Phone is its own class +class PhoneNumber { + constructor( + private areaCode: string, + private number: string + ) {} + + format(): string { + return `(${this.areaCode}) ${this.number}`; + } +} + +class Person { + name: string; + officePhone: PhoneNumber; + homePhone: PhoneNumber; + + getOfficePhone(): string { + return this.officePhone.format(); + } + + getHomePhone(): string { + return this.homePhone.format(); + } +} +``` + +**Mechanics**: +1. Identify a subset of data and methods that belong together +2. Create a new class +3. Move fields and methods to the new class +4. Create a link from old class to new class + +--- + +### Hide Delegate + +**Motivation**: Reduce coupling by hiding the object structure from clients. + +**When to use**: +- Clients navigate through one object to get to another +- Changes to the delegate affect all clients +- You're exposing internal structure + +```typescript +// Before - Client knows about internal structure +class Person { + department: Department; +} + +class Department { + manager: Person; +} + +// Client code - knows too much about structure +const manager = person.department.manager; +``` + +```typescript +// After - Person hides the delegation +class Person { + private department: Department; + + getManager(): Person { + return this.department.manager; + } +} + +// Client code - simpler, less coupled +const manager = person.getManager(); +``` + +**Mechanics**: +1. Create a delegating method on the server +2. Replace client calls to the delegate with calls to the server +3. Consider making the delegate field private + +--- + +## Organizing Data + +Improve how data is structured and accessed. + +### Replace Magic Number with Constant + +**Motivation**: Magic numbers obscure meaning and are easy to mistype. + +**When to use**: +- Numbers appear in code without explanation +- Same number appears in multiple places +- The number has domain meaning + +```typescript +// Before +function potentialEnergy(mass: number, height: number): number { + return mass * height * 9.81; +} + +function calculateDiscount(total: number): number { + if (total > 100) { + return total * 0.1; + } + return 0; +} +``` + +```typescript +// After +const GRAVITATIONAL_CONSTANT = 9.81; +const DISCOUNT_THRESHOLD = 100; +const DISCOUNT_RATE = 0.1; + +function potentialEnergy(mass: number, height: number): number { + return mass * height * GRAVITATIONAL_CONSTANT; +} + +function calculateDiscount(total: number): number { + if (total > DISCOUNT_THRESHOLD) { + return total * DISCOUNT_RATE; + } + return 0; +} +``` + +**Mechanics**: +1. Create a constant with a meaningful name +2. Replace the magic number with the constant +3. Search for other occurrences of the same number + +--- + +### Replace Primitive with Object + +**Motivation**: Primitives with behavior should be objects. + +**When to use**: +- Same validation logic for a primitive appears multiple times +- Formatting/parsing logic is scattered +- The value has business rules + +```typescript +// Before - phone validation scattered everywhere +function validateOrder(order: Order) { + const phone = order.phone; + if (!phone.match(/^\d{10}$/)) { + throw new Error('Invalid phone'); + } +} + +function formatPhone(phone: string): string { + return `(${phone.slice(0,3)}) ${phone.slice(3,6)}-${phone.slice(6)}`; +} +``` + +```typescript +// After - PhoneNumber encapsulates all behavior +class PhoneNumber { + private readonly value: string; + + constructor(phone: string) { + if (!phone.match(/^\d{10}$/)) { + throw new Error('Invalid phone number'); + } + this.value = phone; + } + + format(): string { + return `(${this.value.slice(0,3)}) ${this.value.slice(3,6)}-${this.value.slice(6)}`; + } + + getAreaCode(): string { + return this.value.slice(0, 3); + } + + toString(): string { + return this.value; + } +} + +// Usage +const phone = new PhoneNumber('5551234567'); +console.log(phone.format()); // (555) 123-4567 +``` + +**Mechanics**: +1. Create a class for the value +2. Add validation in constructor +3. Add any formatting/parsing methods +4. Replace usages of the primitive + +--- + +### Encapsulate Collection + +**Motivation**: Exposing a collection allows clients to modify it without the owner knowing. + +**When to use**: +- A getter returns a raw collection +- Clients are adding/removing items directly +- Collection modifications should trigger other behavior + +```typescript +// Before - collection exposed directly +class Course { + name: string; +} + +class Person { + courses: Course[] = []; + + getCourses(): Course[] { + return this.courses; // Returns mutable reference! + } +} + +// Client can bypass the Person +person.getCourses().push(newCourse); +person.getCourses().length = 0; // Dangerous! +``` + +```typescript +// After - collection encapsulated +class Person { + private courses: Course[] = []; + + getCourses(): readonly Course[] { + return [...this.courses]; // Return copy + } + + addCourse(course: Course): void { + this.courses.push(course); + // Can add validation, events, logging here + } + + removeCourse(course: Course): void { + const index = this.courses.indexOf(course); + if (index > -1) { + this.courses.splice(index, 1); + } + } + + get numberOfCourses(): number { + return this.courses.length; + } +} +``` + +**Mechanics**: +1. Add methods for modifying the collection +2. Return a copy or readonly view from the getter +3. Replace direct modifications with method calls + +--- + +## Simplifying Conditionals + +Make conditional logic easier to understand. + +### Decompose Conditional + +**Motivation**: Complex conditionals are hard to read. Extract into well-named functions. + +**When to use**: +- Conditional has complex conditions +- Then/else branches have substantial code +- The logic isn't immediately clear + +```typescript +// Before +function calculateCharge(date: Date, quantity: number): number { + if (date.getMonth() >= 5 && date.getMonth() <= 8) { + return quantity * 1.2 + (quantity > 100 ? quantity * 0.05 : 0); + } else { + return quantity * 1.0 + (quantity > 50 ? quantity * 0.03 : 0); + } +} +``` + +```typescript +// After +function calculateCharge(date: Date, quantity: number): number { + if (isSummer(date)) { + return summerCharge(quantity); + } else { + return regularCharge(quantity); + } +} + +function isSummer(date: Date): boolean { + return date.getMonth() >= 5 && date.getMonth() <= 8; +} + +function summerCharge(quantity: number): number { + const baseCharge = quantity * 1.2; + const bulkDiscount = quantity > 100 ? quantity * 0.05 : 0; + return baseCharge + bulkDiscount; +} + +function regularCharge(quantity: number): number { + const baseCharge = quantity * 1.0; + const bulkDiscount = quantity > 50 ? quantity * 0.03 : 0; + return baseCharge + bulkDiscount; +} +``` + +**Mechanics**: +1. Extract the condition into a function +2. Extract the then-branch into a function +3. Extract the else-branch into a function + +--- + +### Consolidate Conditional Expression + +**Motivation**: Multiple conditions with the same result should be combined. + +**When to use**: +- Several conditions return the same value +- The conditions are really checking one thing +- Combining makes intent clearer + +```typescript +// Before +function disabilityAmount(employee: Employee): number { + if (employee.seniority < 2) return 0; + if (employee.monthsDisabled > 12) return 0; + if (employee.isPartTime) return 0; + // Calculate disability + return employee.salary * 0.6; +} +``` + +```typescript +// After +function disabilityAmount(employee: Employee): number { + if (isNotEligibleForDisability(employee)) return 0; + return employee.salary * 0.6; +} + +function isNotEligibleForDisability(employee: Employee): boolean { + return employee.seniority < 2 || + employee.monthsDisabled > 12 || + employee.isPartTime; +} +``` + +**Mechanics**: +1. Combine conditions using logical operators +2. Extract the combined condition into a function +3. Give it a meaningful name + +--- + +### Replace Nested Conditional with Guard Clauses + +**Motivation**: Deeply nested conditionals are hard to follow. Use early returns. + +**When to use**: +- Deep nesting (more than 2 levels) +- Some branches are exceptional/edge cases +- Happy path is buried in else clauses + +```typescript +// Before - nested pyramid +function getPayAmount(employee: Employee): number { + let result: number; + if (employee.isSeparated) { + result = 0; + } else { + if (employee.isRetired) { + result = employee.pension; + } else { + if (employee.isOnLeave) { + result = employee.salary * 0.5; + } else { + result = employee.salary; + } + } + } + return result; +} +``` + +```typescript +// After - flat with guard clauses +function getPayAmount(employee: Employee): number { + if (employee.isSeparated) return 0; + if (employee.isRetired) return employee.pension; + if (employee.isOnLeave) return employee.salary * 0.5; + return employee.salary; +} +``` + +**Mechanics**: +1. Identify edge cases +2. Replace each with a guard clause (early return) +3. Remove unnecessary else clauses + +--- + +### Replace Conditional with Polymorphism + +**Motivation**: Type-based conditionals often indicate missing polymorphism. + +**When to use**: +- Switching on type code +- Same switch appears in multiple places +- Each case has substantially different behavior + +```typescript +// Before +type BirdType = 'european' | 'african' | 'norwegian_blue'; + +function getSpeed(bird: { type: BirdType; voltage?: number }): number { + switch (bird.type) { + case 'european': + return 35; + case 'african': + return 40 - 2 * bird.numberOfCoconuts; + case 'norwegian_blue': + return bird.voltage > 100 ? 20 : 0; + default: + return 0; + } +} + +function getPlumage(bird: { type: BirdType }): string { + switch (bird.type) { + case 'european': + return 'average'; + case 'african': + return bird.numberOfCoconuts > 2 ? 'tired' : 'average'; + case 'norwegian_blue': + return bird.voltage > 100 ? 'scorched' : 'beautiful'; + default: + return 'unknown'; + } +} +``` + +```typescript +// After +interface Bird { + getSpeed(): number; + getPlumage(): string; +} + +class EuropeanSwallow implements Bird { + getSpeed() { return 35; } + getPlumage() { return 'average'; } +} + +class AfricanSwallow implements Bird { + constructor(private numberOfCoconuts: number) {} + + getSpeed() { return 40 - 2 * this.numberOfCoconuts; } + getPlumage() { return this.numberOfCoconuts > 2 ? 'tired' : 'average'; } +} + +class NorwegianBlueParrot implements Bird { + constructor(private voltage: number) {} + + getSpeed() { return this.voltage > 100 ? 20 : 0; } + getPlumage() { return this.voltage > 100 ? 'scorched' : 'beautiful'; } +} + +// Factory to create the right type +function createBird(data: BirdData): Bird { + switch (data.type) { + case 'european': return new EuropeanSwallow(); + case 'african': return new AfricanSwallow(data.numberOfCoconuts); + case 'norwegian_blue': return new NorwegianBlueParrot(data.voltage); + } +} +``` + +**Mechanics**: +1. Create interface/base class +2. Create a class for each type +3. Move switch logic into each class +4. Replace switch with polymorphic call + +--- + +## Simplifying Function Calls + +Make functions easier to call and understand. + +### Rename Function + +**Motivation**: Good names are the best documentation. If you can't name it, you don't understand it. + +**When to use**: +- Name doesn't describe what the function does +- Name is misleading +- Name is too technical for the domain + +```typescript +// Before - unclear names +function calc(a: number, b: number): number { /* ... */ } +function process(data: unknown): void { /* ... */ } +function handle(event: Event): void { /* ... */ } +function inv(customer: Customer): Invoice { /* ... */ } +``` + +```typescript +// After - descriptive names +function calculateCompoundInterest(principal: number, rate: number): number { /* ... */ } +function validateAndSaveUserProfile(profile: UserProfile): void { /* ... */ } +function trackButtonClick(event: MouseEvent): void { /* ... */ } +function generateInvoiceForCustomer(customer: Customer): Invoice { /* ... */ } +``` + +**Mechanics**: +1. Choose a better name (this is the hard part) +2. Create new function with new name +3. Copy body to new function +4. Update all callers +5. Remove old function + +--- + +### Introduce Parameter Object + +**Motivation**: Groups of parameters that travel together should be a single object. + +**When to use**: +- Same group of parameters appears in multiple functions +- Parameters have a clear relationship +- You want to add behavior to the group + +```typescript +// Before - date range parameters everywhere +function getTotalSales(startDate: Date, endDate: Date): number { /* ... */ } +function getAverageOrders(startDate: Date, endDate: Date): number { /* ... */ } +function generateReport(startDate: Date, endDate: Date, format: string): Report { /* ... */ } +``` + +```typescript +// After - DateRange object +class DateRange { + constructor( + readonly start: Date, + readonly end: Date + ) { + if (start > end) throw new Error('Invalid date range'); + } + + contains(date: Date): boolean { + return date >= this.start && date <= this.end; + } + + get durationInDays(): number { + const ms = this.end.getTime() - this.start.getTime(); + return Math.ceil(ms / (1000 * 60 * 60 * 24)); + } +} + +function getTotalSales(dateRange: DateRange): number { /* ... */ } +function getAverageOrders(dateRange: DateRange): number { /* ... */ } +function generateReport(dateRange: DateRange, format: string): Report { /* ... */ } +``` + +**Mechanics**: +1. Create a class for the parameter group +2. Add validation in constructor +3. Add any relevant methods +4. Replace parameter lists with the new object + +--- + +### Replace Parameter with Method + +**Motivation**: A parameter that can be derived from other available information is redundant. + +**When to use**: +- Parameter value is derived from something else +- Parameter requires complex calculation by caller +- You're passing internal state back in + +```typescript +// Before - caller computes discountRate +function getFinalPrice( + basePrice: number, + discountLevel: number, + discountRate: number +): number { + return basePrice - (basePrice * discountRate); +} + +// Caller has to know how to calculate rate +const rate = getDiscountRate(customer.discountLevel); +const price = getFinalPrice(basePrice, customer.discountLevel, rate); +``` + +```typescript +// After - function derives discountRate internally +function getFinalPrice(basePrice: number, discountLevel: number): number { + const discountRate = getDiscountRate(discountLevel); + return basePrice - (basePrice * discountRate); +} + +// Caller is simpler +const price = getFinalPrice(basePrice, customer.discountLevel); +``` + +**Mechanics**: +1. Extract the parameter calculation into a method +2. Call that method inside the function +3. Remove the parameter + +--- + +## Quick Reference + +| Category | Refactoring | When to Use | +|----------|-------------|-------------| +| **Composing Methods** | Extract Function | Code block can be named | +| | Inline Function | Body is clear as name | +| | Replace Temp with Query | Temp blocks extraction | +| | Introduce Explaining Variable | Complex expression | +| **Moving Features** | Move Function | Uses other class's data | +| | Extract Class | Class does too much | +| | Hide Delegate | Clients navigate too deep | +| **Organizing Data** | Replace Magic Number | Unexplained numbers | +| | Replace Primitive with Object | Primitive has behavior | +| | Encapsulate Collection | Exposed mutable collection | +| **Simplifying Conditionals** | Decompose Conditional | Complex if-then-else | +| | Consolidate Conditional | Same result, multiple checks | +| | Guard Clauses | Deep nesting | +| | Replace with Polymorphism | Switch on type | +| **Function Calls** | Rename Function | Name doesn't fit | +| | Parameter Object | Params travel together | +| | Replace Parameter with Method | Param can be derived | + +--- + +## Refactoring Workflow + +1. **Ensure tests pass** before starting +2. **Make one small change** at a time +3. **Run tests** after each change +4. **Commit frequently** so you can revert +5. **Never refactor and add features** in the same commit diff --git a/skills/clean-code-review/templates/platforms/claude-knowledge.md b/skills/clean-code-review/templates/platforms/claude-knowledge.md new file mode 100644 index 0000000..e74e7e2 --- /dev/null +++ b/skills/clean-code-review/templates/platforms/claude-knowledge.md @@ -0,0 +1,55 @@ +# Clean Code — Claude Project Knowledge + + +You are a pragmatic coding assistant that writes clean, maintainable code. +Your style is concise, direct, and solution-focused. You never over-engineer. +You write code directly — you do not write tutorials or explain before implementing. + + + +## Core Principles +- Apply SRP: each function/class does ONE thing +- Apply DRY: extract duplicated logic into shared functions +- Apply KISS: always choose the simplest working solution +- Apply YAGNI: never build features that aren't needed yet +- Leave code cleaner than you found it + +## Naming +- Variables reveal intent: `userCount` not `n` +- Functions use verb+noun: `getUserById()` not `user()` +- Booleans use question form: `isActive`, `hasPermission`, `canEdit` +- Constants use SCREAMING_SNAKE_CASE: `MAX_RETRY_COUNT` +- If a name needs a comment to explain it, rename it instead + +## Functions +- Max 20 lines per function, ideally 5–10 +- One thing per function, one level of abstraction +- Max 3 arguments, prefer 0–2 +- No unexpected side effects — don't mutate inputs + +## Structure +- Use guard clauses for early returns on edge cases +- Max 2 levels of nesting — flatten with early returns +- Compose small, focused functions together +- Colocate related code in the same module + +## Anti-Patterns — Never Do These +- Never comment obvious code — delete it +- Never create helpers for one-liners — inline them +- Never create a `utils.ts` with a single function +- Never use magic numbers — use named constants +- Never write god functions — split by responsibility +- Never leave deep nesting — use guard clauses + +## Before Editing Any File +- Identify all files that import the target file +- Check if interface changes break dependents +- Verify test coverage — update tests alongside code +- Edit the file AND all dependents in the same task + +## Self-Check +- Verify the user's goal is met exactly +- Verify all necessary files are modified +- Verify lint and type checks pass +- Verify no edge cases are missed + diff --git a/skills/clean-code-review/templates/platforms/copilot-instructions.md b/skills/clean-code-review/templates/platforms/copilot-instructions.md new file mode 100644 index 0000000..aa57dab --- /dev/null +++ b/skills/clean-code-review/templates/platforms/copilot-instructions.md @@ -0,0 +1,59 @@ +# Clean Code — Copilot Instructions + +## Instructions + +Follow these pragmatic coding standards in all generated code. Be concise, direct, and solution-focused. Never over-engineer. + +## Core Principles + +Apply these principles to every piece of code: + +- **SRP** — Single Responsibility. Each function/class does ONE thing. +- **DRY** — Don't Repeat Yourself. Extract shared logic. +- **KISS** — Keep It Simple. Simplest solution that works. +- **YAGNI** — Don't build features that aren't needed yet. + +## Naming Patterns + +``` +// Variables — reveal intent +✅ userCount, isAuthenticated, orderTotal +❌ n, flag, x + +// Functions — verb + noun +✅ getUserById(), calculateTotal(), sendEmail() +❌ user(), calc(), email() + +// Booleans — question form +✅ isActive, hasPermission, canEdit +❌ active, permission, edit +``` + +## Function Structure + +Keep functions small (5–20 lines), with max 3 arguments. Use guard clauses: + +```typescript +// ✅ Guard clauses — flat and readable +function processOrder(order: Order): Result { + if (!order) return { error: 'No order' }; + if (!order.items.length) return { error: 'Empty order' }; + if (!order.payment) return { error: 'No payment' }; + + const total = calculateTotal(order.items); + return chargeAndFulfill(order, total); +} +``` + +## Anti-Patterns to Avoid + +- Don't comment obvious code — let names self-document +- Don't create helpers for one-liners — inline instead +- Don't create `utils.ts` for a single function +- Don't use magic numbers — define named constants +- Don't write functions over 20 lines — split by responsibility +- Don't nest deeper than 2 levels — use early returns + +## Before Editing Files + +Always check: what imports this file, what tests cover it, and whether dependent files need updates too. Edit all affected files together. diff --git a/skills/clean-code-review/templates/platforms/cursorrules.md b/skills/clean-code-review/templates/platforms/cursorrules.md new file mode 100644 index 0000000..c96cf9f --- /dev/null +++ b/skills/clean-code-review/templates/platforms/cursorrules.md @@ -0,0 +1,50 @@ +# Clean Code — Cursor Rules + +# Pragmatic coding standards: concise, direct, no over-engineering. + +## Core Principles +- Follow SRP: each function/class does ONE thing +- Follow DRY: extract duplicates, reuse shared logic +- Follow KISS: always choose the simplest solution that works +- Follow YAGNI: never build features that aren't needed yet +- Leave code cleaner than you found it (Boy Scout Rule) + +## Naming +- Variables must reveal intent: `userCount` not `n` +- Functions use verb+noun: `getUserById()` not `user()` +- Booleans use question form: `isActive`, `hasPermission`, `canEdit` +- Constants use SCREAMING_SNAKE: `MAX_RETRY_COUNT` +- If a name needs a comment to explain it, rename it + +## Functions +- Keep functions under 20 lines, ideally 5–10 +- Each function does one thing at one level of abstraction +- Max 3 arguments, prefer 0–2 +- Never mutate inputs unexpectedly + +## Structure +- Use guard clauses and early returns for edge cases +- Keep nesting to max 2 levels — flatten with early returns +- Compose small functions together +- Colocate related code + +## Anti-Patterns — Never Do These +- Do not comment every line — delete obvious comments +- Do not create helpers for one-liners — inline the code +- Do not create factories for 2 objects — use direct instantiation +- Do not create `utils.ts` for a single function — put code where it's used +- Do not use magic numbers — use named constants +- Do not write god functions — split by responsibility + +## Before Editing Any File +- Check what imports this file — dependents might break +- Check what this file imports — interfaces may change +- Check what tests cover this file — tests might fail +- Edit the file AND all dependent files in the same task +- Never leave broken imports or missing updates + +## Self-Check Before Completing +- Verify the goal is met — did you do exactly what was asked? +- Verify all necessary files are modified +- Verify lint and type checks pass +- Verify no edge cases are missed diff --git a/skills/next-best-practices/.clawhub/origin.json b/skills/next-best-practices/.clawhub/origin.json new file mode 100644 index 0000000..482799c --- /dev/null +++ b/skills/next-best-practices/.clawhub/origin.json @@ -0,0 +1,8 @@ +{ + "version": 1, + "registry": "https://clawhub.ai", + "slug": "next-best-practices", + "installedVersion": "0.1.0", + "installedAt": 1779235116276, + "fingerprint": "fc4914e81195b0c960b7450c8c0c975a28442df24115c36a1aee70b1e05da7ab" +} diff --git a/skills/next-best-practices/SKILL.md b/skills/next-best-practices/SKILL.md new file mode 100644 index 0000000..437896b --- /dev/null +++ b/skills/next-best-practices/SKILL.md @@ -0,0 +1,153 @@ +--- +name: next-best-practices +description: Next.js best practices - file conventions, RSC boundaries, data patterns, async APIs, metadata, error handling, route handlers, image/font optimization, bundling +user-invocable: false +--- + +# Next.js Best Practices + +Apply these rules when writing or reviewing Next.js code. + +## File Conventions + +See [file-conventions.md](./file-conventions.md) for: +- Project structure and special files +- Route segments (dynamic, catch-all, groups) +- Parallel and intercepting routes +- Middleware rename in v16 (middleware → proxy) + +## RSC Boundaries + +Detect invalid React Server Component patterns. + +See [rsc-boundaries.md](./rsc-boundaries.md) for: +- Async client component detection (invalid) +- Non-serializable props detection +- Server Action exceptions + +## Async Patterns + +Next.js 15+ async API changes. + +See [async-patterns.md](./async-patterns.md) for: +- Async `params` and `searchParams` +- Async `cookies()` and `headers()` +- Migration codemod + +## Runtime Selection + +See [runtime-selection.md](./runtime-selection.md) for: +- Default to Node.js runtime +- When Edge runtime is appropriate + +## Directives + +See [directives.md](./directives.md) for: +- `'use client'`, `'use server'` (React) +- `'use cache'` (Next.js) + +## Functions + +See [functions.md](./functions.md) for: +- Navigation hooks: `useRouter`, `usePathname`, `useSearchParams`, `useParams` +- Server functions: `cookies`, `headers`, `draftMode`, `after` +- Generate functions: `generateStaticParams`, `generateMetadata` + +## Error Handling + +See [error-handling.md](./error-handling.md) for: +- `error.tsx`, `global-error.tsx`, `not-found.tsx` +- `redirect`, `permanentRedirect`, `notFound` +- `forbidden`, `unauthorized` (auth errors) +- `unstable_rethrow` for catch blocks + +## Data Patterns + +See [data-patterns.md](./data-patterns.md) for: +- Server Components vs Server Actions vs Route Handlers +- Avoiding data waterfalls (`Promise.all`, Suspense, preload) +- Client component data fetching + +## Route Handlers + +See [route-handlers.md](./route-handlers.md) for: +- `route.ts` basics +- GET handler conflicts with `page.tsx` +- Environment behavior (no React DOM) +- When to use vs Server Actions + +## Metadata & OG Images + +See [metadata.md](./metadata.md) for: +- Static and dynamic metadata +- `generateMetadata` function +- OG image generation with `next/og` +- File-based metadata conventions + +## Image Optimization + +See [image.md](./image.md) for: +- Always use `next/image` over `` +- Remote images configuration +- Responsive `sizes` attribute +- Blur placeholders +- Priority loading for LCP + +## Font Optimization + +See [font.md](./font.md) for: +- `next/font` setup +- Google Fonts, local fonts +- Tailwind CSS integration +- Preloading subsets + +## Bundling + +See [bundling.md](./bundling.md) for: +- Server-incompatible packages +- CSS imports (not link tags) +- Polyfills (already included) +- ESM/CommonJS issues +- Bundle analysis + +## Scripts + +See [scripts.md](./scripts.md) for: +- `next/script` vs native script tags +- Inline scripts need `id` +- Loading strategies +- Google Analytics with `@next/third-parties` + +## Hydration Errors + +See [hydration-error.md](./hydration-error.md) for: +- Common causes (browser APIs, dates, invalid HTML) +- Debugging with error overlay +- Fixes for each cause + +## Suspense Boundaries + +See [suspense-boundaries.md](./suspense-boundaries.md) for: +- CSR bailout with `useSearchParams` and `usePathname` +- Which hooks require Suspense boundaries + +## Parallel & Intercepting Routes + +See [parallel-routes.md](./parallel-routes.md) for: +- Modal patterns with `@slot` and `(.)` interceptors +- `default.tsx` for fallbacks +- Closing modals correctly with `router.back()` + +## Self-Hosting + +See [self-hosting.md](./self-hosting.md) for: +- `output: 'standalone'` for Docker +- Cache handlers for multi-instance ISR +- What works vs needs extra setup + +## Debug Tricks + +See [debug-tricks.md](./debug-tricks.md) for: +- MCP endpoint for AI-assisted debugging +- Rebuild specific routes with `--debug-build-paths` + diff --git a/skills/next-best-practices/_meta.json b/skills/next-best-practices/_meta.json new file mode 100644 index 0000000..a504b39 --- /dev/null +++ b/skills/next-best-practices/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn7fdqv8zpxp37z09bnmfs8k3h81fdve", + "slug": "next-best-practices", + "version": "0.1.0", + "publishedAt": 1771519351301 +} \ No newline at end of file diff --git a/skills/next-best-practices/async-patterns.md b/skills/next-best-practices/async-patterns.md new file mode 100644 index 0000000..dce8d8c --- /dev/null +++ b/skills/next-best-practices/async-patterns.md @@ -0,0 +1,87 @@ +# Async Patterns + +In Next.js 15+, `params`, `searchParams`, `cookies()`, and `headers()` are asynchronous. + +## Async Params and SearchParams + +Always type them as `Promise<...>` and await them. + +### Pages and Layouts + +```tsx +type Props = { params: Promise<{ slug: string }> } + +export default async function Page({ params }: Props) { + const { slug } = await params +} +``` + +### Route Handlers + +```tsx +export async function GET( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params +} +``` + +### SearchParams + +```tsx +type Props = { + params: Promise<{ slug: string }> + searchParams: Promise<{ query?: string }> +} + +export default async function Page({ params, searchParams }: Props) { + const { slug } = await params + const { query } = await searchParams +} +``` + +### Synchronous Components + +Use `React.use()` for non-async components: + +```tsx +import { use } from 'react' + +type Props = { params: Promise<{ slug: string }> } + +export default function Page({ params }: Props) { + const { slug } = use(params) +} +``` + +### generateMetadata + +```tsx +type Props = { params: Promise<{ slug: string }> } + +export async function generateMetadata({ params }: Props): Promise { + const { slug } = await params + return { title: slug } +} +``` + +## Async Cookies and Headers + +```tsx +import { cookies, headers } from 'next/headers' + +export default async function Page() { + const cookieStore = await cookies() + const headersList = await headers() + + const theme = cookieStore.get('theme') + const userAgent = headersList.get('user-agent') +} +``` + +## Migration Codemod + +```bash +npx @next/codemod@latest next-async-request-api . +``` diff --git a/skills/next-best-practices/bundling.md b/skills/next-best-practices/bundling.md new file mode 100644 index 0000000..ac5e814 --- /dev/null +++ b/skills/next-best-practices/bundling.md @@ -0,0 +1,180 @@ +# Bundling + +Fix common bundling issues with third-party packages. + +## Server-Incompatible Packages + +Some packages use browser APIs (`window`, `document`, `localStorage`) and fail in Server Components. + +### Error Signs + +``` +ReferenceError: window is not defined +ReferenceError: document is not defined +ReferenceError: localStorage is not defined +Module not found: Can't resolve 'fs' +``` + +### Solution 1: Mark as Client-Only + +If the package is only needed on client: + +```tsx +// Bad: Fails - package uses window +import SomeChart from 'some-chart-library' + +export default function Page() { + return +} + +// Good: Use dynamic import with ssr: false +import dynamic from 'next/dynamic' + +const SomeChart = dynamic(() => import('some-chart-library'), { + ssr: false, +}) + +export default function Page() { + return +} +``` + +### Solution 2: Externalize from Server Bundle + +For packages that should run on server but have bundling issues: + +```js +// next.config.js +module.exports = { + serverExternalPackages: ['problematic-package'], +} +``` + +Use this for: +- Packages with native bindings (sharp, bcrypt) +- Packages that don't bundle well (some ORMs) +- Packages with circular dependencies + +### Solution 3: Client Component Wrapper + +Wrap the entire usage in a client component: + +```tsx +// components/ChartWrapper.tsx +'use client' + +import { Chart } from 'chart-library' + +export function ChartWrapper(props) { + return +} + +// app/page.tsx (server component) +import { ChartWrapper } from '@/components/ChartWrapper' + +export default function Page() { + return +} +``` + +## CSS Imports + +Import CSS files instead of using `` tags. Next.js handles bundling and optimization. + +```tsx +// Bad: Manual link tag + + +// Good: Import CSS +import './styles.css' + +// Good: CSS Modules +import styles from './Button.module.css' +``` + +## Polyfills + +Next.js includes common polyfills automatically. Don't load redundant ones from polyfill.io or similar CDNs. + +Already included: `Array.from`, `Object.assign`, `Promise`, `fetch`, `Map`, `Set`, `Symbol`, `URLSearchParams`, and 50+ others. + +```tsx +// Bad: Redundant polyfills + + +// Good: Next.js Script component +import Script from 'next/script' + + +``` + +## Don't Put Script in Head + +`next/script` should not be placed inside `next/head`. It handles its own positioning. + +```tsx +// Bad: Script inside Head +import Head from 'next/head' +import Script from 'next/script' + + + + +// Good: Next.js component +import { GoogleAnalytics } from '@next/third-parties/google' + +export default function Layout({ children }) { + return ( + + {children} + + + ) +} +``` + +## Google Tag Manager + +```tsx +import { GoogleTagManager } from '@next/third-parties/google' + +export default function Layout({ children }) { + return ( + + + {children} + + ) +} +``` + +## Other Third-Party Scripts + +```tsx +// YouTube embed +import { YouTubeEmbed } from '@next/third-parties/google' + + + +// Google Maps +import { GoogleMapsEmbed } from '@next/third-parties/google' + + +``` + +## Quick Reference + +| Pattern | Issue | Fix | +|---------|-------|-----| +| ` +``` + +--- + +## 8. 完整示例:生产级 Composable + +```typescript +// hooks/web/useSuggestion.ts +import { ref, onUnmounted } from 'vue' +import { useEngine } from './useEngine' +import { SUGGESTION_TIMEOUT } from '@/constants' + +interface SuggestionOptions { + timeout?: number + maxResults?: number +} + +export function useSuggestion(options?: SuggestionOptions) { + const { currentEngine } = useEngine() + const { timeout = SUGGESTION_TIMEOUT, maxResults = 10 } = options ?? {} + + // 状态 + const suggestions = ref([]) + const loading = ref(false) + + // 清理:JSONP 脚本和超时定时器 + let scriptEl: HTMLScriptElement | null = null + let timer: ReturnType | null = null + + function cleanup() { + if (scriptEl) { + scriptEl.remove() + scriptEl = null + } + if (timer) { + clearTimeout(timer) + timer = null + } + } + + async function fetch(keyword: string) { + if (!keyword.trim()) { + suggestions.value = [] + return + } + + cleanup() + loading.value = true + + return new Promise((resolve) => { + const callbackName = `suggestion_${Date.now()}` + + // JSONP 回调 + ;(window as any)[callbackName] = (data: string[]) => { + suggestions.value = data.slice(0, maxResults) + loading.value = false + cleanup() + delete (window as any)[callbackName] + resolve() + } + + // 超时处理 + timer = setTimeout(() => { + suggestions.value = [] + loading.value = false + cleanup() + delete (window as any)[callbackName] + resolve() + }, timeout) + + // 注入脚本 + const url = currentEngine.value.suggestionUrl(keyword, callbackName) + scriptEl = document.createElement('script') + scriptEl.src = url + document.head.appendChild(scriptEl) + }) + } + + // 自动清理 + onUnmounted(cleanup) + + return { suggestions, loading, fetch } +} +``` + +这个示例综合了多种模式: +- **Options 模式**:可配置超时和最大结果数 +- **异步资源**:loading 状态管理 +- **生命周期感知**:onUnmounted 自动清理 +- **最小暴露**:只返回 suggestions、loading、fetch +- **错误处理**:超时降级为空列表 + +--- + +## 9. 测试 Composable + +Composable 是纯函数(返回响应式状态 + 方法),非常适合单元测试。推荐使用 **Vitest + @vue/test-utils**。 + +### 测试纯计算型 Composable + +```typescript +// hooks/__tests__/useDesign.test.ts +import { describe, it, expect } from 'vitest' +import { useDesign } from '../web/useDesign' + +describe('useDesign', () => { + it('should generate correct prefix class', () => { + const { getPrefixCls } = useDesign() + expect(getPrefixCls('layout')).toBe('mi-layout') + }) + + it('should expose namespace variables', () => { + const { variables, simplePrefixCls } = useDesign() + expect(variables.namespace).toBeDefined() + expect(simplePrefixCls).toBeDefined() + }) +}) +``` + +### 测试有状态 Composable + +```typescript +// hooks/__tests__/useEngine.test.ts +import { describe, it, expect, beforeEach } from 'vitest' +import { setActivePinia, createPinia } from 'pinia' +import { useEngine } from '../web/useEngine' + +describe('useEngine', () => { + beforeEach(() => { + setActivePinia(createPinia()) // 每次测试前创建新的 pinia 实例 + }) + + it('should return current search engine', () => { + const { selectEngine, engineInfo } = useEngine() + expect(selectEngine.value).toBe('baidu') + expect(engineInfo.value.label).toBe('百度') + }) + + it('should switch to next engine', () => { + const { selectEngine, nextEngine } = useEngine() + nextEngine() + expect(selectEngine.value).toBe('google') + }) + + it('should cycle back to first engine', () => { + const { selectEngine, updateSelectEngine, nextEngine } = useEngine() + // 切换到最后一个 + updateSelectEngine('sogou') + nextEngine() + expect(selectEngine.value).toBe('baidu') + }) +}) +``` + +### 测试含生命周期的 Composable + +```typescript +// hooks/__tests__/useNetwork.test.ts +import { describe, it, expect, vi, afterEach } from 'vitest' +import { useNetwork } from '../web/useNetwork' + +describe('useNetwork', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should reflect online status', async () => { + vi.spyOn(navigator, 'onLine', 'get').mockReturnValue(true) + const { isOnline } = useNetwork() + expect(isOnline.value).toBe(true) + }) + + it('should update when going offline', async () => { + vi.spyOn(navigator, 'onLine', 'get').mockReturnValue(false) + const { isOnline } = useNetwork() + expect(isOnline.value).toBe(false) + }) +}) +``` + +### 测试异步 Composable + +```typescript +// hooks/__tests__/useLocalForage.test.ts +import { describe, it, expect } from 'vitest' +import { useLocalForage } from '../web/useLocalForage' + +describe('useLocalForage', () => { + it('should get and set items', async () => { + const { setItem, getItem, loading } = useLocalForage('test') + + await setItem('key1', { name: 'test' }) + expect(loading.value).toBe(false) + + const result = await getItem<{ name: string }>('key1') + expect(result?.name).toBe('test') + }) + + it('should handle missing items', async () => { + const { getItem } = useLocalForage('test') + const result = await getItem('nonexistent') + expect(result).toBeNull() + }) +}) +``` + +### 测试 Store Bridge Composable + +Store Bridge 模式的 composable 测试关键是初始化 pinia: + +```typescript +// hooks/__tests__/usePageIcon.test.ts +import { describe, it, expect, beforeEach } from 'vitest' +import { setActivePinia, createPinia } from 'pinia' +import { usePageIcon } from '../web/usePageIcon' + +describe('usePageIcon', () => { + beforeEach(() => { + setActivePinia(createPinia()) + }) + + it('should add page icon', () => { + const { addPageIcon, curPageIcons } = usePageIcon() + + addPageIcon({ + label: '测试', + url: 'https://example.com', + icon: 'test-icon', + iconType: 'online', + type: 'icon' + }) + + expect(curPageIcons.value.length).toBe(1) + expect(curPageIcons.value[0].label).toBe('测试') + }) +}) +``` + +### 测试原则 + +| 原则 | 说明 | +|------|------| +| 每个 test 独立 | 用 `beforeEach` + `setActivePinia(createPinia())` 重置状态 | +| 只测试公开接口 | 只测 `return` 的值和方法,不测内部实现 | +| Mock 副作用 | 网络请求、浏览器 API 用 `vi.spyOn` / `vi.mock` 隔离 | +| 覆盖边界情况 | 空输入、异常路径、极限值 | +| 测试异步行为 | 用 `async/await` + 断言 `loading` 状态变化 | + +**目录结构建议:** + +``` +src/hooks/ +├── web/ +│ ├── __tests__/ # 测试文件目录 +│ │ ├── useDesign.test.ts +│ │ ├── useEngine.test.ts +│ │ ├── useNetwork.test.ts +│ │ ├── usePageIcon.test.ts +│ │ └── useLocalForage.test.ts +│ ├── useDesign.ts +│ ├── useEngine.ts +│ └── ... +``` diff --git a/skills/vue-composition-api-best-practices/reference/cross-feature-dependencies.md b/skills/vue-composition-api-best-practices/reference/cross-feature-dependencies.md new file mode 100644 index 0000000..afa579e --- /dev/null +++ b/skills/vue-composition-api-best-practices/reference/cross-feature-dependencies.md @@ -0,0 +1,431 @@ +--- +title: Handling Cross-Feature Dependencies +impact: MEDIUM +impactDescription: 跨功能依赖管理不当会导致紧密耦合、不可预测的行为,以及功能交互时难以调试 +type: best-practice +tags: [vue3, composition-api, dependencies, coupling, architecture, event-bus] +--- + +# 处理跨功能依赖 + +**影响级别:MEDIUM** - 当功能需要交互时,合理的依赖管理可确保行为可预测、代码可维护。 + +## 任务清单 + +- [ ] 通过函数参数显式传递依赖 +- [ ] 避免通过外层作用域闭包产生隐式依赖 +- [ ] 使用回调函数进行跨功能通信 +- [ ] 对于多对多通信,使用事件总线并自动清理 +- [ ] 考虑依赖方向(单向,避免循环) +- [ ] 优先使用 Store 桥接组合式函数访问共享状态 + +## 问题所在 + +功能之间经常需要交互,但隐式依赖会使代码难以理解和测试。 + +**BAD - 通过外层作用域产生隐式依赖:** + +```vue + +``` + +**GOOD - 通过参数显式传递依赖:** + +```vue + +``` + +## 依赖模式 + +### 模式 1:回调模式(推荐用于简单通信) + +通过回调进行直接的跨功能通信: + +```typescript +function usePagination(options: { + onPageChange?: (page: number) => void +}) { + const currentPage = ref(1) + + const goToPage = (page: number) => { + currentPage.value = page + options.onPageChange?.(page) + } + + return { currentPage, goToPage } +} + +// 用法 +const { handleSearch } = useSearch() +const { currentPage, goToPage } = usePagination({ + onPageChange: () => handleSearch() +}) +``` + +### 模式 2:Ref 注入模式 + +传递响应式 ref 实现共享状态: + +```typescript +function useSearch(query: Ref) { + const results = ref([]) + + watch(query, async (q) => { + results.value = await fetchResults(q) + }) + + return { results } +} + +function useSearchInput() { + const query = ref('') + const debouncedQuery = refDebounced(query, 300) + + return { query, debouncedQuery } +} + +// 用法 — 显式依赖 +const { query, debouncedQuery } = useSearchInput() +const { results } = useSearch(debouncedQuery) +``` + +### 模式 3:组合式函数编排模式 + +创建更高层级的组合式函数来组合多个功能: + +```typescript +function useSearchWithPagination() { + const { searchQuery, searchResults, handleSearch } = useSearch() + const { currentPage, pageSize, changePage } = usePagination({ + onPageChange: handleSearch + }) + + const searchWithParams = () => { + return handleSearch({ + query: searchQuery.value, + page: currentPage.value, + size: pageSize.value + }) + } + + return { + searchQuery, searchResults, + currentPage, pageSize, + searchWithParams, changePage + } +} +``` + +### 模式 4:事件总线模式(适用于复杂的多对多场景) + +使用 `mitt` 并自动清理,实现解耦通信: + +```typescript +// hooks/web/useEmitt.ts +import { onUnmounted } from 'vue' +import { mittBus } from '@/utils/mitt' + +export function useEmitt() { + const listeners: Array<{ event: string; handler: (...args: any[]) => void }> = [] + + function on(event: string, handler: (...args: any[]) => void) { + mittBus.on(event, handler) + listeners.push({ event, handler }) + } + + function emit(event: string, ...args: any[]) { + mittBus.emit(event, ...args) + } + + // 组件卸载时自动解绑所有通过此 hook 注册的事件 + onUnmounted(() => { + listeners.forEach(({ event, handler }) => { + mittBus.off(event, handler) + }) + listeners.length = 0 + }) + + return { on, emit } +} +``` + +**用法 — 发送方:** + +```typescript +// Layout.vue - 发送事件 +const { emit } = useEmitt() +emit('open-contextmenu', { event: e }) +``` + +**用法 — 接收方(自动清理):** + +```typescript +// MiContextMenu.vue - 监听事件 +const { on } = useEmitt() + +on('open-contextmenu', (data) => { + // 在事件位置处理上下文菜单 +}) +// 无需手动 off,组件卸载时自动清理 +// 多次调用 on() 注册多个监听器,全部会在卸载时清理 +``` + +> 另见:[组合式函数设计模式 - 模式 3:生命周期感知](composable-design-patterns.md#模式-3生命周期感知) 了解 `useEmitt` 作为 Lifecycle-Aware 模式的完整设计原理。 + +**何时使用事件总线 vs 回调:** + +| 场景 | 模式 | 原因 | +|----------|---------|--------| +| 直接父子关系 | 回调/Props | 简单、显式、类型安全 | +| 同一组件内的兄弟功能 | 回调 | 依赖流清晰 | +| 不同层级树的跨组件通信 | 事件总线 | 需要解耦 | +| 一个事件多个监听器 | 事件总线 | 一对多关系 | +| 功能需要响应 store 变化 | Store 桥接 | 单一数据源 | + +### 模式 5:Store 桥接用于共享状态 + +当多个功能需要相同的 store 数据时,使用 Store 桥接组合式函数,而不是直接访问 store: + +```typescript +// ✅ Good — Store 桥接提供统一接口 +// hooks/web/useSideCategory.ts +export const useSideCategory = () => { + const appStore = useAppStoreWithOut() + + const selectCategory = computed(() => appStore.selectCategory) + const sidebarCategories = computed(() => appStore.sidebarCategories) + + const removeCategory = (val: string) => { + // 复杂的业务逻辑集中在这里 + const idx = sidebarCategories.value.findIndex((s) => s.key === val) + if (idx !== -1) { + const newCategories = [...sidebarCategories.value] + newCategories.splice(idx, 1) + appStore.deletePageIconInfo(val) // 同时清理相关数据 + // 处理选中状态... + appStore.setSidebarCategories(newCategories) + } + } + + return { selectCategory, sidebarCategories, removeCategory } +} + +// ❌ Bad — 组件中分散的直接 store 访问 +// ComponentA.vue +const appStore = useAppStore() +const idx = appStore.sidebarCategories.findIndex(...) +appStore.deletePageIconInfo(val) +appStore.setSidebarCategories(...) + +// ComponentB.vue - 重复的逻辑! +const appStore = useAppStore() +const idx = appStore.sidebarCategories.findIndex(...) +appStore.deletePageIconInfo(val) +appStore.setSidebarCategories(...) +``` + +## 依赖方向规则 + +### ✅ Good — 单向依赖 + +``` +┌─────────────┐ +│ 父组件 │ +│ Component │ +└──────┬──────┘ + │ 传递依赖 + ▼ +┌─────────────┐ ┌─────────────┐ +│ 功能 A │────▶│ 功能 B │ +│ (搜索) │ │ (分页) │ +└─────────────┘ └─────────────┘ +``` + +### ✅ Good — 跨树通信使用事件总线 + +``` +┌──────────┐ emit ┌──────────────┐ +│ Layout │───────────────▶│ ContextMenu │ +└──────────┘ └──────────────┘ + +┌──────────┐ emit ┌──────────────┐ +│ Search │───────────────▶│ Suggestion │ +└──────────┘ └──────────────┘ +``` + +### ❌ 避免 — 循环依赖 + +``` +┌─────────────┐ ┌─────────────┐ +│ 功能 A │◀───▶│ 功能 B │ +│ │ │ │ +└─────────────┘ └─────────────┘ + 循环依赖! +``` + +## 常见场景 + +### 搜索 + 筛选 + 分页 + +```vue + +``` + +### 通过事件总线跨组件通信 + +```vue + + + + + +``` + +### 表单 + 验证 + 提交 + +```vue + +``` + +## 参考资料 + +- [Vue.js Composables](https://vuejs.org/guide/reusability/composables.html) +- [Vue.js Reactivity in Depth](https://vuejs.org/guide/extras/reactivity-in-depth.html) +- [mitt - Tiny Event Emitter](https://github.com/developit/mitt) diff --git a/skills/vue-composition-api-best-practices/reference/feature-extraction.md b/skills/vue-composition-api-best-practices/reference/feature-extraction.md new file mode 100644 index 0000000..528d5aa --- /dev/null +++ b/skills/vue-composition-api-best-practices/reference/feature-extraction.md @@ -0,0 +1,393 @@ +--- +title: Feature Extraction to Composables +impact: MEDIUM +impactDescription: 未能提取可复用逻辑将导致代码重复、组件间行为不一致以及更高的维护成本 +type: best-practice +tags: [vue3, composition-api, composables, reusability, dry] +--- + +# 将功能提取为组合式函数 + +**影响等级:MEDIUM** - 将通用功能提取到外部组合式函数中可以促进代码复用、保持一致性和简化测试。 + +## 任务清单 + +- [ ] 识别多个组件中使用的逻辑 +- [ ] 提取到 `composables/` 或 `hooks/` 目录 +- [ ] 保持组合式函数专注于单一职责 +- [ ] 使用参数进行配置和依赖注入 +- [ ] 返回响应式引用和方法 +- [ ] 考虑使用 Store 桥接模式抽象 store 访问 + +## 问题 + +当相似逻辑在多个组件中重复时,任何 bug 修复或功能增强都必须在多处应用,增加了维护负担和不一致的风险。 + +**BAD - 组件间重复的逻辑:** + +```vue + + + + + +``` + +**GOOD - 提取到组合式函数:** + +```typescript +// hooks/web/useWindowSize.ts +import { ref, onMounted, onUnmounted } from 'vue' + +export function useWindowSize() { + const width = ref(window.innerWidth) + const height = ref(window.innerHeight) + + function handleResize() { + width.value = window.innerWidth + height.value = window.innerHeight + } + + onMounted(() => window.addEventListener('resize', handleResize)) + onUnmounted(() => window.removeEventListener('resize', handleResize)) + + return { width, height } +} +``` + +```vue + + +``` + +## 何时提取 + +| 信号 | 示例 | 操作 | +|--------|---------|--------| +| 在 2+ 组件中使用 | 窗口大小、认证状态 | 提取到组合式函数 | +| 复杂逻辑 | 表单验证、分页 | 提取以提高清晰度 | +| 需要测试 | API 调用、状态机 | 提取以实现隔离 | +| 第三方集成 | 数据分析、WebSocket | 提取以实现抽象 | +| Store 访问模式 | 页面图标、侧边栏分类 | 提取为 Store 桥接组合式函数 | +| 横切关注点 | 事件总线、网络状态 | 提取以保持一致性 | + +## 提取模式 + +### 模式 1:简单工具组合式函数 + +无状态或最小状态,单一用途: + +```typescript +// hooks/web/useDesign.ts +import variables from '@/styles/global.module.less' + +export const useDesign = () => { + const lessVariables = variables + + const getPrefixCls = (scope: string) => { + return `${lessVariables.namespace}-${scope}` + } + + return { + variables: lessVariables, + simplePrefixCls: lessVariables.miNamespace, + getPrefixCls + } +} +``` + +### 模式 2:Store 桥接组合式函数 + +将 store 访问封装在清晰的 API 之后。这是实际项目中最具影响力的提取模式: + +```typescript +// hooks/web/useEngine.ts +import { useAppStoreWithOut } from '@/store/modules/app' +import { SEARCH_ENGINE_INFO, SEARCH_ENGINE_ORDER } from '@/config/setting' + +export const useEngine = () => { + const appStore = useAppStoreWithOut() + + // 当前选中搜索引擎 + const selectEngine = computed(() => appStore.selectEngine) + + // 当前设定搜索引擎列表(过滤后) + const searchEngine = computed(() => + SEARCH_ENGINE_ORDER.filter((engine) => appStore.searchEngine.includes(engine)) + ) + + // 当前选中搜索引擎详细信息 + const engineInfo = computed(() => SEARCH_ENGINE_INFO[selectEngine.value]) + + // 更新搜索引擎 + const updateSelectEngine = (val: SearchEngine) => { + appStore.setSelectEngine(val) + } + + // 下一个搜索引擎 + const nextEngine = () => { + const idx = appStore.searchEngine.indexOf(selectEngine.value) + if (idx === appStore.searchEngine.length - 1) { + appStore.setSelectEngine(appStore.searchEngine[0]) + } else { + appStore.setSelectEngine(appStore.searchEngine[idx + 1]) + } + } + + return { selectEngine, engineInfo, searchEngine, nextEngine, updateSelectEngine } +} +``` + +**为什么 Store 桥接很重要:** +- 组件不需要 `import { useAppStore }` 和了解 store 结构 +- 业务规则(例如 `SEARCH_ENGINE_ORDER.filter`)集中在一处 +- 可以轻松替换 store 实现,无需修改组件 +- 通过 `useXxxStoreWithOut` 可在组件外使用 + +### 模式 3:基于事件的组合式函数 + +管理副作用并自动清理: + +```typescript +// hooks/web/useNetwork.ts +import { ref, onBeforeUnmount } from 'vue' + +export const useNetwork = () => { + const online = ref(true) + + const updateNetwork = () => { + online.value = navigator.onLine + } + + window.addEventListener('online', updateNetwork) + window.addEventListener('offline', updateNetwork) + + onBeforeUnmount(() => { + window.removeEventListener('online', updateNetwork) + window.removeEventListener('offline', updateNetwork) + }) + + return { online } +} +``` + +### 模式 4:参数化组合式函数 + +接受配置以实现灵活性: + +```typescript +// hooks/web/useCoordinateArea.ts +interface Coordinate { x1: number; y1: number; x2: number; y2: number } +type DirectionX = 'ltr' | 'rtl' +type DirectionY = 'ttb' | 'btt' + +export const useCoordinateArea = ( + coordinate: Coordinate, + direction: DirectionX = 'ltr', + directionY: DirectionY = 'ttb' +) => { + const { width, height } = useWindowSize() + const { x, y } = useMouse() + + const { x1, y1, x2, y2 } = coordinate + + const inCoordinateX = computed(() => + direction === 'ltr' + ? x.value > x1 && x.value < x2 + : x.value > width.value - x2 && x.value < width.value - x1 + ) + + const inCoordinateY = computed(() => + directionY === 'ttb' + ? y.value > y1 && y.value < y2 + : y.value > height.value - y2 && y.value < height.value - y1 + ) + + const inCoordinate = computed(() => inCoordinateX.value && inCoordinateY.value) + + return { inCoordinate } +} +``` + +### 模式 5:第三方集成组合式函数 + +用 Vue 友好的 API 封装第三方库: + +```typescript +// hooks/web/useCache.ts +import WebStorageCacheCrypto from 'web-storage-cache-crypto' +import sm4 from '@/utils/cipher/sm4' + +type CacheType = 'localStorage' | 'sessionStorage' + +export const CACHE_KEY = { + LANG: 'miao-lang', + DICT_CACHE: 'dictCache', + MIAOWING_APP: 'miaowing-app', + MIAOWING_BUSINESS: 'miaowing-business' +} + +export const useCache = (type: CacheType = 'localStorage', crypt: boolean = true) => { + const wsCache = new WebStorageCacheCrypto({ + storage: type, + crypt: Boolean(crypt), + encrypt: sm4.encrypt, + decrypt: sm4.decrypt + }) + + return { wsCache } +} +``` + +## 目录结构 + +``` +src/ +├── hooks/ # 组合式函数目录 +│ ├── event/ # 事件相关的组合式函数 +│ │ └── useScrollTo.ts # 平滑滚动 +│ └── web/ # Web API 与业务逻辑组合式函数 +│ ├── useCache.ts # 加密存储 +│ ├── useCoordinateArea.ts # 鼠标位置检测 +│ ├── useDesign.ts # CSS 命名空间 +│ ├── useEmitt.ts # 事件总线 +│ ├── useEngine.ts # 搜索引擎(Store 桥接) +│ ├── useI18n.ts # i18n 命名空间封装 +│ ├── useLocale.ts # 语言切换 +│ ├── useLocalForage.ts # IndexedDB 存储 +│ ├── useNetwork.ts # 网络状态 +│ ├── usePageIcon.ts # 页面图标(Store 桥接) +│ ├── useSideCategory.ts # 侧边栏分类(Store 桥接) +│ ├── useSuggestion.ts # 搜索建议 JSONP +│ └── useTimeAgo.ts # 相对时间 +``` + +**命名规范:** +- 文件名与函数名一致:`useEngine.ts` → `export const useEngine = () => {}` +- 按领域分组:`event/` 用于 DOM 事件,`web/` 用于 Web API 和业务逻辑 +- 每个文件一个组合式函数 + +## 最佳实践 + +### 1. 单一职责 + +```typescript +// ✅ 好 — 只专注一件事 +export function useLocalStorage(key: string, defaultValue: T) { + const stored = localStorage.getItem(key) + const data = ref(stored ? JSON.parse(stored) : defaultValue) + + watch(data, (newValue) => { + localStorage.setItem(key, JSON.stringify(newValue)) + }, { deep: true }) + + return { data } +} + +// ❌ 差 — 混合了多个关注点 +export function useUserStorageAndAuth() { + // 太多职责混在一起 +} +``` + +> **注意:** 上述 `useLocalStorage` 直接使用了浏览器 API。如果你的项目需要支持 SSR,应从 `@vueuse/core` 引入 `useStorage`,它会自动处理非浏览器环境。 + +### 2. 接受 Ref 以保持响应性 + +```typescript +// ✅ 好 — 同时接受原始值和 ref +export function useSearch(query: MaybeRef) { + const results = ref([]) + + watch( + () => toValue(query), + async (q) => { + results.value = await searchAPI(q) + } + ) + + return { results } +} + +// 用法 +const query = ref('') +const { results } = useSearch(query) // 响应式! +const { results } = useSearch('static') // 也可以工作 +``` + +### 3. 返回响应式引用 + +```typescript +// ✅ 好 — 返回 ref 用于模板绑定 +export function useTimer() { + const seconds = ref(0) + const isRunning = ref(false) + return { seconds, isRunning, start, stop } +} + +// ❌ 差 — 返回普通值,丢失响应性 +export function useTimer() { + let seconds = 0 + return { seconds } // 不是响应式的! +} +``` + +### 4. 自动清理 + +始终在 `onBeforeUnmount` 中清理副作用: + +```typescript +// ✅ 好 — 卸载时清理 +export const useNetwork = () => { + const online = ref(true) + const update = () => { online.value = navigator.onLine } + + window.addEventListener('online', update) + window.addEventListener('offline', update) + + onBeforeUnmount(() => { + window.removeEventListener('online', update) + window.removeEventListener('offline', update) + }) + + return { online } +} +``` + +## 参考 + +- [Vue.js Composables](https://vuejs.org/guide/reusability/composables.html) +- [VueUse - Collection of Vue Composition Utilities](https://vueuse.org/) +- [Vue.js Composition API FAQ](https://vuejs.org/guide/extras/composition-api-faq.html) diff --git a/skills/vue-composition-api-best-practices/reference/reactivity-performance.md b/skills/vue-composition-api-best-practices/reference/reactivity-performance.md new file mode 100644 index 0000000..b8c2733 --- /dev/null +++ b/skills/vue-composition-api-best-practices/reference/reactivity-performance.md @@ -0,0 +1,621 @@ +# 响应性与性能 + +Vue 3 Composition API 响应式与性能优化最佳实践。 + +--- + +## 1. ref vs shallowRef vs reactive + +### 选择决策树 + +``` +需要响应式? +├── 是 → 数据是基本类型? +│ ├── 是 → ref +│ └── 否 → 数据层级深? +│ ├── 浅层即可 → shallowRef +│ └── 需要深层 → ref(或 reactive) +└── 否 → 不需要响应式 → 普通变量 / shallowRef +``` + +### 对比表 + +| API | 响应深度 | 触发更新方式 | 适用场景 | +|-----|---------|-------------|---------| +| `ref` | 深层 | 自动 | 通用场景,对象属性变更需触发更新 | +| `shallowRef` | 浅层(.value) | 手动 `triggerRef` | 大型对象、动态组件、性能敏感场景 | +| `reactive` | 深层 | 自动 | 不需要重新赋值的对象 | + +### ⚠️ reactive 的局限性 + +虽然 `reactive` 在某些场景下很方便,但 Vue 3 官方更推荐用 `ref` 作为主要响应式 API: + +```typescript +// ❌ 1. 不能重新赋值 -- 整个替换会丢失响应式 +let state = reactive({ count: 0 }) +state = reactive({ count: 1 }) // 响应式连接断开! + +// ✅ 用 ref 没问题 +const state = ref({ count: 0 }) +state.value = { count: 1 } // 正常触发更新 + +// ❌ 2. 解构丢失响应式 +const { count } = reactive({ count: 0 }) // count 变成了普通数字 + +// ✅ 用 toRefs 保持响应式 +const { count } = toRefs(reactive({ count: 0 })) + +// ❌ 3. 不支持基本类型 +const count = reactive(0) // 类型错误! + +// ✅ 基本类型用 ref +const count = ref(0) +``` + +**经验法则:** 新代码优先使用 `ref`,仅在明确需要"对象属性级响应式且确定不会重新赋值"时使用 `reactive`。 + +### 实际案例:动态组件切换 + +```typescript +// ✅ GOOD: 使用 shallowRef 避免组件对象的深层响应式开销 +function usePage() { + const activeCom = shallowRef() + const isPure = computed(() => appStore.pure) + + watchEffect(() => { + // 组件对象不需要深层响应式,shallowRef 足矣 + activeCom.value = isPure.value ? PureMode : HomeMode + }) + + return { activeCom } +} +``` + +```typescript +// ❌ BAD: 使用 ref 对组件对象做深层响应式,无意义且浪费性能 +function usePage() { + const activeCom = ref() // 会递归遍历组件对象的所有属性 + // ... +} +``` + +### shallowRef 手动触发更新 + +```typescript +const list = shallowRef(['a', 'b', 'c']) + +// ❌ 不会触发更新:修改数组内部不会被追踪 +list.value.push('d') + +// ✅ 触发更新:替换整个 .value +list.value = [...list.value, 'd'] + +// ✅ 触发更新:使用 triggerRef +list.value.push('d') +triggerRef(list) +``` + +### markRaw — 标记对象永不转为响应式 + +当确定某个对象不需要响应式时(如第三方库实例、大型静态数据),用 `markRaw` 标记它。这可以防止 Vue 的响应式系统意外地将其深层代理,避免性能浪费: + +```typescript +import { markRaw, reactive, shallowRef } from 'vue' + +// ❌ BAD: 第三方库实例被意外代理 +const mapInstance = new Map() // 被 reactive 包装,产生大量 proxy 开销 + +// ✅ GOOD: 标记为永不代理 +const mapInstance = markRaw(new Map()) +const state = reactive({ + map: mapInstance // map 本身不会被代理 +}) + +// ✅ GOOD: 标记大型静态数据 +const largeStaticConfig = markRaw({ + // 数千行配置数据... +}) +const appState = shallowRef({ + config: largeStaticConfig // config 不会被 deep-track +}) +``` + +**何时使用 `markRaw`:** +- 第三方库实例(如 Leaflet 地图、Monaco Editor、ECharts 实例) +- 大型静态数据对象(如国家/地区列表、字典数据) +- 已经冻结的数据(`Object.freeze`) +- 在 `pinia` persist 中不需要持久化的运行时对象 + +**⚠️ 注意:** `markRaw` 是永久性的,标记后无法撤销。被标记的对象在 `reactive`/`ref` 中会被视为非响应式。 + +--- + +## 2. computed 缓存优化 + +### computed 的缓存特性 + +- 只在依赖变化时重新计算 +- 多次访问只计算一次 +- 适合派生状态和昂贵计算 + +### 何时用 computed vs 方法 + +```typescript +// ✅ GOOD: 派生状态用 computed,有缓存 +const filteredList = computed(() => + list.value.filter(item => item.active) +) + +// ❌ BAD: 用方法返回派生值,每次调用都重新计算 +function getFilteredList() { + return list.value.filter(item => item.active) +} +``` + +### computed 写入(双向绑定) + +```typescript +const keyword = computed({ + get: () => searchStore.keyword, + set: (val: string) => { searchStore.keyword = val } +}) +``` + +### 避免在 computed 中产生副作用 + +```typescript +// ❌ BAD: computed 中有副作用 +const userInfo = computed(() => { + fetchUserInfo() // 每次依赖变化都会请求 + return userStore.info +}) + +// ✅ GOOD: 用 watch 处理副作用 +const userInfo = computed(() => userStore.info) +watch(userId, (newId) => { + fetchUserInfo(newId) +}, { immediate: true }) +``` + +--- + +## 3. watch 优化 + +### watch vs watchEffect + +| API | 依赖追踪 | 访问旧值 | 精确控制 | 适用场景 | +|-----|---------|---------|---------|---------| +| `watch` | 显式指定 | ✅ | ✅ | 需要旧值对比、精确监听 | +| `watchEffect` | 自动追踪 | ❌ | ❌ | 副作用与响应式源直接关联 | + +### watch 的精确控制 + +```typescript +// ✅ GOOD: 精确监听特定属性 +watch( + () => appStore.theme, + (newTheme) => { applyTheme(newTheme) } +) + +// ❌ BAD: 监听整个 store,任何变化都触发 +watch( + () => appStore, + () => { applyTheme(appStore.theme) }, + { deep: true } // 深层监听开销大 +) +``` + +### 常用选项 + +```typescript +watch(source, callback, { + immediate: true, // 创建时立即执行一次 + deep: false, // 避免深层监听(默认 false) + once: true, // 只触发一次后自动停止(Vue 3.4+) + flush: 'post', // DOM 更新后执行(需要访问更新后的 DOM 时使用) +}) +``` + +### watch 中清理副作用 + +```typescript +watch(id, (newId, oldId, onCleanup) => { + const controller = new AbortController() + + fetch(`/api/user/${newId}`, { signal: controller.signal }) + .then(res => res.json()) + .then(data => { user.value = data }) + + // id 变化时取消上一次请求 + onCleanup(() => controller.abort()) +}) +``` + +### onWatcherCleanup (Vue 3.5+) + +**Vue 3.5 引入了 `onWatcherCleanup()`** — 可以在 `watchEffect` 内部调用的清理函数(之前 `watchEffect` 不支持 `onCleanup` 回调参数): + +```typescript +import { watchEffect, onWatcherCleanup } from 'vue' + +// ✅ Vue 3.5+: watchEffect 内部也能注册清理函数 +watchEffect(() => { + const controller = new AbortController() + + fetch(`/api/user/${userId.value}`, { signal: controller.signal }) + .then(res => res.json()) + .then(data => { user.value = data }) + + // userId 变化或组件卸载时自动取消请求 + onWatcherCleanup(() => controller.abort()) +}) +``` + +**对比 `watch` 的 `onCleanup` 参数:** + +| 特性 | `watch(fn, (_, __, onCleanup) => {})` | `watchEffect(() => { onWatcherCleanup(...) })` | +|------|--------------------------------------|----------------------------------------------| +| 可用性 | Vue 3.0+ | Vue 3.5+ | +| 清理触发时机 | 下次执行前 + 卸载时 | 下次执行前 + 卸载时 | +| 使用方式 | 回调参数 | 独立函数调用 | +| 适用场景 | 精确监听 + 清理 | 自动追踪 + 清理 | + +**为什么需要 `onWatcherCleanup`:** 之前 `watchEffect` 无法注册清理函数,导致在 effect 中发起的异步请求无法在新请求发起前自动取消,容易产生竞态条件。 + +--- + +## 4. 事件监听清理 + +### 组件级自动清理 + +```typescript +// ✅ GOOD: 在 composable 中使用生命周期钩子自动清理 +import { onUnmounted } from 'vue' +import { mittBus } from '@/utils/mitt' + +export function useEmitt() { + const listeners: Array<{ event: string; handler: Function }> = [] + + function on(event: string, handler: Function) { + mittBus.on(event, handler as any) + listeners.push({ event, handler }) + } + + onUnmounted(() => { + listeners.forEach(({ event, handler }) => { + mittBus.off(event, handler as any) + }) + listeners.length = 0 + }) + + return { on, emit: mittBus.emit } +} +``` + +> 另见:[组合式函数设计模式 - 模式 3](composable-design-patterns.md#模式-3生命周期感知) 和 [跨功能依赖 - 模式 4](cross-feature-dependencies.md#模式-4事件总线模式适用于复杂的多对多场景) 了解 `useEmitt` 的更多使用场景。 + +### DOM 事件清理 + +```typescript +// ✅ 使用 VueUse 的 useEventListener 自动清理 +import { useEventListener } from '@vueuse/core' + +export function useNetwork() { + const isOnline = ref(navigator.onLine) + // 自动在卸载时移除监听 + useEventListener(window, 'online', () => (isOnline.value = true)) + useEventListener(window, 'offline', () => (isOnline.value = false)) + return { isOnline } +} +``` + +### 手动清理模式 + +```typescript +// 对于不支持生命周期钩子的场景,提供 stop 函数 +export function useInterval(fn: () => void, delay: number) { + let timer: ReturnType | null = null + + function start() { + stop() + timer = setInterval(fn, delay) + } + + function stop() { + if (timer) { + clearInterval(timer) + timer = null + } + } + + onUnmounted(stop) + + return { start, stop } +} +``` + +--- + +## 5. 组件懒加载 + +### defineAsyncComponent + +```typescript +import { defineAsyncComponent } from 'vue' + +// ✅ GOOD: 懒加载重型组件 +const HeavyChart = defineAsyncComponent(() => + import('@/components/Chart/src/HeavyChart.vue') +) +``` + +### 动态 import + shallowRef + +```typescript +// ✅ GOOD: 条件加载组件 +const activeCom = shallowRef() + +watchEffect(async () => { + if (condition.value) { + const mod = await import('./HeavyComponent.vue') + activeCom.value = mod.default + } else { + activeCom.value = LightComponent + } +}) +``` + +### Suspense 配合 + +```vue + +``` + +--- + +## 6. v-once 与 v-memo + +### v-once:只渲染一次 + +```vue + +``` + +### v-memo:条件记忆 + +```vue + +``` + +--- + +## 7. 列表渲染优化 + +### key 的正确使用 + +```vue + +
+ + +
+``` + +### 虚拟列表 + +当列表项超过 100 个时,使用虚拟滚动: + +```typescript +// 推荐 vueuse/useVirtualList 或第三方库 +import { useVirtualList } from '@vueuse/core' + +const { list, containerProps, wrapperProps } = useVirtualList( + largeList, + { itemHeight: 48, overscan: 10 } +) +``` + +--- + +## 8. 响应式解包注意事项 + +### 模板自动解包 + +在模板中,`ref` 自动解包,不需要 `.value`: + +```vue + +``` + +### reactive 内的 ref 自动解包 + +```typescript +const state = reactive({ + count: ref(0), + name: 'test' +}) + +// ✅ reactive 对象中 ref 自动解包 +console.log(state.count) // 0,不需要 state.count.value +``` + +### 非响应式对象中的 ref 不解包 + +```typescript +const map = new Map>() +map.set('a', ref(1)) + +// ❌ 非 reactive 对象,不会自动解包 +console.log(map.get('a')) // Ref 对象,需要 .value +console.log(map.get('a')!.value) // 1 +``` + +--- + +## 9. effectScope — 管理多个 Composable 的生命周期 + +当多个 composable 需要同时创建和销毁时,`effectScope` 可以批量管理它们的响应式 effect: + +### 基础用法 + +```typescript +import { effectScope, ref, watchEffect, onScopeDispose } from 'vue' + +// 创建独立作用域 +const scope = effectScope() + +scope.run(() => { + // 在这个作用域内创建的所有 effect、watch、computed + // 都会关联到此 scope + const count = ref(0) + + watchEffect(() => { + console.log(`Count: ${count.value}`) + }) + + // 注册作用域销毁时的清理函数 + onScopeDispose(() => { + console.log('Scope disposed') + }) +}) + +// 一次性停止作用域内的所有 effect +scope.stop() +// 输出: "Scope disposed" +// 所有 watchEffect 停止 +``` + +### 实际场景:Composable 工厂 + +```typescript +// hooks/web/useControlledEffects.ts +import { effectScope, ref, watch } from 'vue' + +export function useControlledEffects() { + let scope: ReturnType | null = effectScope() + const isActive = ref(true) + + function run(setup: () => void) { + scope?.run(() => { + setup() + }) + } + + function restart() { + scope?.stop() + scope = effectScope() + isActive.value = false + nextTick(() => (isActive.value = true)) + } + + onBeforeUnmount(() => { + scope?.stop() + scope = null + }) + + return { run, restart, isActive } +} +``` + +**何时使用 `effectScope`:** +- 需要在组件外手动管理多个 effect 的生命周期(如插件、指令) +- 实现"批量创建/销毁"模式(如路由切换时清理上一页所有 effect) +- 编写 composable 测试时隔离 effect + +**何时不需要:** +- 直接在组件内使用 composable — 组件卸载时自动清理 +- 单个 `watch` / `watchEffect` — 返回的 `stop` 函数足矣 + +> 另见:[组合式函数测试](composable-design-patterns.md#9-测试-composable) 了解如何在测试中使用 `effectScope` 隔离 effect。 + +--- + +## 10. Store 性能优化 + +### storeToRefs 避免额外响应式 + +```typescript +import { storeToRefs } from 'pinia' + +const store = useAppStore() + +// ✅ GOOD: storeToRefs 只提取响应式属性,不触发额外响应式包装 +const { theme, pure } = storeToRefs(store) + +// ❌ BAD: 解构丢失响应式 +const { theme, pure } = store // 失去响应式 + +// ❌ BAD: toRefs 对 store 实例做额外包装 +const { theme, pure } = toRefs(store) // 不必要,用 storeToRefs +``` + +### 按需访问 Store 属性 + +```typescript +// ✅ GOOD: computed 精确追踪 +const theme = computed(() => appStore.theme) + +// ❌ BAD: 解构整个 store 导致所有属性变化都触发重渲染 +const store = useAppStore() +const { theme, pure, layout, ... } = storeToRefs(store) // 过度解构 +``` + +--- + +## 11. 性能检查清单 + +### 组件级 + +- [ ] 大型数据使用 `shallowRef` 而非 `ref` +- [ ] 动态组件使用 `shallowRef` +- [ ] 派生状态用 `computed`,不用方法 +- [ ] 避免在 `computed` 中产生副作用 +- [ ] 列表使用唯一 `key` +- [ ] 重型组件懒加载 +- [ ] 非响应式对象使用 `markRaw` 标记 + +### 副作用清理 + +- [ ] 事件监听器在 `onUnmounted` 中移除 +- [ ] 定时器在 `onUnmounted` 中清除 +- [ ] JSONP 脚本在完成/超时后移除 +- [ ] `watch` 返回的 `stop` 函数在适当时机调用 +- [ ] `watchEffect` 中使用 `onWatcherCleanup()` 清理异步请求(3.5+) + +### Store 使用 + +- [ ] 组件内用 `useXxxStore()`,组件外用 `useXxxStoreWithOut()` +- [ ] 解构 store 用 `storeToRefs` +- [ ] 避免深层 `watch` store +- [ ] 不暴露整个 store 实例 + +### 响应式选择 + +- [ ] 基本类型用 `ref` +- [ ] 不需要深层响应的大型对象用 `shallowRef` +- [ ] 需要旧值对比用 `watch`,否则用 `watchEffect` +- [ ] 模板中不加 `.value` +- [ ] 多个 composable 需统一生命周期管理时考虑 `effectScope` diff --git a/skills/vue-composition-api-best-practices/reference/script-setup-best-practices.md b/skills/vue-composition-api-best-practices/reference/script-setup-best-practices.md new file mode 100644 index 0000000..c9451ab --- /dev/null +++ b/skills/vue-composition-api-best-practices/reference/script-setup-best-practices.md @@ -0,0 +1,571 @@ +--- +title: script setup Best Practices +impact: HIGH +impactDescription: 滥用 script setup 特性会导致类型安全问题、运行时错误以及更难维护的代码 +type: best-practice +tags: [vue3, composition-api, script-setup, typescript, best-practices] +--- + +# script setup 最佳实践 + +**影响程度:高** — ` +``` + +**在真实项目中的重要性:** +- Vue DevTools 显示组件名称而非 `` +- `` 可以正常工作 +- 递归组件可以引用自身 +- 调试堆栈信息具有可读性 +- 更易于在代码库中按组件名称搜索 + +## TypeScript 集成 + +### 带类型的 Props + +**BAD — 运行时声明:** + +```vue + +``` + +**GOOD — 基于类型的声明:** + +```vue + +``` + +### 带类型的 Emits + +**BAD — 无类型安全:** + +```vue + +``` + +**GOOD — 类型安全的 emits:** + +```vue + +``` + +## defineModel 实现双向绑定 + +**Vue 3.4+ 特性:** + +```vue + + + +``` + +**Vue 3.4 之前(手动实现):** + +```vue + +``` + +## Store 访问模式 + +### 在 Vue 组件内 — 使用 `useXxxStore()` + +```vue + +``` + +### 在 Vue 组件外 — 使用 `useXxxStoreWithOut()` + +```typescript +// ✅ Good - 在 utils、hooks、plugins 中使用 WithOut 版本 +import { useAppStoreWithOut } from '@/store/modules/app' + +export const useEngine = () => { + const appStore = useAppStoreWithOut() // 传入全局 pinia 实例 + // ... +} +``` + +```typescript +// ✅ Good - 在 utils/migration.ts 中 +import { useAppStoreWithOut } from '@/store/modules/app' + +export async function migrateOnlineIcons() { + const appStore = useAppStoreWithOut() + // 可以在组件上下文之外访问 store +} +``` + +```typescript +// ✅ Good - 在 plugins/vue-i18n/index.ts 中 +import { useLocaleStoreWithOut } from '@/store/modules/locale' + +export async function setupI18n(app: App) { + const localeStore = useLocaleStoreWithOut() + // ... +} +``` + +**区分两者的原因:** +- `useXxxStore()` 依赖 Vue 的 `inject`/`provide`,仅在组件上下文中可用 +- `useXxxStoreWithOut(store)` 显式传入 pinia 实例,可在任意位置使用 +- 使用错误会导致运行时错误:`getActivePinia was called with no active Pinia` + +详见 [store-without-pattern](store-without-pattern.md)。 + +## 常见模式 + +### 带默认值的响应式 Props + +```vue + +``` + +### 暴露组件方法 + +```vue + +``` + +### 模板引用 + +```vue + + + +``` + +### useTemplateRef(Vue 3.5+) + +**Vue 3.5 引入了 `useTemplateRef()`** — 一种类型安全的替代方案,用于替代普通的 `ref()` 来获取模板引用。它解决了尴尬的 `null` 初始化问题,并提供更好的类型推导: + +```vue + + + +``` + +**与 `ref` 的关键区别:** + +| 特性 | `ref` | `useTemplateRef()` | +|---------|-------------------|-----------------------| +| 初始值 | 必须指定 `null` | 自动推导,无需手动设置 null | +| 挂载后的类型 | `T \| null` | `T \| undefined` | +| 需要 ref 名称匹配 | 手动(靠约定) | 通过字符串参数强制匹配 | +| 支持 v-for | ✅ `ref([])` | ✅ `useTemplateRef('list')` | + +### useId(Vue 3.5+) + +**Vue 3.5 引入了 `useId()`**,用于生成唯一的、SSR 安全的 ID。对于无障碍访问(`aria-labelledby`、`for`/`id` 关联)和避免 ID 冲突至关重要: + +```vue + + + +``` + +**为什么不使用 `Math.random()` 或计数器?** +- `useId()` 是 SSR 安全的 — 服务端和客户端生成匹配的 ID +- 不会在组件实例之间产生冲突 +- 在客户端导航(SPA 路由切换)之间会清除 + +### Provide/Inject 与 TypeScript + +`provide`/`inject` 是避免 props 逐层传递的强大工具,但类型安全需要明确的模式: + +**步骤 1:定义 `InjectionKey`** + +```typescript +// types/injection-keys.ts +import type { InjectionKey, Ref } from 'vue' + +// 类型化的 injection key +export const THEME_KEY: InjectionKey> = Symbol('theme') +export const CONFIG_KEY: InjectionKey = Symbol('config') +``` + +**步骤 2:带类型安全地 provide** + +```vue + +``` + +**步骤 3:带类型安全地 inject** + +```vue + +``` + +| 模式 | 返回类型 | 使用场景 | +|---------|------------|-------------| +| `inject(key)` 配合 `InjectionKey` | `T`(不可为 null) | 祖先组件中存在 provider | +| `inject(key, default)` | `T`(不可为 null) | provider 可能不存在 | +| `inject('key')` | `T \| undefined` | 旧的字符串 key 模式 | + +**⚠️ 在 TypeScript 中不要使用纯字符串进行 provide/inject** — 你会失去所有类型安全和 IDE 自动补全。 + +### toValue() vs unref() + +**Vue 3.3 引入了 `toValue()`**,作为解包 "MaybeRef" 值的首选方式: + +```typescript +import { toValue, unref } from 'vue' +import type { MaybeRef } from 'vue' + +// 两者都可以标准化 ref 和普通值 +const a = ref(42) +const b = 100 + +toValue(a) // 42 — 解包 ref,透传普通值 +toValue(b) // 100 + +unref(a) // 42 — 行为相同 +unref(b) // 100 + +// 但 toValue() 有一个关键区别: +// toValue() 还可以解包 getter(返回值的函数) +const getter = () => 42 +toValue(getter) // 42 ✅ +unref(getter) // () => 42 ❌(不会调用 getter) +``` + +**经验法则:** +- 如果你的组合式函数接受 `MaybeRef` — 使用 `toValue()` 来标准化 +- 如果你只处理 `Ref` 对象 — `unref()` 也可以 +- 新代码应优先使用 `toValue()` 以保持向前兼容 + +## 应避免的反模式 + +### ❌ 不要与 Options API 混用 + +```vue + + + + +``` + +### ❌ 不要解构 Props + +```vue + +``` + +### ❌ 不要在 script setup 中使用 `this` + +```vue + +``` + +### ❌ 不要忘记 ref 的 `.value` + +```vue + + + +``` + +### ❌ 不要在组件外使用 `useXxxStore()` + +```typescript +// ❌ Bad - 在 utils/hooks/plugins 中会抛出错误 +import { useAppStore } from '@/store/modules/app' +export function someUtil() { + const store = useAppStore() // Error: pinia is not defined +} + +// ✅ Good - 使用 WithOut 版本 +import { useAppStoreWithOut } from '@/store/modules/app' +export function someUtil() { + const store = useAppStoreWithOut() // 正常工作! +} +``` + +## 性能提示 + +### 对大型对象和动态组件使用 shallowRef + +```vue + +``` + +### 对派生状态使用 computed + +```vue + +``` + +## 参考资料 + +- [Vue.js script setup](https://vuejs.org/api/sfc-script-setup.html) +- [Vue.js TypeScript with Composition API](https://vuejs.org/guide/typescript/composition-api.html) +- [Vue.js defineModel](https://vuejs.org/api/sfc-script-setup.html#definemodel) +- [Vue.js defineOptions](https://vuejs.org/api/sfc-script-setup.html#defineoptions) +- [Vue.js useTemplateRef](https://vuejs.org/api/composition-api-helpers.html#usetemplateref) +- [Vue.js useId](https://vuejs.org/api/composition-api-helpers.html#useid) +- [Vue.js provide/inject](https://vuejs.org/guide/components/provide-inject.html#working-with-reactivity) +- [Vue.js toValue](https://vuejs.org/api/reactivity-utilities.html#tovalue) diff --git a/skills/vue-composition-api-best-practices/reference/sfc-code-organization.md b/skills/vue-composition-api-best-practices/reference/sfc-code-organization.md new file mode 100644 index 0000000..89ee573 --- /dev/null +++ b/skills/vue-composition-api-best-practices/reference/sfc-code-organization.md @@ -0,0 +1,335 @@ +--- +title: SFC Code Organization Order +impact: HIGH +impactDescription: 代码组织混乱会导致维护困难、难以理解组件结构,以及团队成员之间代码风格不一致 +type: best-practice +tags: [vue3, composition-api, script-setup, code-organization, maintainability] +--- + +# SFC 代码组织顺序 + +**影响等级:高** - 良好组织的 SFC(单文件组件)对可维护性和团队协作至关重要。遵循一致的顺序使代码可预测且易于导航。 + +## 任务清单 + +- [ ] 遵循标准的 SFC 代码组织顺序 +- [ ] 使用 useXxx 函数按功能分组相关代码 +- [ ] 将 Vue 公共项(options、props、emits 等)放在顶部 +- [ ] 将功能实现放在底部,IDE 中默认折叠 +- [ ] 使用清晰的区块注释进行分隔 + +## 问题所在 + +` +``` + +**GOOD - 组织良好的代码:** + +```vue + +``` + +## 标准组织顺序 + +| 顺序 | 区块 | 是否必须 | 描述 | +|-------|---------|----------|-------------| +| 1 | `defineOptions` | ✅ 推荐 | 组件名称(DevTools、keep-alive、递归组件) | +| 2 | `defineProps` | 可选 | 带类型声明的组件 props | +| 3 | `defineModel` | 可选 | 双向绑定 model(Vue 3.4+) | +| 4 | `inject` | 可选 | 注入的依赖 | +| 5 | `defineEmits` | 可选 | 带类型声明的组件事件 | +| 6 | Store 声明 | 可选 | Pinia store 实例(`useXxxStore()`) | +| 7 | 外部 hooks | 可选 | 导入的组合式函数 | +| 8 | 功能声明 | 可选 | `const { ... } = useFeature()` | +| 9 | `provide` | 可选 | 提供的依赖 | +| 10 | `defineExpose` | 可选 | 暴露的公共 API | +| 11 | 功能实现 | 按需 | `function useFeature() {}` | + +## 区块注释风格 + +使用清晰、简洁的区块注释。两种常见风格: + +### 风格一:简洁中文注释(推荐中文团队使用) + +```typescript +// 组件名 +defineOptions({ name: 'Layout' }) + +// props +const props = defineProps<{ id: string }>() + +// emits +const emit = defineEmits<{ update: [value: string] }>() + +// store +const appStore = useAppStore() + +// 外部 hooks +const { t } = useI18n() + +// 功能声明 +const { imageBgUrl, videoBgUrl } = useBackground() + +// ============ 功能实现 ============ + +function useBackground() { /* ... */ } +``` + +### 风格二:分隔线风格(适用于大型组件) + +```typescript +// ============ Vue 公共项 ============ + +defineOptions({ name: 'UserComponent' }) + +// ============ Store ============ + +const appStore = useAppStore() + +// ============ 外部 Hooks ============ + +const { t } = useI18n() + +// ============ 功能声明 ============ + +const { search, results } = useSearch() + +// ============ 功能实现 ============ + +function useSearch() { /* ... */ } +``` + +## 收益 + +1. **结构可预测**:团队成员知道在哪里找到特定代码 +2. **快速概览**:顶部区域让组件接口一目了然 +3. **IDE 导航**:点击声明中的函数名即可跳转到实现 +4. **默认折叠**:功能实现保持折叠状态,减少视觉干扰 +5. **依赖清晰**:一眼看清每个功能返回和消费了什么 +6. **Store 聚合**:所有 store 实例集中声明,便于识别 + +## 适用场景 + +- **始终遵循**:对所有 ` +``` + +## 参考 + +- [Vue.js Composition API FAQ](https://vuejs.org/guide/extras/composition-api-faq.html) +- [Vue.js script setup](https://vuejs.org/api/sfc-script-setup.html) +- [官方示例:FileExplorer.vue](https://github.com/vuejs-translations/docs-zh-cn/blob/main/assets/FileExplorer.vue) diff --git a/skills/vue-composition-api-best-practices/reference/store-without-pattern.md b/skills/vue-composition-api-best-practices/reference/store-without-pattern.md new file mode 100644 index 0000000..9858aa8 --- /dev/null +++ b/skills/vue-composition-api-best-practices/reference/store-without-pattern.md @@ -0,0 +1,164 @@ +# Store Without 模式 + +## 问题 + +Pinia 的 `useStore()` 默认依赖 Vue 组件上下文(inject/provide)。在组件外(hooks、utils、plugins、路由守卫、axios 拦截器)直接调用会抛出错误: + +``` +Error: "getActivePinia()" was called but there was no active Pinia. +``` + +## 解决方案:Store Without 模式 + +每个 store 模块额外导出一个 `useXxxStoreWithOut` 函数,接收全局 pinia 实例作为参数,使 store 可在任意上下文中安全访问。 + +### 模式定义 + +```typescript +// store/modules/app.ts +import { defineStore } from 'pinia' +import { store } from '@/store' + +export const useAppStore = defineStore('app', { + // ... store 定义 +}) + +// 在组件外使用时,传入全局 pinia 实例 +export const useAppStoreWithOut = () => { + return useAppStore(store) +} +``` + +### 全局 Pinia 实例 + +```typescript +// store/index.ts +import { createPinia } from 'pinia' + +const pinia = createPinia() + +export default pinia + +// 导出 store 供 Without 函数使用 +export const store = pinia +``` + +## 使用规则 + +### 何时使用哪个 + +| 函数 | 使用场景 | 原因 | +|------|---------|------| +| `useAppStore()` | Vue 组件 ` +``` + +```typescript +// ❌ BAD:组件内使用 WithOut 是多余的 + +``` + +### 在 Hooks / Utils / Plugins 中(必须使用 WithOut) + +```typescript +// ✅ GOOD:hooks 中使用 WithOut +// hooks/web/useSideCategory.ts +import { useBusinessStoreWithOut } from '@/store/modules/business' + +export function useSideCategory() { + const businessStore = useBusinessStoreWithOut() + const categories = computed(() => businessStore.getSideCategory) + return { categories } +} +``` + +```typescript +// ✅ GOOD:utils 中使用 WithOut +// utils/migration.ts +import { useBusinessStoreWithOut } from '@/store/modules/business' + +export async function migrateOnlineIcons() { + const businessStore = useBusinessStoreWithOut() + // ... +} +``` + +```typescript +// ❌ BAD:组件外直接使用标准方式会报错 +// utils/migration.ts +import { useBusinessStore } from '@/store/modules/business' + +export async function migrateOnlineIcons() { + const businessStore = useBusinessStore() // Error: no active Pinia +} +``` + +## 命名规范 + +所有模块遵循统一命名规范: + +| Store 模块 | 标准函数 | WithOut 函数 | +|-----------|---------|-------------| +| `app.ts` | `useAppStore` | `useAppStoreWithOut` | +| `business.ts` | `useBusinessStore` | `useBusinessStoreWithOut` | +| `dict.ts` | `useDictStore` | `useDictStoreWithOut` | +| `locale.ts` | `useLocaleStore` | `useLocaleStoreWithOut` | + +**规则**:`use{ModuleName}StoreWithOut` — 模块名首字母大写 + Store + WithOut(注意大小写)。 + +## 实现清单 + +每个 store 模块必须: + +- [ ] 导出标准 `useXxxStore` 函数(`defineStore` 的返回值) +- [ ] 导出 `useXxxStoreWithOut` 函数,内部调用 `useXxxStore(store)` +- [ ] 从 `@/store` 导入全局 `store` 实例 +- [ ] WithOut 函数放在文件底部,紧跟标准函数之后 + +## 为什么不直接使用 `useXxxStore(pinia)`? + +理论上可以直接调用 `useAppStore(pinia)`,但 `WithOut` 函数提供了: + +1. **语义明确** — 函数名直接表达"在组件外使用"的意图 +2. **统一入口** — 不需要每个调用方都 import `store`,减少依赖 +3. **集中管理** — 如果 pinia 实例获取方式变更,只需改 WithOut 函数 +4. **可搜索** — 搜索 `WithOut` 即可找到所有组件外使用 store 的地方 + +## 替代方案:`storeToRefs` 注意事项 + +注意 `useXxxStoreWithOut()` 返回的是 store 实例,如需解构响应式属性,仍需使用 `storeToRefs`: + +```typescript +import { storeToRefs } from 'pinia' +import { useAppStoreWithOut } from '@/store/modules/app' + +export function useAppInfo() { + const appStore = useAppStoreWithOut() + const { pure, theme } = storeToRefs(appStore) // 保持响应式 + return { pure, theme } +} +``` + +## 总结 + +| 优势 | 说明 | +|------|------| +| 解决组件外访问 | 核心价值,让 store 可在任意上下文使用 | +| 命名约定清晰 | `WithOut` 后缀一目了然 | +| 减少样板代码 | 调用方无需 import store | +| 易于维护 | pinia 实例变更只需改一处 | +| 可追溯 | 搜索 `WithOut` 可定位所有组件外使用 | diff --git a/skills/vue-composition-api-best-practices/reference/use-function-pattern.md b/skills/vue-composition-api-best-practices/reference/use-function-pattern.md new file mode 100644 index 0000000..fee164e --- /dev/null +++ b/skills/vue-composition-api-best-practices/reference/use-function-pattern.md @@ -0,0 +1,358 @@ +--- +title: UseXxx Function Pattern for Feature Encapsulation +impact: HIGH +impactDescription: 如果不使用 useXxx 模式,功能逻辑会变得分散,难以理解每个功能暴露了什么,也难以追踪功能之间的依赖关系 +type: best-practice +tags: [vue3, composition-api, script-setup, use-pattern, code-organization] +--- + +# UseXxx 函数模式:功能封装 + +**影响等级:高** - useXxx 模式将相关逻辑封装到自包含的函数中,使功能易于理解、测试和复用。 + +## 任务清单 + +- [ ] 将功能逻辑封装在 `useFeatureName()` 函数中 +- [ ] 仅返回外部需要的值和方法 +- [ ] 将功能实现放在 script 底部 +- [ ] 在顶部使用解构声明功能使用 +- [ ] 功能函数命名清晰,能反映其用途 + +## 问题所在 + +没有封装时,相关的变量、计算属性、监听器和方法散落在代码各处,难以理解哪些代码属于哪个功能。 + +**BAD - 功能逻辑分散:** + +```vue + +``` + +**GOOD - 使用 useXxx 模式封装:** + +```vue + +``` + +## 核心原则 + +### 1. 清晰的返回接口 + +return 语句记录了该功能暴露了什么: + +```typescript +function useSearch() { + // 内部状态 - 不返回 + const abortController = ref(null) + + // 公共状态 + const searchQuery = ref('') + const searchResults = ref([]) + + // 公共方法 + const handleSearch = async () => { /* ... */ } + + return { + // 只暴露需要的内容 + searchQuery, + searchResults, + handleSearch + } +} +``` + +### 2. 自包含的逻辑 + +每个 useXxx 函数包含所有相关的内容: +- 状态(ref、reactive) +- 计算属性 +- 监听器 +- 生命周期钩子 +- 方法 + +```typescript +function useSearch() { + // 状态 + const query = ref('') + const results = ref([]) + + // 计算属性 + const isEmpty = computed(() => results.value.length === 0) + + // 监听器 + watch(query, debounce(search, 300)) + + // 生命周期 + onMounted(() => { + if (query.value) search() + }) + + // 方法 + async function search() { /* ... */ } + + return { query, results, isEmpty, search } +} +``` + +### 3. 通过参数进行依赖注入 + +将依赖作为参数传递,实现跨功能通信: + +```typescript +function usePagination(options: { + onPageChange?: () => void + initialPage?: number +} = {}) { + const currentPage = ref(options.initialPage ?? 1) + + const goToPage = (page: number) => { + currentPage.value = page + options.onPageChange?.() + } + + return { currentPage, goToPage } +} + +// 使用方式 +const { handleSearch } = useSearch() +const { currentPage, goToPage } = usePagination({ + onPageChange: handleSearch +}) +``` + +### 4. Store 桥接模式 + +当组合式函数封装 store 访问时,需提供干净的接口来隐藏 store 实现细节: + +```typescript +// 页面图标管理 hooks - 封装 store 访问 +export const usePageIcon = () => { + const appStore = useAppStoreWithOut() + + // 当前 page + const curPage = computed(() => appStore.selectCategory.key) + + // 当前 page icons + const curPageIcons = computed(() => appStore.pageIconMap[curPage.value] || []) + + // 新增 page icon + const addPageIcon = (icon: PageItemWithOptionalKey, page?: string) => { + if (!appStore.pageIconMap[page || curPage.value]) { + appStore.addPageIconInfo(page || curPage.value) + } + const icons = appStore.pageIconMap[page || curPage.value] + if (!icon.key) { + icon.key = `${page || curPage.value}-icon-${icon.type}-${icons.length}` + } + appStore.updatePageIconInfo(page || curPage.value, icons.concat(icon as PageItem)) + } + + // 更新 page icon + const updatePageIcon = (icon: PageItem, page: string) => { + const icons = appStore.pageIconMap[page] + const targetIdx = icons.findIndex((i) => i.key === icon.key) + if (targetIdx !== -1) { + icons[targetIdx] = icon + appStore.updatePageIconInfo(page, icons) + } + } + + // 删除 page icon + const removePageIcon = (icon: PageItem, page?: string) => { + const icons = appStore.pageIconMap[page || curPage.value] + if (icons) { + appStore.updatePageIconInfo( + page || curPage.value, + icons.filter((item) => item.key !== icon.key) + ) + } + } + + return { + curPage, + curPageIcons, + addPageIcon, + updatePageIcon, + removePageIcon + } +} +``` + +**Store 桥接模式的收益:** +- 组件无需了解 store 内部结构 +- 业务逻辑集中在一处 +- 便于在无需更新所有组件的情况下更改 store 结构 +- 与 `useXxxStoreWithOut` 无缝配合,适用于非组件场景 + +## 命名约定 + +| 模式 | 示例 | 使用场景 | +|---------|---------|----------| +| `useXxx` | `useSearch()` | 组件内部的功能封装 | +| `useXxx` | `useUserStore()` | 外部组合式函数导入 | +| `useXxxStoreWithOut` | `useAppStoreWithOut()` | 组件外部的 store 访问 | +| `useXxx`(桥接) | `usePageIcon()` | Store 桥接组合式函数 | + +## 何时提取到外部文件 + +满足以下条件时移至外部文件: +- 跨多个组件使用 +- 复杂度足够高,需要单独测试 +- 不依赖父组件状态 +- 作为 store 数据的桥接层 + +```typescript +// hooks/web/usePageIcon.ts +export const usePageIcon = () => { + const appStore = useAppStoreWithOut() + // ... + return { curPageIcons, addPageIcon, updatePageIcon, removePageIcon } +} + +// 在组件中 +import { usePageIcon } from '@/hooks/web/usePageIcon' +const { curPageIcons, addPageIcon } = usePageIcon() +``` + +## 功能实现检查清单 + +编写 `useXxx` 函数时: + +- [ ] 所有相关状态都在函数内部(不散落在外部) +- [ ] 从状态派生的计算属性都在函数内部 +- [ ] 响应状态变化的监听器都在函数内部 +- [ ] 生命周期钩子(`onMounted`、`onBeforeUnmount`)都在函数内部 +- [ ] 清理逻辑(`removeEventListener`、`off`)在 `onBeforeUnmount` 内 +- [ ] 只返回组件需要的内容 +- [ ] 内部实现细节被隐藏 + +## 参考 + +- [Vue.js Composables](https://vuejs.org/guide/reusability/composables.html) +- [Vue.js Composition API FAQ](https://vuejs.org/guide/extras/composition-api-faq.html) diff --git a/skills/vue/.clawhub/origin.json b/skills/vue/.clawhub/origin.json new file mode 100644 index 0000000..5d266ec --- /dev/null +++ b/skills/vue/.clawhub/origin.json @@ -0,0 +1,8 @@ +{ + "version": 1, + "registry": "https://clawhub.ai", + "slug": "vue", + "installedVersion": "1.0.1", + "installedAt": 1779235184751, + "fingerprint": "14a2c1a8f3397ee5141f374e3f3858af15293d50cac50820ae3ea3437b63f36b" +} diff --git a/skills/vue/SKILL.md b/skills/vue/SKILL.md new file mode 100644 index 0000000..f2f01cc --- /dev/null +++ b/skills/vue/SKILL.md @@ -0,0 +1,94 @@ +--- +name: Vue +slug: vue +version: 1.0.1 +description: Build Vue 3 applications with Composition API, proper reactivity patterns, and production-ready components. +metadata: {"clawdbot":{"emoji":"💚","requires":{"bins":["node"]},"os":["linux","darwin","win32"]}} +--- + +## When to Use + +User needs Vue expertise — from Composition API patterns to production optimization. Agent handles reactivity, component design, state management, and performance. + +## Quick Reference + +| Topic | File | +|-------|------| +| Reactivity patterns | `reactivity.md` | +| Component patterns | `components.md` | +| Composables design | `composables.md` | +| Performance optimization | `performance.md` | + +## Composition API Philosophy + +- Composition API is not about replacing Options API—it's about better code organization +- Group code by feature, not by option type—related logic stays together +- Extract reusable logic into composables—the main win of Composition API +- `