Vue Hook Store 设计模式最佳实践指南

发布于:2025-05-30 ⋅ 阅读:(19) ⋅ 点赞:(0)

Vue Hook Store 设计模式最佳实践指南

一、引言

在 Vue 3 组合式 API 与 TypeScript 普及的背景下,Hook Store 设计模式应运而生,它结合了 Vue 组合式 API 的灵活性与状态管理的最佳实践,为开发者提供了一种轻量级、可测试且易于维护的状态管理方案。本文将深入探讨 Vue Hook Store 的设计理念、核心模式与实战技巧,帮助开发者构建高质量的 Vue 应用。

二、Hook Store 设计模式核心概念

2.1 定义与核心优势

Hook Store 是一种基于 Vue 组合式 API 的状态管理模式,它将状态、逻辑与副作用封装在可复用的 hook 中,具有以下优势:

  • 轻量级:无需额外依赖,仅使用 Vue 内置 API
  • 高内聚:状态与逻辑紧密关联,提高代码可维护性
  • 可测试性:纯函数式设计,易于编写单元测试
  • 灵活组合:通过 hook 组合实现复杂状态管理

2.2 与传统状态管理方案对比

特性 Hook Store Vuex/Pinia
学习曲线 中高
代码复杂度 中高
类型推导 优秀 良好
可测试性 优秀 良好
适用场景 中小型项目 / 模块 大型项目

三、Hook Store 基础架构

3.1 基本结构

一个典型的 Hook Store 包含以下部分:

// useCounter.ts
import { ref, computed, watch, type Ref } from 'vue'

export interface CounterState {
  count: number
  title: string
}

export const useCounter = (initialState: CounterState = { count: 0, title: 'Counter' }) => {
  // 状态管理
  const state = ref(initialState) as Ref<CounterState>
  
  // 计算属性
  const doubleCount = computed(() => state.value.count * 2)
  
  // 方法
  const increment = () => {
    state.value.count++
  }
  
  const decrement = () => {
    state.value.count--
  }
  
  // 副作用
  watch(() => state.value.count, (newCount) => {
    console.log(`Count changed to: ${newCount}`)
  })
  
  // 导出状态与方法
  return {
    state,
    doubleCount,
    increment,
    decrement
  }
}

3.2 在组件中使用

