feat: biblioteca inteligente libs/ + 5 novas skills (20 skills total)

NOVAS SKILLS:
- next-best-practices      v0.1.0  (CLEAN) — Next.js App Router, RSC, caching, data
- nextjs-patterns          v1.0.0  (CLEAN) — Next.js 15: Server Actions, route handlers
- vite                     v1.0.0  (CLEAN) — env vars, aliases, proxy, CJS compat
- uncle-bob                v1.0.0  (CLEAN) — Clean Code, SOLID, Clean Architecture
- clean-code-review        v1.0.0  (CLEAN) — naming, guard clauses, anti-patterns, refactoring
- vue                      v1.0.0  (CLEAN) — Vue framework
- vue-composition-api-best-practices v1.0.0 (CLEAN) — composables, Pinia, reactivity

BIBLIOTECA INTELIGENTE libs/ (10 dominios, 11 arquivos):
- typescript/ — TS safe + generics gotchas
- react/ — Next.js App Router + Vite config
- vue/ — Composition API + Pinia
- linux/ — System diagnostic cheatsheet
- database/ — PostgreSQL + MySQL patterns
- browser/ — Chromium CLI + E2E testing
- security/ — SAST audit (OWASP Top 10)
- best-practices/ — Clean Code + SOLID + Clean Architecture
- deploy/ — Docker multi-stack + OpenClaw ops
- + INDEX.md como guia de navegacao

