Files
pulse-memory/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

11 KiB
Raw Blame History

title, impact, impactDescription, type, tags
title impact impactDescription type tags
Feature Extraction to Composables MEDIUM 未能提取可复用逻辑将导致代码重复、组件间行为不一致以及更高的维护成本 best-practice
vue3
composition-api
composables
reusability
dry

将功能提取为组合式函数

影响等级:MEDIUM - 将通用功能提取到外部组合式函数中可以促进代码复用、保持一致性和简化测试。

任务清单

  • 识别多个组件中使用的逻辑
  • 提取到 composables/hooks/ 目录
  • 保持组合式函数专注于单一职责
  • 使用参数进行配置和依赖注入
  • 返回响应式引用和方法
  • 考虑使用 Store 桥接模式抽象 store 访问

问题

当相似逻辑在多个组件中重复时,任何 bug 修复或功能增强都必须在多处应用,增加了维护负担和不一致的风险。

BAD - 组件间重复的逻辑:

<!-- 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 - 提取到组合式函数:

// 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 }
}
<!-- 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:简单工具组合式函数

无状态或最小状态,单一用途:

// 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
  }
}

模式 2Store 桥接组合式函数

将 store 访问封装在清晰的 API 之后。这是实际项目中最具影响力的提取模式:

// 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:基于事件的组合式函数

管理副作用并自动清理:

// 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:参数化组合式函数

接受配置以实现灵活性:

// 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 封装第三方库:

// 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.tsexport const useEngine = () => {}
  • 按领域分组:event/ 用于 DOM 事件,web/ 用于 Web API 和业务逻辑
  • 每个文件一个组合式函数

最佳实践

1. 单一职责

// ✅ 好 — 只专注一件事
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 以保持响应性

// ✅ 好 — 同时接受原始值和 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. 返回响应式引用

// ✅ 好 — 返回 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 中清理副作用:

// ✅ 好 — 卸载时清理
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 }
}

参考