Files
pulse-libs/skills/vue-composition-api-best-practices/reference/feature-extraction.md
T
Pulse ae39e45460 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)
2026-05-19 21:03:25 -03:00

394 lines
11 KiB
Markdown

---
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
<!-- ComponentA.vue -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
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))
</script>
<!-- ComponentB.vue -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
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))
// 使用 width/height 的其他逻辑...
</script>
```
**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
<!-- ComponentA.vue / ComponentB.vue -->
<script setup lang="ts">
import { useWindowSize } from '@/hooks/web/useWindowSize'
const { width, height } = useWindowSize()
</script>
```
## 何时提取
| 信号 | 示例 | 操作 |
|--------|---------|--------|
| 在 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<T>(key: string, defaultValue: T) {
const stored = localStorage.getItem(key)
const data = ref<T>(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<string>) {
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)