第10章 UI组件库与工具
第29节 VueUse工具库
10.29.1 常用组合式函数
什么是VueUse?
VueUse是一个基于Vue 3组合式API的实用工具集,提供了大量的组合式函数,涵盖了浏览器API、传感器、状态管理、动画等多个领域。它的设计理念是"实用优先",提供了简单易用的API,帮助开发者快速实现各种功能。
安装VueUse
使用npm安装:
npm install @vueuse/core使用yarn安装:
yarn add @vueuse/core使用pnpm安装:
pnpm add @vueuse/core浏览器API封装
鼠标位置追踪
<template>
<div class="demo-mouse">
<h3>鼠标位置追踪</h3>
<p>X: {{ x }}</p>
<p>Y: {{ y }}</p>
<div
class="mouse-area"
:style="{ cursor: 'crosshair' }"
>
移动鼠标查看坐标变化
</div>
</div>
</template>
<script setup>
import { useMouse } from '@vueuse/core'
// 鼠标位置追踪
const { x, y } = useMouse()
</script>
<style scoped>
.demo-mouse {
max-width: 400px;
margin: 0 auto;
}
.mouse-area {
width: 100%;
height: 200px;
border: 1px solid #eee;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 20px;
background-color: #f5f7fa;
}
</style>本地存储
<template>
<div class="demo-storage">
<h3>本地存储示例</h3>
<el-input
v-model="storedValue"
placeholder="输入内容会自动保存到本地存储"
style="width: 100%; margin-bottom: 20px;"
></el-input>
<el-button type="primary" @click="clearStorage">清除存储</el-button>
<p style="margin-top: 20px; color: #606266;">
刷新页面后,内容会从本地存储恢复
</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useLocalStorage } from '@vueuse/core'
// 本地存储,默认值为'初始值'
const storedValue = useLocalStorage('my-key', '初始值')
const clearStorage = () => {
storedValue.value = ''
}
</script>
<style scoped>
.demo-storage {
max-width: 400px;
margin: 0 auto;
}
</style>防抖函数
<template>
<div class="demo-debounce">
<h3>防抖函数示例</h3>
<el-input
v-model="searchText"
placeholder="输入内容后1秒执行搜索"
style="width: 100%; margin-bottom: 20px;"
></el-input>
<div class="search-result">
<h4>搜索结果:</h4>
<p>{{ searchResult }}</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useDebounceFn } from '@vueuse/core'
const searchText = ref('')
const searchResult = ref('')
// 防抖函数,延迟1000ms执行
const debouncedSearch = useDebounceFn((text) => {
searchResult.value = `搜索了: ${text}`
console.log('搜索:', text)
}, 1000)
// 监听输入变化
searchText.value && debouncedSearch(searchText.value)
</script>
<style scoped>
.demo-debounce {
max-width: 400px;
margin: 0 auto;
}
.search-result {
margin-top: 20px;
padding: 15px;
border: 1px solid #eee;
border-radius: 4px;
background-color: #f5f7fa;
}
</style>传感器相关hooks
网络状态监测
<template>
<div class="demo-network">
<h3>网络状态监测</h3>
<div class="status" :class="{ online: online, offline: !online }">
{{ online ? '在线' : '离线' }}
</div>
<div class="network-info">
<p>IP地址: {{ ip }}</p>
<p>网络类型: {{ networkType }}</p>
<p>有效带宽: {{ downlink }} Mbps</p>
<p>RTT: {{ rtt }} ms</p>
</div>
</div>
</template>
<script setup>
import { useNetwork } from '@vueuse/core'
const {
online,
ip,
networkType,
downlink,
rtt
} = useNetwork()
</script>
<style scoped>
.demo-network {
max-width: 400px;
margin: 0 auto;
}
.status {
width: 100px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 20px;
font-weight: bold;
margin: 20px 0;
}
.status.online {
background-color: #67c23a;
color: white;
}
.status.offline {
background-color: #f56c6c;
color: white;
}
.network-info {
padding: 15px;
border: 1px solid #eee;
border-radius: 4px;
background-color: #f5f7fa;
}
.network-info p {
margin: 5px 0;
}
</style>屏幕尺寸监测
<template>
<div class="demo-screen">
<h3>屏幕尺寸监测</h3>
<div class="screen-info">
<p>屏幕宽度: {{ width }}px</p>
<p>屏幕高度: {{ height }}px</p>
<p>可用宽度: {{ availWidth }}px</p>
<p>可用高度: {{ availHeight }}px</p>
<p>色彩深度: {{ colorDepth }}位</p>
<p>像素比: {{ pixelRatio }}</p>
</div>
</div>
</template>
<script setup>
import { useScreen } from '@vueuse/core'
const {
width,
height,
availWidth,
availHeight,
colorDepth,
pixelRatio
} = useScreen()
</script>
<style scoped>
.demo-screen {
max-width: 400px;
margin: 0 auto;
}
.screen-info {
padding: 15px;
border: 1px solid #eee;
border-radius: 4px;
background-color: #f5f7fa;
}
.screen-info p {
margin: 5px 0;
}
</style>状态管理
计数器
<template>
<div class="demo-counter">
<h3>计数器</h3>
<div class="counter-value">{{ count }}</div>
<div class="counter-buttons">
<el-button type="primary" @click="increment">+</el-button>
<el-button type="danger" @click="decrement">-</el-button>
<el-button @click="reset">重置</el-button>
</div>
<div class="counter-info">
<p>最大值: {{ max }}</p>
<p>最小值: {{ min }}</p>
</div>
</div>
</template>
<script setup>
import { useCounter } from '@vueuse/core'
// 计数器,初始值为0,最小值为0,最大值为10
const { count, increment, decrement, reset, max, min } = useCounter(0, {
min: 0,
max: 10
})
</script>
<style scoped>
.demo-counter {
max-width: 400px;
margin: 0 auto;
text-align: center;
}
.counter-value {
font-size: 48px;
font-weight: bold;
margin: 20px 0;
color: #409eff;
}
.counter-buttons {
display: flex;
gap: 10px;
justify-content: center;
margin-bottom: 20px;
}
.counter-info {
padding: 15px;
border: 1px solid #eee;
border-radius: 4px;
background-color: #f5f7fa;
}
.counter-info p {
margin: 5px 0;
}
</style>时间管理
<template>
<div class="demo-time">
<h3>时间管理</h3>
<div class="time-display">
<p>{{ formattedTime }}</p>
<p>{{ formattedDate }}</p>
</div>
<div class="time-controls">
<el-button type="primary" @click="{ pause(); isPaused = true }" :disabled="isPaused">暂停</el-button>
<el-button type="success" @click="{ resume(); isPaused = false }" :disabled="!isPaused">继续</el-button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useNow } from '@vueuse/core'
const now = useNow()
const isPaused = ref(false)
const formattedTime = computed(() => {
return now.value.toLocaleTimeString()
})
const formattedDate = computed(() => {
return now.value.toLocaleDateString()
})
</script>
<style scoped>
.demo-time {
max-width: 400px;
margin: 0 auto;
text-align: center;
}
.time-display {
padding: 20px;
border: 1px solid #eee;
border-radius: 4px;
background-color: #f5f7fa;
margin-bottom: 20px;
}
.time-display p {
margin: 5px 0;
}
.time-display p:first-child {
font-size: 32px;
font-weight: bold;
color: #409eff;
}
.time-controls {
display: flex;
gap: 10px;
justify-content: center;
}
</style>10.29.2 浏览器API封装
剪贴板操作
<template>
<div class="demo-clipboard">
<h3>剪贴板操作</h3>
<el-input
v-model="clipboardText"
placeholder="输入要复制的内容"
style="width: 100%; margin-bottom: 20px;"
></el-input>
<div class="clipboard-buttons">
<el-button type="primary" @click="copy">复制到剪贴板</el-button>
<el-button @click="paste">从剪贴板粘贴</el-button>
</div>
<div class="clipboard-info" v-if="lastAction">
<p>{{ lastAction }}</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useClipboard } from '@vueuse/core'
const clipboardText = ref('Hello, VueUse!')
const lastAction = ref('')
const { copy, paste } = useClipboard()
const handleCopy = async () => {
await copy(clipboardText.value)
lastAction.value = `已复制到剪贴板: ${clipboardText.value}`
}
const handlePaste = async () => {
const text = await paste()
clipboardText.value = text
lastAction.value = `已从剪贴板粘贴: ${text}`
}
</script>
<style scoped>
.demo-clipboard {
max-width: 400px;
margin: 0 auto;
}
.clipboard-buttons {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.clipboard-info {
padding: 15px;
border: 1px solid #eee;
border-radius: 4px;
background-color: #f5f7fa;
}
</style>滚动操作
<template>
<div class="demo-scroll">
<h3>滚动操作</h3>
<div class="scroll-buttons">
<el-button type="primary" @click="scrollToTop">滚动到顶部</el-button>
<el-button type="success" @click="scrollToBottom">滚动到底部</el-button>
<el-button @click="scrollTo(500)">滚动到500px位置</el-button>
</div>
<div class="scroll-info">
<p>滚动位置: {{ y }}px</p>
<p>是否在顶部: {{ atTop }}</p>
<p>是否在底部: {{ atBottom }}</p>
</div>
<div class="scroll-area" ref="scrollArea">
<div v-for="i in 50" :key="i" class="scroll-item">
项目 {{ i }}
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useScroll } from '@vueuse/core'
const scrollArea = ref(null)
const { y, atTop, atBottom, scrollTo } = useScroll(scrollArea)
const scrollToTop = () => {
scrollTo(0)
}
const scrollToBottom = () => {
scrollTo(scrollArea.value.scrollHeight)
}
</script>
<style scoped>
.demo-scroll {
max-width: 400px;
margin: 0 auto;
}
.scroll-buttons {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.scroll-info {
padding: 15px;
border: 1px solid #eee;
border-radius: 4px;
background-color: #f5f7fa;
margin-bottom: 20px;
}
.scroll-info p {
margin: 5px 0;
}
.scroll-area {
height: 300px;
overflow-y: auto;
border: 1px solid #eee;
border-radius: 4px;
}
.scroll-item {
padding: 10px;
border-bottom: 1px solid #f0f0f0;
}
.scroll-item:last-child {
border-bottom: none;
}
</style>10.29.3 传感器相关hooks
设备方向
<template>
<div class="demo-orientation">
<h3>设备方向</h3>
<div class="orientation-info">
<p>α: {{ alpha }}°</p>
<p>β: {{ beta }}°</p>
<p>γ: {{ gamma }}°</p>
</div>
<div class="orientation-demo">
<div
class="orientation-box"
:style="{
transform: `rotateX(${beta}deg) rotateY(${-gamma}deg) rotateZ(${alpha}deg)`
}"
>
<div class="box-face front">前</div>
<div class="box-face back">后</div>
<div class="box-face left">左</div>
<div class="box-face right">右</div>
<div class="box-face top">上</div>
<div class="box-face bottom">下</div>
</div>
</div>
<p class="orientation-tip">在移动设备上查看效果更佳</p>
</div>
</template>
<script setup>
import { useDeviceOrientation } from '@vueuse/core'
const { alpha, beta, gamma } = useDeviceOrientation()
</script>
<style scoped>
.demo-orientation {
max-width: 400px;
margin: 0 auto;
text-align: center;
}
.orientation-info {
padding: 15px;
border: 1px solid #eee;
border-radius: 4px;
background-color: #f5f7fa;
margin-bottom: 20px;
}
.orientation-info p {
margin: 5px 0;
}
.orientation-demo {
height: 300px;
display: flex;
align-items: center;
justify-content: center;
perspective: 1000px;
margin: 20px 0;
}
.orientation-box {
width: 150px;
height: 150px;
position: relative;
transform-style: preserve-3d;
transition: transform 0.1s ease;
}
.box-face {
position: absolute;
width: 150px;
height: 150px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: white;
border: 2px solid #409eff;
}
.front { background: rgba(64, 158, 255, 0.8); transform: translateZ(75px); }
.back { background: rgba(103, 194, 58, 0.8); transform: rotateY(180deg) translateZ(75px); }
.left { background: rgba(230, 162, 60, 0.8); transform: rotateY(-90deg) translateZ(75px); }
.right { background: rgba(245, 108, 108, 0.8); transform: rotateY(90deg) translateZ(75px); }
.top { background: rgba(144, 147, 153, 0.8); transform: rotateX(90deg) translateZ(75px); }
.bottom { background: rgba(192, 160, 224, 0.8); transform: rotateX(-90deg) translateZ(75px); }
.orientation-tip {
color: #909399;
font-size: 14px;
}
</style>地理位置
<template>
<div class="demo-geolocation">
<h3>地理位置</h3>
<el-button
type="primary"
@click="getLocation"
:loading="loading"
>
获取位置信息
</el-button>
<div class="location-info" v-if="location">
<p>纬度: {{ location.latitude }}</p>
<p>经度: {{ location.longitude }}</p>
<p>精度: {{ location.accuracy }}米</p>
<p v-if="location.altitude">海拔: {{ location.altitude }}米</p>
<p v-if="location.speed">速度: {{ location.speed }}米/秒</p>
</div>
<p class="location-error" v-if="error">{{ error }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useGeolocation } from '@vueuse/core'
const loading = ref(false)
const location = ref(null)
const error = ref('')
const { coords, getLocation: fetchLocation, error: geoError } = useGeolocation()
const getLocation = async () => {
loading.value = true
error.value = ''
try {
await fetchLocation()
location.value = coords.value
} catch (err) {
error.value = '获取位置信息失败,请检查浏览器权限设置'
} finally {
loading.value = false
}
}
</script>
<style scoped>
.demo-geolocation {
max-width: 400px;
margin: 0 auto;
}
.location-info {
padding: 15px;
border: 1px solid #eee;
border-radius: 4px;
background-color: #f5f7fa;
margin: 20px 0;
}
.location-info p {
margin: 5px 0;
}
.location-error {
color: #f56c6c;
margin: 20px 0;
}
</style>10.29.4 动画与状态hooks
动画控制
<template>
<div class="demo-animation">
<h3>动画控制</h3>
<div class="animation-controls">
<el-button type="primary" @click="play" :disabled="isPlaying">播放</el-button>
<el-button type="danger" @click="pause" :disabled="!isPlaying">暂停</el-button>
<el-button @click="reset">重置</el-button>
</div>
<div class="animation-info">
<p>进度: {{ Math.round(progress * 100) }}%</p>
<p>时间: {{ time }}ms</p>
</div>
<div
class="animation-box"
:style="{
transform: `translateX(${progress * 300}px) rotate(${progress * 360}deg)`,
opacity: progress
}"
>
动画元素
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRafFn } from '@vueuse/core'
const progress = ref(0)
const time = ref(0)
const duration = 2000 // 动画持续时间2秒
const isPlaying = ref(false)
const { pause, resume, reset } = useRafFn(({ timestamp, delta }) => {
time.value += delta
progress.value = Math.min(time.value / duration, 1)
if (progress.value >= 1) {
pause()
isPlaying.value = false
}
}, { immediate: false })
const play = () => {
resume()
isPlaying.value = true
}
const handlePause = () => {
pause()
isPlaying.value = false
}
const handleReset = () => {
reset()
progress.value = 0
time.value = 0
isPlaying.value = false
}
</script>
<style scoped>
.demo-animation {
max-width: 500px;
margin: 0 auto;
}
.animation-controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.animation-info {
padding: 15px;
border: 1px solid #eee;
border-radius: 4px;
background-color: #f5f7fa;
margin-bottom: 20px;
}
.animation-info p {
margin: 5px 0;
}
.animation-box {
width: 100px;
height: 100px;
background-color: #409eff;
color: white;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: transform 0.1s ease, opacity 0.1s ease;
}
</style>主题切换
<template>
<div class="demo-theme" :class="{ 'dark-theme': isDark }">
<h3>主题切换</h3>
<el-switch
v-model="isDark"
active-text="深色主题"
inactive-text="浅色主题"
></el-switch>
<div class="theme-content">
<p>这是一个主题切换示例</p>
<p>当前主题: {{ isDark ? '深色' : '浅色' }}</p>
</div>
</div>
</template>
<script setup>
import { useDark, useToggle } from '@vueuse/core'
const isDark = useDark()
const toggleTheme = useToggle(isDark)
</script>
<style scoped>
.demo-theme {
max-width: 400px;
margin: 0 auto;
padding: 20px;
border-radius: 4px;
background-color: #ffffff;
color: #303133;
transition: background-color 0.3s ease, color 0.3s ease;
}
.demo-theme.dark-theme {
background-color: #1a1a1a;
color: #ffffff;
}
.theme-content {
margin-top: 20px;
padding: 15px;
border: 1px solid #eee;
border-radius: 4px;
background-color: #f5f7fa;
transition: background-color 0.3s ease;
}
.demo-theme.dark-theme .theme-content {
background-color: #2c2c2c;
border-color: #404040;
}
</style>最佳实践
按需导入:
- VueUse支持按需导入,只导入需要使用的函数
- 使用Tree Shaking可以自动移除未使用的代码
合理使用:
- 对于简单的功能,可以考虑自己实现,避免引入过多依赖
- 对于复杂的功能,使用VueUse可以提高开发效率
注意性能:
- 某些函数(如传感器相关)可能会消耗较多资源,注意合理使用
- 对于频繁更新的数据,可以考虑使用防抖或节流
版本兼容性:
- 确保使用的VueUse版本与Vue 3版本兼容
- 定期更新VueUse到最新版本,获取新功能和bug修复
类型安全:
- VueUse提供了完整的TypeScript类型支持
- 使用TypeScript可以获得更好的开发体验和类型安全
小结
本节我们学习了VueUse工具库的使用,包括:
- VueUse的安装与基本使用
- 常用组合式函数:浏览器API、传感器、状态管理、动画
- 浏览器API封装:鼠标位置、本地存储、防抖函数
- 传感器相关hooks:网络状态、屏幕尺寸、设备方向、地理位置
- 动画与状态hooks:动画控制、主题切换
VueUse是一个功能丰富、易于使用的工具库,能够帮助我们快速实现各种功能,提高开发效率。通过合理使用VueUse的组合式函数,我们可以减少重复代码,提高代码的可维护性和可读性。
思考与练习
- 安装VueUse并使用useMouse函数实现鼠标位置追踪。
- 使用useLocalStorage函数实现一个简单的待办事项列表,数据持久化到本地存储。
- 使用useDebounceFn函数实现一个搜索框,输入后延迟1秒执行搜索。
- 使用useNetwork函数监测网络状态变化。
- 使用useScroll函数实现滚动到顶部的功能。
- 使用useDark函数实现主题切换功能。