VueUse 实战:用好组合式工具函数提升开发效率
VueUse 是 Vue 3 生态中最大的工具函数集,200+ 个组合式函数覆盖了状态管理、网络请求、动画、设备检测等几乎所有场景。但很多人只用了 useDark 和 useStorage。本文从实际项目出发,挑选真正高频、高价值的工具函数深入讲解。
状态与持久化
useStorage:响应式的 localStorage
import { useStorage } from '@vueuse/core'
// 基本用法:自动同步到 localStorage
const theme = useStorage('theme', 'light')
// 支持 sessionStorage、自定义 serializer
const token = useStorage('token', '', sessionStorage)
// 对象类型自动序列化
const user = useStorage('user', { name: '', avatar: '' })
user.value.name = 'John' // 自动写入 localStorage
实战技巧:跨标签页同步
// useStorage 默认监听 storage 事件,多标签页自动同步
const cart = useStorage('cart', [])
// 在标签页 A 修改 cart,标签页 B 的 cart 自动更新
useDark / useColorMode:主题切换
import { useDark, useToggle } from '@vueuse/core'
const isDark = useDark({
selector: 'html',
attribute: 'class',
valueDark: 'dark',
valueLight: ''
})
const toggleDark = useToggle(isDark)
useColorMode 更灵活,支持多主题:
import { useColorMode } from '@vueuse/core'
const mode = useColorMode({
modes: {
light: '',
dark: 'dark',
sepia: 'sepia',
nord: 'nord'
}
})
// mode.value: 'light' | 'dark' | 'sepia' | 'nord'
// 自动持久化,自动切换 class
useLocalStorage 的进阶:useAsyncState
import { useAsyncState } from '@vueuse/core'
const { state, isLoading, error, execute } = useAsyncState(
() => fetch('/api/users').then(r => r.json()),
[], // 初始值
{ delay: 500 } // 延迟显示 loading(防闪烁)
)
// 手动刷新
execute()
网络请求层
useFetch:类型安全的请求封装
import { useFetch } from '@vueuse/core'
const { data, error, isFetching, abort } = useFetch('/api/users', {
immediate: false // 手动触发
}).json<User[]>()
// 手动触发
execute()
// 取消请求
abort()
实战:带防抖的搜索
import { useFetch, useDebounce } from '@vueuse/core'
const keyword = ref('')
const debounced = useDebounce(keyword, 300)
const { data } = useFetch(() => `/api/search?q=${debounced.value}`, {
refetch: true // debounced 变化自动重新请求
}).json<SearchResult[]>()
useEventSource:SSE 流式数据
import { useEventSource } from '@vueuse/core'
const { data, status, close } = useEventSource('/api/stream')
// data 自动更新为最新收到的事件
// status: 'connecting' | 'open' | 'closed'
DOM 与交互
useIntersectionObserver:懒加载和无限滚动
import { useIntersectionObserver } from '@vueuse/core'
// 图片懒加载
const imgRef = ref<HTMLImageElement>()
const isVisible = ref(false)
useIntersectionObserver(
imgRef,
([{ isIntersecting }]) => {
if (isIntersecting) {
isVisible.value = true
}
},
{ threshold: 0.1 }
)
<template>
<img ref="imgRef" :src="isVisible ? src : placeholder" />
</template>
实战:无限滚动
import { useIntersectionObserver } from '@vueuse/core'
const sentinelRef = ref<HTMLElement>()
const list = ref<Item[]>([])
const page = ref(1)
const hasMore = ref(true)
useIntersectionObserver(sentinelRef, async ([{ isIntersecting }]) => {
if (!isIntersecting || !hasMore.value) return
const res = await fetch(`/api/items?page=${page.value}`)
const items = await res.json()
if (items.length === 0) {
hasMore.value = false
return
}
list.value.push(...items)
page.value++
})
<template>
<div v-for="item in list" :key="item.id">{{ item.name }}</div>
<div ref="sentinelRef">
<span v-if="hasMore">加载中...</span>
<span v-else>没有更多了</span>
</div>
</template>
useEventListener:自动清理的事件监听
import { useEventListener } from '@vueuse/core'
// 在组件卸载时自动移除监听
useEventListener(window, 'scroll', handleScroll)
useEventListener(document, 'keydown', (e) => {
if (e.key === 'Escape') closeModal()
})
// 也可以监听 ref
const buttonRef = ref<HTMLButtonElement>()
useEventListener(buttonRef, 'click', handleClick)
useMouse / useMouseInElement:鼠标追踪
import { useMouse, useMouseInElement } from '@vueuse/core'
// 全局鼠标位置
const { x, y, sourceType } = useMouse()
// sourceType: 'mouse' | 'touch' | 'pen'
// 相对于某个元素的鼠标位置
const targetRef = ref<HTMLElement>()
const { elementX, elementY, isOutside } = useMouseInElement(targetRef)
实战:卡片悬浮光效
<script setup lang="ts">
import { useMouseInElement } from '@vueuse/core'
const cardRef = ref<HTMLElement>()
const { elementX, elementY, isOutside } = useMouseInElement(cardRef)
const spotlightStyle = computed(() => ({
opacity: isOutside.value ? 0 : 1,
background: `radial-gradient(240px at ${elementX.value}px ${elementY.value}px, rgba(255,255,255,0.15), transparent 40%)`
}))
</script>
<template>
<div ref="cardRef" class="card">
<div class="spotlight" :style="spotlightStyle" />
<slot />
</div>
</template>
感知与设备检测
useMediaQuery:响应式断点
import { useMediaQuery } from '@vueuse/core'
const isMobile = useMediaQuery('(max-width: 768px)')
// isMobile 是 ref<boolean>,窗口变化时自动更新
const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)')
实战:响应式组件
<script setup>
import { useMediaQuery } from '@vueuse/core'
import { useBreakpoints } from '@vueuse/core'
const breakpoints = useBreakpoints({
mobile: 0,
tablet: 768,
desktop: 1024,
wide: 1440
})
const device = breakpoints.active() // 'mobile' | 'tablet' | 'desktop' | 'wide'
// 根据 device 渲染不同布局
const cols = computed(() => {
switch (device.value) {
case 'mobile': return 1
case 'tablet': return 2
case 'desktop': return 3
case 'wide': return 4
}
})
</script>
usePreferredDark / useNetwork
import { usePreferredDark, useNetwork } from '@vueuse/core'
const prefersDark = usePreferredDark() // ref<boolean>
const { isOnline, type, saveData } = useNetwork()
// isOnline: ref<boolean>
// type: 'wifi' | 'cellular' | 'ethernet' | ...
// saveData: 用户开启了省流模式
// 省流模式下不加图片动画
watch(saveData, (save) => {
if (save) disableAnimations()
})
动画与过渡
useTransition:平滑数值动画
import { useTransition } from '@vueuse/core'
const rawValue = ref(0)
const animatedValue = useTransition(rawValue, {
duration: 800,
transition: TransitionPresets.easeOutCubic
})
// 改变 rawValue,animatedValue 会平滑过渡
rawValue.value = 100
实战:数字滚动效果
<script setup>
import { useTransition, TransitionPresets } from '@vueuse/core'
const props = defineProps<{ value: number }>()
const source = ref(0)
watch(() => props.value, (v) => source.value = v)
const animated = useTransition(source, {
duration: 600,
transition: TransitionPresets.easeOutCubic
})
const display = computed(() => Math.round(animated.value).toLocaleString())
</script>
<template>
<span>{{ display }}</span>
</template>
useIntervalFn / useTimeoutFn:定时器管理
import { useIntervalFn, useTimeoutFn } from '@vueuse/core'
// 倒计时验证码
const countdown = ref(0)
const { pause, resume } = useIntervalFn(() => {
countdown.value--
if (countdown.value <= 0) pause()
}, 1000, { immediateCallback: false })
function sendCode() {
countdown.value = 60
resume()
}
// 自动取消的 timeout
const { start, stop } = useTimeoutFn(() => {
showTooltip.value = false
}, 3000)
逻辑复用模式
组合多个 composable
// 封装一个完整的搜索逻辑
function useSearch() {
const keyword = ref('')
const debounced = useDebounce(keyword, 300)
const { state: results, isLoading } = useAsyncState(
() => fetch(`/api/search?q=${debounced.value}`).then(r => r.json()),
[],
{ watch: [debounced] } // debounced 变化重新执行
)
return { keyword, results, isLoading }
}
在 Nuxt 中自动导入
Nuxt 已经内置了 VueUse,无需手动 import:
<script setup>
// useStorage, useDark 等直接可用
const theme = useStorage('theme', 'light')
const { x, y } = useMouse()
</script>
非 Nuxt 项目需要在 main.ts 中安装插件:
import { createApp } from 'vue'
import VueUse from '@vueuse/core'
import App from './App.vue'
// VueUse 不需要 app.use(),直接 import 使用即可
// 如果需要自动导入,使用 unplugin-auto-import
// vite.config.ts
import AutoImport from 'unplugin-auto-import/vite'
import { VueUseAutoImport } from '@vueuse/auto-import'
export default {
plugins: [
AutoImport({
imports: [VueUseAutoImport]
})
]
}
性能优化类工具
useDebounceFn / useThrottleFn
import { useDebounceFn, useThrottleFn } from '@vueuse/core'
// 防抖:停止输入 300ms 后才执行
const search = useDebounceFn((q: string) => {
fetch(`/api/search?q=${q}`)
}, 300)
// 节流:每 300ms 最多执行一次(滚动场景)
const onScroll = useThrottleFn(() => {
console.log(window.scrollY)
}, 300)
useMemoize:函数结果缓存
import { useMemoize } from '@vueuse/core'
const expensiveCalc = useMemoize((n: number) => {
console.log('calculating...', n)
return heavyComputation(n)
})
expensiveCalc(100) // calculating... 100
expensiveCalc(100) // 缓存命中,不执行计算
总结
VueUse 的价值不只是「少写几行代码」,更在于经过社区验证、处理了边界情况(SSR 兼容、生命周期自动清理、内存泄漏防护)。
当你在项目中反复写 addEventListener + removeEventListener、setInterval + clearInterval、localStorage.getItem + JSON.parse 时,先查一下 VueUse 有没有对应的 composable——大概率有,而且比你自己写的更健壮。
建议把 VueUse 文档加到书签,开发中遇到「需要监听什么」「需要缓存什么」「需要防抖节流」的场景,先去 VueUse 搜一下。
Comments | 4条评论