<template>
  <div>
    <h1>{{ counterState.title }}</h1>
    <p>Count: {{ counterState.count }}</p>
    <p>Double Count: {{ doubleCount }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
  </div>
</template>

<script setup>
import { useCounter } from './useCounter'

const { state: counterState, doubleCount, increment, decrement } = useCounter()
</script>

四、Hook Store 高级模式

4.1 模块化设计

将不同业务领域的状态拆分为独立的 hook store:

src/
  stores/
    auth/
      useAuth.ts       # 认证状态
      useUserProfile.ts # 用户资料
    products/
      useProducts.ts   # 产品列表
      useCart.ts       # 购物车
    utils/
      useLocalStorage.ts # 本地存储工具

4.2 状态持久化

通过自定义 hook 实现状态持久化:

// utils/useLocalStorage.ts
import { ref, watch, type Ref } from 'vue'

export const useLocalStorage = <T>(key: string, initialValue: T): Ref<T> => {
  const getSavedValue = () => {
    try {
      const saved = localStorage.getItem(key)
      return saved ? JSON.parse(saved) : initialValue
    } catch (error) {
      console.error(error)
      return initialValue
    }
  }
  
  const state = ref(getSavedValue()) as Ref<T>
  
  watch(state, (newValue) => {
    localStorage.setItem(key, JSON.stringify(newValue))
  }, { deep: true })
  
  return state
}

4.3 异步操作处理

在 hook store 中处理 API 请求:

// stores/products/useProducts.ts
import { ref, computed, type Ref } from 'vue'
import { fetchProducts } from '@/api/products'

export interface Product {
  id: number
  name: string
  price: number
}

export interface ProductsState {
  items: Product[]
  loading: boolean
  error: string | null
}

export const useProducts = () => {
  const state = ref<ProductsState>({
    items: [],
    loading: false,
    error: null
  }) as Ref<ProductsState>
  
  const getProducts = async () => {
    state.value.loading = true
    state.value.error = null
  
    try {
      const response = await fetchProducts()
      state.value.items = response.data
    } catch (error: any) {
      state.value.error = error.message
    } finally {
      state.value.loading = false
    }
  }
  
  const addProduct = (product: Product) => {
    state.value.items.push(product)
  }
  
  return {
    state,
    getProducts,
    addProduct
  }
}

4.4 状态共享与全局状态

使用 provide/inject 实现跨组件状态共享:

// stores/useGlobalState.ts
import { provide, inject, ref, type Ref } from 'vue'

const GLOBAL_STATE_KEY = Symbol('globalState')

interface GlobalState {
  theme: 'light' | 'dark'
  isSidebarOpen: boolean
}

export const useProvideGlobalState = () => {
  const state = ref<GlobalState>({
    theme: 'light',
    isSidebarOpen: true
  }) as Ref<GlobalState>
  
  const toggleTheme = () => {
    state.value.theme = state.value.theme === 'light' ? 'dark' : 'light'
  }
  
  const toggleSidebar = () => {
    state.value.isSidebarOpen = !state.value.isSidebarOpen
  }
  
  provide(GLOBAL_STATE_KEY, {
    state,
    toggleTheme,
    toggleSidebar
  })
  
  return {
    state,
    toggleTheme,
    toggleSidebar
  }
}

export const useGlobalState = () => {
  return inject(GLOBAL_STATE_KEY)!
}

在根组件中提供全局状态:

<!-- App.vue -->
<script setup>
import { useProvideGlobalState } from './stores/useGlobalState'

useProvideGlobalState()
</script>

在子组件中使用:

<!-- ChildComponent.vue -->
<script setup>
import { useGlobalState } from './stores/useGlobalState'

const { state, toggleTheme } = useGlobalState()
</script>

五、Hook Store 最佳实践

5.1 设计原则

  1. 单一职责:每个 hook store 只负责一个明确的业务领域
  2. 最小暴露:只暴露必要的状态和方法
  3. 组合优先:通过组合多个 hook store 实现复杂功能
  4. 类型安全:充分利用 TypeScript 提供类型保障

5.2 测试策略

使用 vitest 和 @vue/test-utils 编写单元测试:

// __tests__/useCounter.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { useCounter } from '../useCounter'

describe('useCounter', () => {
  it('should initialize with default values', () => {
    const { state } = useCounter()
    expect(state.value.count).toBe(0)
    expect(state.value.title).toBe('Counter')
  })
  
  it('should increment count', () => {
    const { state, increment } = useCounter()
    increment()
    expect(state.value.count).toBe(1)
  })
  
  it('should decrement count', () => {
    const { state, decrement } = useCounter()
    decrement()
    expect(state.value.count).toBe(-1)
  })
  
  it('should compute double count', () => {
    const { state, doubleCount } = useCounter()
    state.value.count = 5
    expect(doubleCount.value).toBe(10)
  })
  
  it('should log count changes', () => {
    const consoleLogSpy = vi.spyOn(console, 'log')
    const { state } = useCounter()
  
    state.value.count = 10
    expect(consoleLogSpy).toHaveBeenCalledWith('Count changed to: 10')
  
    consoleLogSpy.mockRestore()
  })
})

5.3 性能优化

  1. 使用 shallowRef 代替 ref 存储大型对象,避免深层响应式开销
  2. 使用 readonly 包装状态,防止意外修改
  3. 在大型列表场景中使用 reactive 而非 ref 包裹数组
  4. 使用 computed 缓存复杂计算结果
import { shallowRef, readonly, computed } from 'vue'

export const useLargeDataStore = () => {
  // 使用shallowRef存储大型数据
  const largeList = shallowRef<Item[]>([]) as Ref<Item[]>
  
  // 使用readonly防止外部修改
  const readonlyList = readonly(largeList)
  
  // 使用computed缓存计算结果
  const filteredList = computed(() => 
    largeList.value.filter(item => item.active)
  )
  
  return {
    readonlyList,
    filteredList
  }
}

六、应用案例:完整的 Todo 应用

6.1 项目结构

src/
  stores/
    todos/
      useTodos.ts        # Todo列表管理
      useFilter.ts       # 过滤状态
      useLocalStorage.ts # 本地存储
  components/
    TodoList.vue
    TodoItem.vue
    TodoFilter.vue
  App.vue

6.2 Todo Store 实现

// stores/todos/useTodos.ts
import { ref, computed, type Ref } from 'vue'
import { useLocalStorage } from './useLocalStorage'

export interface Todo {
  id: number
  text: string
  completed: boolean
}

export const useTodos = () => {
  // 使用localStorage持久化存储
  const todos = useLocalStorage<Todo[]>('todos', [])
  
  const addTodo = (text: string) => {
    const newTodo: Todo = {
      id: Date.now(),
      text,
      completed: false
    }
    todos.value.push(newTodo)
  }
  
  const toggleTodo = (id: number) => {
    const todo = todos.value.find(t => t.id === id)
    if (todo) {
      todo.completed = !todo.completed
    }
  }
  
  const deleteTodo = (id: number) => {
    todos.value = todos.value.filter(t => t.id !== id)
  }
  
  const clearCompleted = () => {
    todos.value = todos.value.filter(t => !t.completed)
  }
  
  return {
    todos,
    addTodo,
    toggleTodo,
    deleteTodo,
    clearCompleted
  }
}

6.3 过滤状态管理

// stores/todos/useFilter.ts
import { ref, computed, type Ref } from 'vue'

export type Filter = 'all' | 'active' | 'completed'

export const useFilter = () => {
  const currentFilter = ref<Filter>('all') as Ref<Filter>
  
  const setFilter = (filter: Filter) => {
    currentFilter.value = filter
  }
  
  return {
    currentFilter,
    setFilter
  }
}

6.4 组合使用

<!-- TodoList.vue -->
<template>
  <div>
    <input v-model="newTodoText" @keyup.enter="addTodo" placeholder="Add todo" />
    <button @click="addTodo">Add</button>
  
    <div>
      <FilterButton :filter="'all'" />
      <FilterButton :filter="'active'" />
      <FilterButton :filter="'completed'" />
    </div>
  
    <ul>
      <TodoItem v-for="todo in filteredTodos" :key="todo.id" :todo="todo" />
    </ul>
  
    <button @click="clearCompleted">Clear Completed</button>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import { useTodos } from '@/stores/todos/useTodos'
import { useFilter } from '@/stores/todos/useFilter'
import TodoItem from './TodoItem.vue'
import FilterButton from './FilterButton.vue'

const { todos, addTodo, clearCompleted } = useTodos()
const { currentFilter } = useFilter()
const newTodoText = ref('')

const filteredTodos = computed(() => {
  switch (currentFilter.value) {
    case 'active':
      return todos.value.filter(todo => !todo.completed)
    case 'completed':
      return todos.value.filter(todo => todo.completed)
    default:
      return todos.value
  }
})
</script>

七、总结与最佳实践建议

7.1 适用场景

  • 中小型项目或模块
  • 需要灵活状态管理的场景
  • 追求最小化依赖的项目
  • 对 TypeScript 支持有高要求的项目

7.2 与其他状态管理方案的配合

  • 与 Pinia/Vuex 结合:在大型应用中,核心全局状态使用 Pinia/Vuex,局部状态使用 Hook Store
  • 与 Vue Router 结合:在路由守卫中使用 Hook Store 管理导航状态
  • 与 API 请求库结合:如 axios、fetch,在 Hook Store 中封装 API 请求逻辑

7.3 未来趋势

随着 Vue 3 组合式 API 的普及,Hook Store 设计模式将越来越受欢迎,未来可能会出现更多基于此模式的工具和最佳实践,进一步提升 Vue 应用的开发体验和代码质量。

通过合理应用 Hook Store 设计模式,开发者可以构建更加模块化、可测试和可维护的 Vue 应用,同时充分发挥 Vue 3 组合式 API 的强大功能。


网站公告

今日签到

点亮在社区的每一天
去签到