一、基础状态 Hook
1. useToggle - 切换布尔状态
ts
import { ref } from 'vue'
export function useToggle(initialValue = false) {
const state = ref(initialValue)
const toggle = () => state.value = !state.value
const set = (value: boolean) => state.value = value
const setTrue = () => state.value = true
const setFalse = () => state.value = false
return {
state,
toggle,
set,
setTrue,
setFalse
}
}
使用示例:
vue
<script setup>
const { state: isOpen, toggle } = useToggle(false)
</script>
<template>
<button @click="toggle">{{ isOpen ? '关闭' : '打开' }}</button>
</template>
二、DOM 相关 Hook
2. useClickOutside - 点击外部区域
ts
import { onMounted, onUnmounted, ref } from 'vue'
export function useClickOutside(targetRef: Ref<HTMLElement | null>, callback: () => void) {
const listener = (event: MouseEvent) => {
if (!targetRef.value || targetRef.value.contains(event.target as Node)) {
return
}
callback()
}
onMounted(() => document.addEventListener('click', listener))
onUnmounted(() => document.removeEventListener('click', listener))
}
使用示例:
vue
<script setup>
import { ref } from 'vue'
const dropdownRef = ref(null)
const isOpen = ref(false)
useClickOutside(dropdownRef, () => {
isOpen.value = false
})
</script>
<template>
<div ref="dropdownRef">
<button @click="isOpen = !isOpen">菜单</button>
<div v-if="isOpen">下拉内容</div>
</div>
</template>
三、网络请求 Hook
3. useFetch - 数据请求
ts
import { ref } from 'vue'
interface UseFetchOptions {
immediate?: boolean
}
export function useFetch<T>(url: string, options: UseFetchOptions = {}) {
const data = ref<T | null>(null)
const error = ref<Error | null>(null)
const loading = ref(false)
const execute = async (params?: Record<string, any>) => {
loading.value = true
error.value = null
try {
const response = await fetch(`${url}?${new URLSearchParams(params)}`)
if (!response.ok) throw new Error(response.statusText)
data.value = await response.json()
} catch (err) {
error.value = err as Error
} finally {
loading.value = false
}
}
if (options.immediate) {
execute()
}
return {
data,
error,
loading,
execute,
refresh: execute
}
}
使用示例:
vue
<script setup>
const { data: users, loading, error } = useFetch('https://api.example.com/users', {
immediate: true
})
</script>
四、浏览器 API Hook
4. useLocalStorage - 本地存储
ts
import { ref, watch } from 'vue'
export function useLocalStorage<T>(key: string, initialValue: T) {
const storedValue = localStorage.getItem(key)
const value = ref<T>(storedValue ? JSON.parse(storedValue) : initialValue)
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
return value
}
使用示例:
vue
<script setup>
const theme = useLocalStorage('theme', 'light')
</script>
<template>
<button @click="theme = theme === 'light' ? 'dark' : 'light'">
切换主题 (当前: {{ theme }})
</button>
</template>
五、UI 交互 Hook
5. useDebounce - 防抖
ts
import { ref, watch, onUnmounted } from 'vue'
export function useDebounce<T>(value: T, delay = 300) {
const debouncedValue = ref(value)
let timeoutId: number | null = null
const clearTimer = () => {
if (timeoutId) {
clearTimeout(timeoutId)
}
}
watch(() => value, (newValue) => {
clearTimer()
timeoutId = setTimeout(() => {
debouncedValue.value = newValue
}, delay)
})
onUnmounted(clearTimer)
return debouncedValue
}
使用示例:
vue
<script setup>
const searchQuery = ref('')
const debouncedQuery = useDebounce(searchQuery, 500)
watch(debouncedQuery, (value) => {
console.log('搜索:', value)
// 执行搜索API请求
})
</script>
<template>
<input v-model="searchQuery" placeholder="搜索...">
</template>
六、时间相关 Hook
6. useInterval - 定时器
ts
import { onMounted, onUnmounted, ref } from 'vue'
export function useInterval(callback: () => void, interval: number) {
const intervalId = ref<number | null>(null)
const start = () => {
stop()
intervalId.value = setInterval(callback, interval)
}
const stop = () => {
if (intervalId.value) {
clearInterval(intervalId.value)
intervalId.value = null
}
}
onMounted(start)
onUnmounted(stop)
return {
start,
stop
}
}
使用示例:
vue
<script setup>
const count = ref(0)
const { stop } = useInterval(() => {
count.value++
}, 1000)
</script>
<template>
<div>计数: {{ count }}</div>
<button @click="stop">停止</button>
</template>
七、表单相关 Hook
7. useForm - 表单管理
ts
import { reactive, toRefs } from 'vue'
interface FormOptions<T> {
initialValues: T
onSubmit: (values: T) => Promise<void> | void
validate?: (values: T) => Record<keyof T, string>
}
export function useForm<T extends Record<string, any>>(options: FormOptions<T>) {
const state = reactive({
values: { ...options.initialValues },
errors: {} as Record<keyof T, string>,
isSubmitting: false
})
const handleChange = (field: keyof T, value: any) => {
state.values[field] = value
if (state.errors[field]) {
delete state.errors[field]
}
}
const handleSubmit = async () => {
if (options.validate) {
state.errors = options.validate(state.values)
if (Object.keys(state.errors).length > 0) {
return
}
}
state.isSubmitting = true
try {
await options.onSubmit(state.values)
} finally {
state.isSubmitting = false
}
}
const resetForm = () => {
state.values = { ...options.initialValues }
state.errors = {} as Record<keyof T, string>
}
return {
...toRefs(state),
handleChange,
handleSubmit,
resetForm
}
}
使用示例:
vue
<script setup>
const { values, errors, isSubmitting, handleChange, handleSubmit } = useForm({
initialValues: {
username: '',
password: ''
},
onSubmit: async (values) => {
console.log('提交表单:', values)
// 调用API
},
validate: (values) => {
const errors = {}
if (!values.username) errors.username = '请输入用户名'
if (!values.password) errors.password = '请输入密码'
return errors
}
})
</script>
<template>
<form @submit.prevent="handleSubmit">
<input
:value="values.username"
@input="e => handleChange('username', e.target.value)"
placeholder="用户名"
>
<div v-if="errors.username">{{ errors.username }}</div>
<input
type="password"
:value="values.password"
@input="e => handleChange('password', e.target.value)"
placeholder="密码"
>
<div v-if="errors.password">{{ errors.password }}</div>
<button type="submit" :disabled="isSubmitting">
{{ isSubmitting ? '提交中...' : '登录' }}
</button>
</form>
</template>
八、响应式工具 Hook
8. useMediaQuery - 媒体查询
ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useMediaQuery(query: string) {
const matches = ref(false)
const mediaQuery = window.matchMedia(query)
const updateMatches = () => {
matches.value = mediaQuery.matches
}
onMounted(() => {
updateMatches()
mediaQuery.addEventListener('change', updateMatches)
})
onUnmounted(() => {
mediaQuery.removeEventListener('change', updateMatches)
})
return matches
}
使用示例:
vue
<script setup>
const isMobile = useMediaQuery('(max-width: 768px)')
</script>
<template>
<div v-if="isMobile">移动端内容</div>
<div v-else>桌面端内容</div>
</template>
九、动画 Hook
9. useAnimationFrame - 动画帧
ts
import { onMounted, onUnmounted, ref } from 'vue'
export function useAnimationFrame(callback: (deltaTime: number) => void) {
const animationId = ref<number | null>(null)
let lastTime = 0
const animate = (time: number) => {
const deltaTime = time - lastTime
lastTime = time
callback(deltaTime)
animationId.value = requestAnimationFrame(animate)
}
const start = () => {
if (animationId.value === null) {
animationId.value = requestAnimationFrame(animate)
}
}
const stop = () => {
if (animationId.value) {
cancelAnimationFrame(animationId.value)
animationId.value = null
}
}
onMounted(start)
onUnmounted(stop)
return {
start,
stop
}
}
使用示例:
vue
<script setup>
const position = ref(0)
const { start } = useAnimationFrame((deltaTime) => {
position.value += deltaTime * 0.01
if (position.value > 200) position.value = 0
})
</script>
<template>
<div
class="box"
:style="{ transform: `translateX(${position}px)` }"
></div>
</template>
十、组合 Hook
10. usePagination - 分页
ts
import { ref, computed } from 'vue'
interface PaginationOptions {
total: number
pageSize?: number
currentPage?: number
}
export function usePagination(options: PaginationOptions) {
const currentPage = ref(options.currentPage || 1)
const pageSize = ref(options.pageSize || 10)
const totalPages = computed(() => Math.ceil(options.total / pageSize.value))
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++
}
}
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--
}
}
const goToPage = (page: number) => {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page
}
}
return {
currentPage,
pageSize,
totalPages,
nextPage,
prevPage,
goToPage
}
}
使用示例:
vue
<script setup>
const { data, total } = await fetchData()
const pagination = usePagination({
total,
pageSize: 20
})
watch(() => pagination.currentPage, () => {
fetchData(pagination.currentPage)
})
</script>
<template>
<div>
<button @click="pagination.prevPage" :disabled="pagination.currentPage === 1">上一页</button>
<span>第 {{ pagination.currentPage }} 页 / 共 {{ pagination.totalPages }} 页</span>
<button @click="pagination.nextPage" :disabled="pagination.currentPage === pagination.totalPages">下一页</button>
</div>
</template>
总结
- 基础状态管理:useToggle
- DOM 交互:useClickOutside
- 数据请求:useFetch
- 浏览器 API:useLocalStorage
- UI 交互:useDebounce
- 定时器:useInterval
- 表单处理:useForm
- 响应式工具:useMediaQuery
- 动画:useAnimationFrame
- 业务逻辑:usePagination
保持 Hook 的单一职责原则,每个 Hook 只关注一个特定功能,这样才能最大化其复用价值。