前言
随着 Vue 3 的普及,Composition API 成为了构建复杂应用的主流方式。相比 Options API,Composition API 提供了更好的逻辑组织和复用能力。而自定义 Hooks 正是这一能力的核心体现,它让我们能够将业务逻辑抽象成可复用的函数,极大地提升了代码的可维护性和开发效率。
什么是自定义 Hooks?
自定义 Hooks 是基于 Composition API 封装的可复用逻辑函数。它们通常以 use 开头命名,返回响应式数据、方法或计算属性。通过自定义 Hooks,我们可以将组件中的逻辑抽离出来,在多个组件间共享。
基本结构
javascript
// 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
javascript
// 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
}
}
使用示例:
vue
<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
javascript
// 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
}
}
使用示例:
vue
<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
javascript
// 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
javascript
// 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 组合
javascript
// 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. 错误处理
javascript
// 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)
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。