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: numbertitle: 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: numbername: stringprice: number
}export interface ProductsState {items: Product[]loading: booleanerror: string | null
}export const useProducts = () => {const state = ref<ProductsState>({items: [],loading: false,error: null}) as Ref<ProductsState>const getProducts = async () => {state.value.loading = truestate.value.error = nulltry {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 設計原則
- 單一職責:每個 hook store 只負責一個明確的業務領域
- 最小暴露:只暴露必要的狀態和方法
- 組合優先:通過組合多個 hook store 實現復雜功能
- 類型安全:充分利用 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 = 5expect(doubleCount.value).toBe(10)})it('should log count changes', () => {const consoleLogSpy = vi.spyOn(console, 'log')const { state } = useCounter()state.value.count = 10expect(consoleLogSpy).toHaveBeenCalledWith('Count changed to: 10')consoleLogSpy.mockRestore()})
})
5.3 性能優化
- 使用
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}
}
六、應用案例:完整的 Todo 應用
6.1 項目結構
src/stores/todos/useTodos.ts # Todo列表管理useFilter.ts # 過濾狀態useLocalStorage.ts # 本地存儲components/TodoList.vueTodoItem.vueTodoFilter.vueApp.vue
6.2 Todo Store 實現
// stores/todos/useTodos.ts
import { ref, computed, type Ref } from 'vue'
import { useLocalStorage } from './useLocalStorage'export interface Todo {id: numbertext: stringcompleted: 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 的強大功能。