前言
隨着 Vue 3 的普及,Composition API 成為了構建複雜應用的主流方式。相比 Options API,Composition API 提供了更好的邏輯組織和複用能力。而自定義 Hooks 正是這一能力的核心體現,它讓我們能夠將業務邏輯抽象成可複用的函數,極大地提升了代碼的可維護性和開發效率。
什麼是自定義 Hooks?
自定義 Hooks 是基於 Composition API 封裝的可複用邏輯函數。它們通常以 use 開頭命名,返回響應式數據、方法或計算屬性。通過自定義 Hooks,我們可以將組件中的邏輯抽離出來,在多個組件間共享。
基本結構
// useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const increment = () => {
count.value++
}
const decrement = () => {
count.value--
}
const doubleCount = computed(() => count.value * 2)
return {
count,
increment,
decrement,
doubleCount
}
}
實戰案例:常用自定義 Hooks
1. 網絡請求 Hook
// useApi.js
import { ref, onMounted } from 'vue'
import axios from 'axios'
export function useApi(url, options = {}) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
const fetchData = async (params = {}) => {
loading.value = true
error.value = null
try {
const response = await axios.get(url, { ...options, params })
data.value = response.data
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
onMounted(() => {
if (options.immediate !== false) {
fetchData()
}
})
return {
data,
loading,
error,
fetchData
}
}
使用示例:
<template>
<div>
<div v-if="loading">加載中...</div>
<div v-else-if="error">{{ error }}</div>
<ul v-else>
<li v-for="item in data" :key="item.id">
{{ item.name }}
</li>
</ul>
<button @click="fetchData">刷新</button>
</div>
</template>
<script setup>
import { useApi } from '@/hooks/useApi'
const { data, loading, error, fetchData } = useApi('/api/users')
</script>
2. 表單驗證 Hook
// useForm.js
import { reactive, computed } from 'vue'
export function useForm(initialValues, rules) {
const formData = reactive({ ...initialValues })
const errors = reactive({})
const validateField = (field) => {
const value = formData[field]
const fieldRules = rules[field] || []
for (const rule of fieldRules) {
if (!rule.validator(value, formData)) {
errors[field] = rule.message
return false
}
}
delete errors[field]
return true
}
const validateAll = () => {
let isValid = true
Object.keys(rules).forEach(field => {
if (!validateField(field)) {
isValid = false
}
})
return isValid
}
const resetForm = () => {
Object.assign(formData, initialValues)
Object.keys(errors).forEach(key => {
delete errors[key]
})
}
const isDirty = computed(() => {
return JSON.stringify(formData) !== JSON.stringify(initialValues)
})
return {
formData,
errors,
validateField,
validateAll,
resetForm,
isDirty
}
}
使用示例:
<template>
<form @submit.prevent="handleSubmit">
<div>
<input
v-model="formData.username"
@blur="() => validateField('username')"
placeholder="用户名"
/>
<span v-if="errors.username" class="error">{{ errors.username }}</span>
</div>
<div>
<input
v-model="formData.email"
@blur="() => validateField('email')"
placeholder="郵箱"
/>
<span v-if="errors.email" class="error">{{ errors.email }}</span>
</div>
<button type="submit" :disabled="!isDirty">提交</button>
<button type="button" @click="resetForm">重置</button>
</form>
</template>
<script setup>
import { useForm } from '@/hooks/useForm'
const { formData, errors, validateField, validateAll, resetForm, isDirty } = useForm(
{ username: '', email: '' },
{
username: [
{
validator: (value) => value.length >= 3,
message: '用户名至少3個字符'
}
],
email: [
{
validator: (value) => /\S+@\S+\.\S+/.test(value),
message: '請輸入有效的郵箱地址'
}
]
}
)
const handleSubmit = () => {
if (validateAll()) {
console.log('表單驗證通過:', formData)
}
}
</script>
3. 防抖節流 Hook
// useDebounce.js
import { ref, watch } from 'vue'
export function useDebounce(value, delay = 300) {
const debouncedValue = ref(value.value)
let timeoutId = null
watch(value, (newValue) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
debouncedValue.value = newValue
}, delay)
})
return debouncedValue
}
// useThrottle.js
export function useThrottle(value, delay = 300) {
const throttledValue = ref(value.value)
let lastTime = 0
watch(value, (newValue) => {
const now = Date.now()
if (now - lastTime >= delay) {
throttledValue.value = newValue
lastTime = now
}
})
return throttledValue
}
4. 本地存儲 Hook
// useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const storedValue = localStorage.getItem(key)
const value = ref(storedValue ? JSON.parse(storedValue) : defaultValue)
watch(value, (newValue) => {
if (newValue === null) {
localStorage.removeItem(key)
} else {
localStorage.setItem(key, JSON.stringify(newValue))
}
}, { deep: true })
const remove = () => {
value.value = null
}
return [value, remove]
}
高級技巧與最佳實踐
1. Hook 組合
// useUserManagement.js
import { useApi } from './useApi'
import { useLocalStorage } from './useLocalStorage'
export function useUserManagement() {
const [currentUser, removeCurrentUser] = useLocalStorage('currentUser', null)
const { data: users, loading, error, fetchData } = useApi('/api/users')
const login = async (credentials) => {
// 登錄邏輯
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
})
const userData = await response.json()
currentUser.value = userData
}
const logout = () => {
removeCurrentUser()
// 其他登出邏輯
}
return {
currentUser,
users,
loading,
error,
login,
logout,
refreshUsers: fetchData
}
}
2. 錯誤處理
// useAsync.js
import { ref, onMounted } from 'vue'
export function useAsync(asyncFunction, immediate = true) {
const result = ref(null)
const loading = ref(false)
const error = ref(null)
const execute = async (...args) => {
loading.value = true
error.value = null
try {
const response = await asyncFunction(...args)
result.value = response
return response
} catch (err) {
error.value = err
throw err
} finally {
loading.value = false
}
}
onMounted(() => {
if (immediate) {
execute()
}
})
return {
result,
loading,
error,
execute
}
}
3. 類型安全(TypeScript)
// useCounter.ts
import { ref, computed, Ref, ComputedRef } from 'vue'
interface UseCounterReturn {
count: Ref<number>
increment: () => void
decrement: () => void
doubleCount: ComputedRef<number>
}
export function useCounter(initialValue: number = 0): UseCounterReturn {
const count = ref(initialValue)
const increment = () => {
count.value++
}
const decrement = () => {
count.value--
}
const doubleCount = computed(() => count.value * 2)
return {
count,
increment,
decrement,
doubleCount
}
}
設計原則與注意事項
1. 單一職責原則
每個 Hook 應該只負責一個特定的功能領域,保持功能單一且專注。
2. 命名規範
- 使用
use前綴 - 名稱清晰表達 Hook 的用途
- 避免過於通用的名稱
3. 返回值設計
- 返回對象而非數組(便於解構時命名)
- 保持返回值的一致性
- 考慮添加輔助方法
4. 性能優化
- 合理使用
watch和computed - 避免不必要的重新計算
- 及時清理副作用
結語
自定義 Hooks 是 Vue 3 Composition API 生態中的重要組成部分,它不僅解決了邏輯複用的問題,更提供了一種更加靈活和可組合的開發模式。通過合理地設計和使用自定義 Hooks,我們可以:
- 提升代碼複用性:將通用邏輯抽象成獨立模塊
- 改善代碼組織:讓組件更加關注視圖邏輯
- 增強可測試性:獨立的邏輯更容易進行單元測試
- 提高開發效率:減少重複代碼編寫
在實際項目中,建議根據業務需求逐步積累和優化自定義 Hooks,建立屬於團隊的 Hooks 庫,這將是提升前端開發質量和效率的重要手段。
記住,好的自定義 Hooks 不僅要解決當前問題,更要具備良好的擴展性和可維護性。隨着經驗的積累,你會發現自己能夠創造出越來越優雅和實用的自定義 Hooks。