在 Vue 3 组合式 API 与 TypeScript 普及的背景下,Hook Store 设计模式应运而生,它结合了 Vue 组合式 API 的灵活性与状态管理的最佳实践,为开发者提供了一种轻量级、可测试且易于维护的状态管理方案。本文将深入探讨 Vue Hook Store 的设计理念、核心模式与实战技巧,帮助开发者构建高质量的 Vue 应用。
Hook Store 是一种基于 Vue 组合式 API 的状态管理模式,它将状态、逻辑与副作用封装在可复用的 hook 中,具有以下优势:
特性 | Hook Store | Vuex/Pinia |
---|---|---|
学习曲线 | 低 | 中高 |
代码复杂度 | 低 | 中高 |
类型推导 | 优秀 | 良好 |
可测试性 | 优秀 | 良好 |
适用场景 | 中小型项目 / 模块 | 大型项目 |
一个典型的 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
}
}
{{ counterState.title }}
Count: {{ counterState.count }}
Double Count: {{ doubleCount }}
将不同业务领域的状态拆分为独立的 hook store:
src/
stores/
auth/
useAuth.ts # 认证状态
useUserProfile.ts # 用户资料
products/
useProducts.ts # 产品列表
useCart.ts # 购物车
utils/
useLocalStorage.ts # 本地存储工具
通过自定义 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
}
在 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
}
}
使用 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)!
}
在根组件中提供全局状态:
在子组件中使用:
使用 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()
})
})
shallowRef
代替 ref
存储大型对象,避免深层响应式开销readonly
包装状态,防止意外修改reactive
而非 ref
包裹数组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
}
}
src/
stores/
todos/
useTodos.ts # Todo列表管理
useFilter.ts # 过滤状态
useLocalStorage.ts # 本地存储
components/
TodoList.vue
TodoItem.vue
TodoFilter.vue
App.vue
// 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
}
}
// 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
}
}
随着 Vue 3 组合式 API 的普及,Hook Store 设计模式将越来越受欢迎,未来可能会出现更多基于此模式的工具和最佳实践,进一步提升 Vue 应用的开发体验和代码质量。
通过合理应用 Hook Store 设计模式,开发者可以构建更加模块化、可测试和可维护的 Vue 应用,同时充分发挥 Vue 3 组合式 API 的强大功能。