第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>

最佳实践

  1. 按需导入

    • VueUse支持按需导入,只导入需要使用的函数
    • 使用Tree Shaking可以自动移除未使用的代码
  2. 合理使用

    • 对于简单的功能,可以考虑自己实现,避免引入过多依赖
    • 对于复杂的功能,使用VueUse可以提高开发效率
  3. 注意性能

    • 某些函数(如传感器相关)可能会消耗较多资源,注意合理使用
    • 对于频繁更新的数据,可以考虑使用防抖或节流
  4. 版本兼容性

    • 确保使用的VueUse版本与Vue 3版本兼容
    • 定期更新VueUse到最新版本,获取新功能和bug修复
  5. 类型安全

    • VueUse提供了完整的TypeScript类型支持
    • 使用TypeScript可以获得更好的开发体验和类型安全

小结

本节我们学习了VueUse工具库的使用,包括:

  • VueUse的安装与基本使用
  • 常用组合式函数:浏览器API、传感器、状态管理、动画
  • 浏览器API封装:鼠标位置、本地存储、防抖函数
  • 传感器相关hooks:网络状态、屏幕尺寸、设备方向、地理位置
  • 动画与状态hooks:动画控制、主题切换

VueUse是一个功能丰富、易于使用的工具库,能够帮助我们快速实现各种功能,提高开发效率。通过合理使用VueUse的组合式函数,我们可以减少重复代码,提高代码的可维护性和可读性。

思考与练习

  1. 安装VueUse并使用useMouse函数实现鼠标位置追踪。
  2. 使用useLocalStorage函数实现一个简单的待办事项列表,数据持久化到本地存储。
  3. 使用useDebounceFn函数实现一个搜索框,输入后延迟1秒执行搜索。
  4. 使用useNetwork函数监测网络状态变化。
  5. 使用useScroll函数实现滚动到顶部的功能。
  6. 使用useDark函数实现主题切换功能。
« 上一篇 27-element-plus 下一篇 » 29-axios-encapsulation