Vue3 自定义Hooks实战:10个常用场景
Vue 3的组合式API让代码复用变得简单很多。自定义Hooks就是其中比较实用的技巧。下面整理了10个开发中经常用到的Hooks,可以直接复制到项目里用。
一、useRequest:请求封装
封装请求的loading、error、data状态,支持手动和自动触发。
// composables/useRequest.ts
import { ref, shallowRef } from 'vue'
export function useRequest<T>(fetcher: () => Promise<T>, options?: { immediate?: boolean }) {
const data = shallowRef<T | undefined>()
const loading = ref(false)
const error = ref<Error | null>(null)
const run = async () => {
loading.value = true
error.value = null
try {
data.value = await fetcher()
return data.value
} catch (e) {
error.value = e as Error
throw e
} finally {
loading.value = false
}
}
if (options?.immediate !== false) {
run()
}
return { data, loading, error, run }
}使用示例:
<script setup lang="ts">
const { data, loading, error, run } = useRequest(() =>
fetch('/api/user').then(r => r.json())
)
</script>
<template>
<div v-if="loading">加载中...</div>
<div v-else-if="error">{{ error.message }}</div>
<div v-else>{{ data }}</div>
</template>二、useDebounce:防抖
防抖的意思是,连续触发时只执行最后一次。比如搜索框输入,用户停下来了才去请求。
// composables/useDebounce.ts
export function useDebounceFn<T extends (...args: unknown[]) => unknown>(
fn: T,
delay = 300
) {
let timer: ReturnType<typeof setTimeout>
return (...args: Parameters<T>) => {
clearTimeout(timer)
timer = setTimeout(() => fn(...args), delay)
}
}使用示例:
<script setup>
const search = (keyword: string) => {
console.log('搜索:', keyword)
}
const debouncedSearch = useDebounceFn(search, 500)
</script>三、useThrottle:节流
节流的意思是,固定时间内只执行一次。比如滚动事件,每隔一段时间触发一次。
// composables/useThrottle.ts
export function useThrottleFn<T extends (...args: unknown[]) => unknown>(
fn: T,
interval = 300
) {
let last = 0
return (...args: Parameters<T>) => {
const now = Date.now()
if (now - last >= interval) {
last = now
fn(...args)
}
}
}四、useLocalStorage:本地存储
把数据自动同步到localStorage,刷新页面后数据还在。
// composables/useLocalStorage.ts
import { ref, watch } from 'vue'
export function useLocalStorage<T>(key: string, defaultValue: T) {
const data = ref<T>(
(() => {
try {
const raw = localStorage.getItem(key)
return raw ? JSON.parse(raw) : defaultValue
} catch {
return defaultValue
}
})()
)
watch(data, (v) => {
localStorage.setItem(key, JSON.stringify(v))
}, { deep: true })
return data
}使用示例:
<script setup>
const settings = useLocalStorage('user-settings', { theme: 'light', fontSize: 14 })
// 修改settings,会自动存到localStorage
</script>五、useClickOutside:点击外部关闭
做下拉框、弹窗、菜单时经常需要:点击元素外面就关闭它。
// composables/useClickOutside.ts
import { onMounted, onUnmounted } from 'vue'
export function useClickOutside(
target: () => HTMLElement | null,
callback: () => void
) {
const handler = (e: MouseEvent) => {
const el = target()
if (el && !el.contains(e.target as Node)) {
callback()
}
}
onMounted(() => document.addEventListener('click', handler))
onUnmounted(() => document.removeEventListener('click', handler))
}使用示例:
<script setup>
import { ref } from 'vue'
const dropdownRef = ref(null)
const showMenu = ref(false)
useClickOutside(() => dropdownRef.value, () => {
showMenu.value = false
})
</script>
<template>
<div ref="dropdownRef" v-if="showMenu">下拉菜单</div>
</template>六、useInfiniteScroll:无限滚动
滚动到页面底部时自动加载更多数据。
// composables/useInfiniteScroll.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useInfiniteScroll(
loadMore: () => Promise<void>,
options?: { root?: Element | null; threshold?: number }
) {
const loading = ref(false)
const sentinel = ref<HTMLElement | null>(null)
const observer = new IntersectionObserver(
async (entries) => {
if (loading.value || !entries[0]?.isIntersecting) return
loading.value = true
try {
await loadMore()
} finally {
loading.value = false
}
},
{ root: options?.root ?? null, threshold: options?.threshold ?? 0 }
)
onMounted(() => {
if (sentinel.value) observer.observe(sentinel.value)
})
onUnmounted(() => observer.disconnect())
return { sentinel, loading }
}七、useClipboard:剪贴板
复制文本到剪贴板,带成功失败的提示状态。
// composables/useClipboard.ts
import { ref } from 'vue'
export function useClipboard() {
const copied = ref(false)
const copy = async (text: string) => {
try {
await navigator.clipboard.writeText(text)
copied.value = true
setTimeout(() => {
copied.value = false
}, 2000)
return true
} catch {
return false
}
}
return { copied, copy }
}八、useDarkMode:暗色模式
切换网站的主题色,并把用户的选择存下来。
// composables/useDarkMode.ts
import { ref, watch } from 'vue'
export function useDarkMode() {
const isDark = ref(
typeof document !== 'undefined' &&
document.documentElement.classList.contains('dark')
)
watch(isDark, (v) => {
const root = document.documentElement
if (v) {
root.classList.add('dark')
} else {
root.classList.remove('dark')
}
localStorage.setItem('theme', v ? 'dark' : 'light')
}, { immediate: true })
const toggle = () => {
isDark.value = !isDark.value
}
return { isDark, toggle }
}使用时需要在全局CSS中定义暗色模式的样式。
九、useCounter:计数器
简单的数字加减,带最小值和最大值的限制。
// composables/useCounter.ts
import { ref, computed } from 'vue'
export function useCounter(initial = 0, options?: { min?: number; max?: number }) {
const count = ref(initial)
const inc = () => {
if (options?.max !== undefined && count.value >= options.max) return
count.value++
}
const dec = () => {
if (options?.min !== undefined && count.value <= options.min) return
count.value--
}
const reset = () => {
count.value = initial
}
return {
count: computed(() => count.value),
inc,
dec,
reset,
}
}十、useForm:表单校验
管理表单数据和校验错误。
// composables/useForm.ts
import { reactive, toRefs } from 'vue'
export function useForm<T extends Record<string, unknown>>(
initial: T,
validate?: (values: T) => Record<keyof T, string | undefined>
) {
const form = reactive({ ...initial }) as T
const errors = reactive({} as Record<keyof T, string>)
const validateForm = () => {
if (!validate) return true
const res = validate(form)
let valid = true
for (const k of Object.keys(res) as (keyof T)[]) {
errors[k] = res[k] ?? ''
if (res[k]) valid = false
}
return valid
}
const reset = () => {
Object.assign(form, initial)
Object.keys(errors).forEach((k) => {
errors[k as keyof T] = ''
})
}
return { ...toRefs(form), errors, validateForm, reset }
}常用场景速查
| Hook | 使用场景 |
|---|---|
| useRequest | 接口请求,管理loading和error |
| useDebounce | 搜索输入、窗口resize |
| useThrottle | 滚动事件、鼠标移动 |
| useLocalStorage | 用户设置、浏览记录 |
| useClickOutside | 下拉框、弹窗、右键菜单 |
| useInfiniteScroll | 商品列表、文章列表 |
| useClipboard | 复制链接、复制代码 |
| useDarkMode | 深色浅色主题切换 |
| useCounter | 购物车数量、分页页码 |
| useForm | 登录注册、信息填写 |
自定义Hooks的核心是把业务逻辑从组件里抽出来。这样代码更干净,也好复用。写组件的时候如果发现逻辑有点复杂,就可以考虑拆成Hook。
本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!