.learnings/ — LRN-20260519-003 criado (biblioteca compartilhada)
This commit is contained in:
Pulse
2026-05-19 21:03:25 -03:00
parent 22d9f5b21d
commit ae39e45460
83 changed files with 13349 additions and 1 deletions
@@ -0,0 +1,676 @@
# 组合式函数设计模式
基于 Vue 3 Composition API 的组合式函数(composable)设计模式总结。
---
## 1. 目录结构规范
```
src/hooks/
├── web/ # 业务相关 hooks
│ ├── useDesign.ts # 命名空间/样式前缀
│ ├── useEmitt.ts # 事件总线
│ ├── useEngine.ts # 搜索引擎
│ ├── useI18n.ts # 国际化
│ ├── useLocalForage.ts # IndexedDB 存储
│ ├── useLocale.ts # 语言切换
│ ├── useNetwork.ts # 网络状态
│ ├── usePageIcon.ts # 页面图标
│ ├── useSideCategory.ts # 侧边栏分类
│ ├── useSuggestion.ts # 搜索建议
│ └── useTimeAgo.ts # 时间格式化
└── event/ # DOM 事件相关 hooks
└── useScrollTo.ts # 滚动定位
```
**规则**
- 按功能域划分子目录(`web/``event/`
- 每个文件一个 composable,文件名即函数名
- 函数名以 `use` 开头
---
## 2. 五种设计模式
### 模式 1:有状态服务
封装独立的响应式状态和操作逻辑。
```typescript
// hooks/web/useNetwork.ts
import { ref } from 'vue'
export function useNetwork() {
const isOnline = ref(navigator.onLine)
const updateOnline = () => (isOnline.value = navigator.onLine)
window.addEventListener('online', updateOnline)
window.addEventListener('offline', updateOnline)
// 注意:此处未自动清理,因为网络状态是全局性的
// 如果需要组件级清理,参考 Pattern 3
return { isOnline }
}
```
**特征**:维护全局状态,通常不需要组件级清理。
### 模式 2Store 桥接
用 composable 封装 store 访问,隐藏 store 内部实现细节。
```typescript
// hooks/web/usePageIcon.ts
import { computed } from 'vue'
import { useBusinessStoreWithOut } from '@/store/modules/business'
export function usePageIcon() {
const businessStore = useBusinessStoreWithOut()
const pageIcon = computed(() => businessStore.getPageIcon)
function addPageIcon(icon: IconItem) {
businessStore.addPageIcon(icon)
}
function removePageIcon(id: string) {
businessStore.removePageIcon(id)
}
return { pageIcon, addPageIcon, removePageIcon }
}
```
**特征**
- 使用 `useXxxStoreWithOut` 访问 store(因为 hook 可能在组件外使用)
- 对外暴露语义化接口,隐藏 store action 细节
- 不维护自身状态,仅转发 store 数据
**何时使用**:当多个组件需要以相同方式访问同一 store 数据时。
### 模式 3:生命周期感知
自动在组件卸载时清理副作用。
```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 }
}
```
**特征**
- 使用 `onUnmounted` 自动清理
- 内部维护清理队列
- 防止内存泄漏
**何时使用**:涉及事件监听、定时器、DOM 事件等需要清理的副作用。
> 另见:[跨功能依赖 - 模式 4:事件总线模式](cross-feature-dependencies.md#模式-4事件总线模式适用于复杂的多对多场景) 了解此模式在跨组件通信中的实际应用。
### 模式 4:异步资源
封装异步资源加载,提供加载状态。
```typescript
// hooks/web/useLocalForage.ts
import { ref } from 'vue'
import localforage from 'localforage'
export function useLocalForage(storeName: string) {
const store = localforage.createInstance({ name: storeName })
const loading = ref(false)
async function getItem<T>(key: string): Promise<T | null> {
loading.value = true
try {
return await store.getItem<T>(key)
} finally {
loading.value = false
}
}
async function setItem<T>(key: string, value: T): Promise<void> {
loading.value = true
try {
await store.setItem(key, value)
} finally {
loading.value = false
}
}
return { loading, getItem, setItem }
}
```
**特征**
- 提供 `loading` 状态
- `try/finally` 保证状态重置
- 支持泛型返回值
**何时使用**:封装 IndexedDB、fetch、文件读取等异步操作。
### 模式 5:参数化工具
接收参数,返回计算结果或操作函数,不维护持久状态。
```typescript
// hooks/web/useDesign.ts
import { useAppStoreWithOut } from '@/store/modules/app'
export function useDesign(scope: string) {
const appStore = useAppStoreWithOut()
const prefixCls = computed(() => `${appStore.getPrefixCls}-${scope}`)
const variables = computed(() => ({
'--prefix-cls': prefixCls.value,
}))
return { prefixCls, variables }
}
```
```typescript
// hooks/event/useScrollTo.ts
export function useScrollTo() {
function scrollTo(target: HTMLElement, options?: ScrollToOptions) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start',
...options,
})
}
return { scrollTo }
}
```
**特征**
- 纯函数式,无副作用
- 参数决定返回值
- 不需要清理
**何时使用**:工具类逻辑,如样式计算、DOM 操作、格式化函数。
---
## 3. 参数设计原则
### 单一职责参数
```typescript
// ✅ GOOD: 每个参数职责明确
export function useLocalForage(storeName: string) { ... }
// ✅ GOOD: 可选参数用 Options 模式
export function useSuggestion(engine: string, options?: SuggestionOptions) { ... }
```
### Options 模式
当参数超过 2 个时,使用 Options 对象:
```typescript
interface SuggestionOptions {
timeout?: number
maxResults?: number
callbackName?: string
}
export function useSuggestion(engine: string, options?: SuggestionOptions) {
const { timeout = 5000, maxResults = 10, callbackName } = options ?? {}
// ...
}
```
---
## 4. 返回值设计原则
### 最小暴露原则
只返回外部真正需要的:
```typescript
// ✅ GOOD: 只暴露必要的接口
export function usePageIcon() {
const businessStore = useBusinessStoreWithOut()
const pageIcon = computed(() => businessStore.getPageIcon)
function addPageIcon(icon: IconItem) { businessStore.addPageIcon(icon) }
function removePageIcon(id: string) { businessStore.removePageIcon(id) }
return { pageIcon, addPageIcon, removePageIcon }
}
// ❌ BAD: 暴露了整个 store
export function usePageIcon() {
const businessStore = useBusinessStoreWithOut()
return { businessStore } // 调用方可随意修改 store
}
```
### Ref vs Computed
| 返回类型 | 使用场景 | 特征 |
|---------|---------|------|
| `computed` | 派生状态,依赖其他响应式源 | 只读,自动更新,有缓存 |
| `ref` | 独立状态 | 可读写 |
| `readonly(ref)` | 只读状态,内部可修改 | 防止外部篡改 |
```typescript
export function useSideCategory() {
// 派生自 store → computed
const categories = computed(() => businessStore.getSideCategory)
// 独立状态 → ref
const activeId = ref<string>('')
// 只读暴露 → readonly
const isEditing = ref(false)
const editingState = readonly(isEditing)
return { categories, activeId, editingState }
}
```
---
## 5. 类型设计原则
### 泛型约束
```typescript
// ✅ GOOD: 泛型支持不同数据类型
export function useLocalForage(storeName: string) {
async function getItem<T>(key: string): Promise<T | null> { ... }
async function setItem<T>(key: string, value: T): Promise<void> { ... }
return { getItem, setItem }
}
```
### 输入类型严格
```typescript
// ✅ GOOD: 参数类型精确
export function useEngine(engineType: SearchEngineType) { ... }
// ❌ BAD: 参数类型过于宽泛
export function useEngine(engineType: string) { ... }
```
### 返回类型推断
让 TypeScript 自动推断返回类型,除非需要导出:
```typescript
// 一般不需要显式声明返回类型
export function usePageIcon() {
// TypeScript 自动推断返回 { pageIcon: ComputedRef<...>, addPageIcon: (...) => void, ... }
return { pageIcon, addPageIcon, removePageIcon }
}
// 如果其他模块需要使用返回值类型,用 Extract 类型工具
export type PageIconReturn = ReturnType<typeof usePageIcon>
```
---
## 6. 错误处理原则
### 静默失败 vs 抛出异常
| 场景 | 策略 | 原因 |
|------|------|------|
| 数据获取 | 静默失败 + 降级 | 不应阻塞 UI |
| 关键操作 | 抛出异常 | 必须让调用方感知 |
| 生命周期清理 | 静默失败 | 卸载时不应抛错 |
```typescript
// 数据获取:静默失败 + 降级
export function useSuggestion(engine: string) {
const suggestions = ref<string[]>([])
async function fetchSuggestion(keyword: string) {
try {
suggestions.value = await doFetch(keyword)
} catch {
suggestions.value = [] // 降级为空列表
}
}
return { suggestions, fetchSuggestion }
}
// 关键操作:抛出异常
export function useBackup() {
async function exportData(): Promise<Blob> {
const data = await collectData()
if (!data) throw new Error('No data to export')
return packZip(data)
}
return { exportData }
}
```
---
## 7. Composable 与组件的边界
### 放在 Composable 中
- 可复用的状态逻辑
- 与特定 UI 无关的数据转换
- Store 访问桥接
- 浏览器 API 封装
### 放在组件中
- 模板渲染相关计算
- 仅当前组件使用的 UI 状态(如弹窗开关)
- DOM 直接操作(通过 ref
```typescript
// ✅ 放 composable:可复用的搜索引擎逻辑
// hooks/web/useEngine.ts
export function useEngine() {
const businessStore = useBusinessStoreWithOut()
const currentEngine = computed(() => businessStore.getSearchEngine)
function switchEngine() { ... }
return { currentEngine, switchEngine }
}
// ✅ 放组件:仅当前组件使用的弹窗状态
<script setup lang="ts">
const dialogVisible = ref(false)
const openDialog = () => (dialogVisible.value = true)
</script>
```
---
## 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<string[]>([])
const loading = ref(false)
// 清理:JSONP 脚本和超时定时器
let scriptEl: HTMLScriptElement | null = null
let timer: ReturnType<typeof setTimeout> | 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<void>((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
│ └── ...
```