139 @vueuse/core依赖介绍使用

VueUse 常见能力详解

1. VueUse 是什么

@vueuse/core 是基于 Vue Composition API 构建的实用工具库。

它把常见的浏览器 API、DOM 操作、事件监听、定时器、本地存储、网络通信等能力,封装成符合 Vue 响应式风格的组合式函数(Composable)。

可以把它理解为:

Vue Composition API 的常用工具箱。

例如,原生方式监听窗口尺寸变化:

ts 复制代码
import { onMounted, onUnmounted } from 'vue'

function handleResize() {
  console.log(window.innerWidth)
}

onMounted(() => {
  window.addEventListener('resize', handleResize)
})

onUnmounted(() => {
  window.removeEventListener('resize', handleResize)
})

使用 VueUse:

ts 复制代码
import { useEventListener } from '@vueuse/core'

useEventListener(window, 'resize', () => {
  console.log(window.innerWidth)
})

VueUse 通常能帮我们处理:

  1. 将结果转换为 Vue 响应式数据。
  2. 在组件挂载时注册监听。
  3. 在组件卸载时自动清理监听器、观察器和定时器。
  4. 提供 stoppauseresumeexecute 等控制方法。
  5. 统一浏览器 API 与 Vue Composition API 的使用方式。

2. VueUse 是否只支持 Vue 3

需要根据 VueUse 版本判断:

  • VueUse 9.13.0 同时支持 Vue 2 和 Vue 3。
  • VueUse 11.x 是最后一批支持 Vue 2 的版本。
  • VueUse 12 及之后只支持 Vue 3。

当前项目实际使用的是 VueUse 9.13.0,因此下面的 API 和示例主要以 9.13.0 为准。

官方文档:


3. VueUse 的基本使用方式

VueUse 一般采用按需导入:

ts 复制代码
import {
  onClickOutside,
  useElementSize,
  useEventListener,
  useLocalStorage,
  useWindowSize,
} from '@vueuse/core'

大多数 VueUse 函数会返回以下内容之一:

  • 响应式 ref
  • computed
  • 状态对象
  • 启动、暂停、停止等控制函数
  • 浏览器原始对象

例如:

ts 复制代码
const { width, height } = useWindowSize()

这里的 widthheight 是:

ts 复制代码
Ref<number>

在 JavaScript 中需要访问 .value

ts 复制代码
console.log(width.value)

在 Vue 模板中会自动解包:

vue 复制代码
<template>
  <div>
    当前窗口:{{ width }} × {{ height }}
  </div>
</template>

第一梯队:非常常见

4. useEventListener:通用事件监听

4.1 解决什么问题

useEventListener 是响应式版本的:

ts 复制代码
target.addEventListener()
target.removeEventListener()

它负责:

  • 在合适的生命周期注册事件。
  • 组件卸载时自动移除事件。
  • 当目标 ref 发生变化时,解绑旧元素并绑定新元素。
  • 提供手动停止监听的方法。

4.2 监听 window

ts 复制代码
import { useEventListener } from '@vueuse/core'

useEventListener(window, 'resize', () => {
  console.log('窗口尺寸发生变化')
})

4.3 监听 document

ts 复制代码
useEventListener(document, 'click', (event) => {
  console.log('页面被点击', event.target)
})

4.4 监听 DOM 元素

vue 复制代码
<script setup lang="ts">
import { ref } from 'vue'
import { useEventListener } from '@vueuse/core'

const buttonRef = ref<HTMLElement | null>(null)

useEventListener(buttonRef, 'click', () => {
  console.log('按钮被点击')
})
</script>

<template>
  <button ref="buttonRef">
    点击
  </button>
</template>

4.5 手动停止监听

ts 复制代码
const stop = useEventListener(window, 'resize', handleResize)

// 不再需要时手动停止
stop()

正常情况下组件卸载时会自动停止,不一定需要手动调用。

手动停止适合:

  • 某个业务阶段结束后立即取消监听。
  • 弹窗关闭后不再监听。
  • 地图工具退出后释放事件。
  • 监听器只需要执行一段时间。

4.6 常见应用场景

  • 监听 window.resize
  • 监听 document.click
  • 监听鼠标移动
  • 监听滚轮
  • 监听键盘
  • 监听 Cesium 容器
  • 监听第三方库创建的 DOM
  • 监听视频、音频事件

4.7 与原生事件相比的优势

原生写法:

ts 复制代码
onMounted(() => {
  window.addEventListener('resize', handleResize)
})

onUnmounted(() => {
  window.removeEventListener('resize', handleResize)
})

VueUse 写法:

ts 复制代码
useEventListener(window, 'resize', handleResize)

VueUse 减少了生命周期管理代码,也降低了忘记清理事件造成内存泄漏的风险。

官方文档:

useEventListener


5. useLocalStorage:响应式永久本地存储

5.1 解决什么问题

useLocalStorage 把 Vue 的响应式状态与浏览器 localStorage 双向绑定。

ts 复制代码
import { useLocalStorage } from '@vueuse/core'

const theme = useLocalStorage('app-theme', 'light')

修改响应式变量:

ts 复制代码
theme.value = 'dark'

浏览器中会自动保存:

text 复制代码
app-theme = dark

刷新页面后,状态仍然保留。

5.2 保存对象

ts 复制代码
const mapSettings = useLocalStorage('map-settings', {
  showLabels: true,
  opacity: 1,
  baseMap: 'satellite',
})

修改对象:

ts 复制代码
mapSettings.value.opacity = 0.5
mapSettings.value.showLabels = false

数据会自动序列化后写入 localStorage

5.3 保存数组

ts 复制代码
const recentProjects = useLocalStorage<string[]>(
  'recent-projects',
  [],
)

recentProjects.value.push('project-001')

5.4 常见应用场景

  • 用户主题设置
  • 表格列配置
  • 用户筛选条件
  • 地图底图设置
  • 地图图层显示偏好
  • 最近访问项目
  • 是否已经看过提示
  • 页面布局偏好
  • 非敏感草稿

5.5 注意事项

不要在 localStorage 中保存:

  • 用户密码
  • 高敏感业务数据
  • 不能暴露的长期凭证
  • 特别大的数据
  • 大型 GeoJSON
  • 文件二进制内容

还要注意:

  • Date 序列化后通常变成字符串。
  • MapSet、类实例可能丢失类型。
  • 同一个 key 不要混用不同数据类型。
  • localStorage 是同步 API,大数据读写可能阻塞主线程。
  • 它不能代替后端数据库。
  • 它也不能代替完整的 Pinia 业务状态管理。

官方文档:

useLocalStorage


6. useSessionStorage:当前标签页会话存储

6.1 基本使用

ts 复制代码
import { useSessionStorage } from '@vueuse/core'

const currentStep = useSessionStorage('import-step', 1)

修改状态:

ts 复制代码
currentStep.value = 2

刷新当前页面后仍然可以恢复。

关闭当前标签页后,这份数据通常会被清除。

6.2 与 useLocalStorage 的区别

能力 useLocalStorage useSessionStorage
刷新页面后 保留 保留
关闭标签页后 保留 通常清除
新标签页 可以读取相同数据 通常属于独立会话
适合长期设置
适合临时流程 一般

6.3 常见应用场景

  • 当前标签页的操作步骤
  • 临时筛选状态
  • 导入流程进度
  • 当前会话内的草稿
  • 临时展开状态
  • 当前页面导航状态

官方文档:

useSessionStorage


7. onClickOutside:点击元素外部

7.1 解决什么问题

onClickOutside 用于实现:

点击某个元素之外的区域时执行回调。

常见于:

  • 下拉菜单
  • 地图图例
  • 底图选择器
  • 浮动工具栏
  • Popover
  • 自定义弹出面板

7.2 基本使用

vue 复制代码
<script setup lang="ts">
import { ref } from 'vue'
import { onClickOutside } from '@vueuse/core'

const panelRef = ref<HTMLElement | null>(null)
const visible = ref(false)

onClickOutside(panelRef, () => {
  visible.value = false
})
</script>

<template>
  <div v-if="visible" ref="panelRef">
    面板内容
  </div>
</template>

判断逻辑是:

text 复制代码
点击 panelRef 内部
    → 不执行回调

点击 panelRef 外部
    → 执行回调

7.3 同时排除按钮和面板

实际开发中通常有两个元素:

  • 打开面板的按钮
  • 弹出的面板
ts 复制代码
const triggerRef = ref<HTMLElement | null>(null)
const popupRef = ref<HTMLElement | null>(null)
const popupVisible = ref(false)

onClickOutside(
  triggerRef,
  () => {
    if (popupVisible.value)
      popupVisible.value = false
  },
  {
    ignore: [popupRef],
  },
)

此时行为是:

text 复制代码
点击按钮
    → 不关闭

点击面板内部
    → 不关闭

点击其他区域
    → 关闭

7.4 ignore 的作用

ignore 表示哪些元素虽然不在目标元素内部,但仍然不应该被视为外部区域。

可以传 DOM ref

ts 复制代码
{
  ignore: [popupRef]
}

也可以传 CSS 选择器:

ts 复制代码
{
  ignore: [
    '.arco-trigger-popup',
    '[data-ignore-outside]',
  ]
}

也可以同时排除多个元素:

ts 复制代码
{
  ignore: [
    popupRef,
    toolbarRef,
    '.another-popup',
  ]
}

7.5 为什么 Teleport 场景需要 ignore

很多弹出面板会通过 Teleport 挂载到 body

text 复制代码
工具栏
└── 图例按钮

body
└── 图例面板

从 DOM 结构看,图例面板并不是图例按钮的子元素。

因此只监听按钮时,点击图例面板可能被误判为外部点击。

通过:

ts 复制代码
{
  ignore: [popupRef]
}

可以告诉 VueUse:

这个 Teleport 面板虽然不在按钮 DOM 内部,但业务上仍然属于内部区域。

7.6 在 Cesium 地图中的价值

某些 UI 组件默认的外部点击能力,可能依赖普通 DOM 的事件传播。

Cesium 画布、第三方地图引擎或者特殊事件处理逻辑,可能导致组件内部的默认外部点击监听不能稳定触发。

onClickOutside 可以在文档层统一监听,并明确指定按钮和面板的范围,因此更适合:

  • Cesium 地图
  • Canvas
  • Teleport 面板
  • 多层弹窗
  • 第三方地图容器

官方文档:

onClickOutside


8. useDebounceFn:函数防抖

8.1 防抖是什么

防抖表示:

高频触发过程中不立即执行,等操作停止一段时间后,只执行最后一次。

例如用户快速输入:

text 复制代码
地
地图
地图图
地图图层

如果设置 300ms 防抖,那么用户停止输入 300ms 后才真正发起一次搜索。

8.2 基本使用

ts 复制代码
import { ref, watch } from 'vue'
import { useDebounceFn } from '@vueuse/core'

const keyword = ref('')

const search = useDebounceFn(() => {
  console.log('请求搜索接口', keyword.value)
}, 300)

watch(keyword, search)

8.3 常见应用场景

  • 搜索框
  • 表单自动保存
  • 窗口缩放结束后重新计算
  • 地图相机移动结束后查询
  • 避免按钮连续提交
  • 避免短时间重复校验

8.4 注意异步请求问题

防抖只能阻止新的函数被频繁调用,不能取消已经发出的旧请求。

例如:

ts 复制代码
const search = useDebounceFn(async () => {
  const result = await requestSearch(keyword.value)
  list.value = result
}, 300)

如果旧请求已经发出,之后新请求先返回,旧请求又后返回,旧数据可能覆盖新数据。

复杂搜索还需要:

  • 使用 AbortController
  • 使用请求序号
  • 判断当前 keyword
  • 取消上一个请求

官方文档:

useDebounceFn


9. useThrottleFn:函数节流

9.1 节流是什么

节流表示:

在持续触发期间,每隔指定时间最多执行一次。

ts 复制代码
import { useThrottleFn } from '@vueuse/core'

const handleMouseMove = useThrottleFn(() => {
  console.log('更新鼠标位置')
}, 100)

监听鼠标:

ts 复制代码
useEventListener(window, 'mousemove', handleMouseMove)

即使浏览器一秒产生几百次鼠标事件,业务代码也只会大约每 100ms 执行一次。

9.2 常见应用场景

  • scroll
  • mousemove
  • resize
  • 地图相机移动
  • 拖动位置同步
  • 高频坐标计算
  • 高频状态上报

9.3 防抖和节流如何选择

场景 推荐
搜索框停止输入后查询 防抖
表单停止修改后保存 防抖
窗口缩放结束后重算 防抖
滚动过程中持续更新 节流
鼠标移动过程中显示坐标 节流
地图移动过程中限制刷新频率 节流

简单记忆:

text 复制代码
防抖:我只关心最后一次。

节流:我需要持续执行,但不能执行得太频繁。

官方文档:

useThrottleFn


10. refDebouncedrefThrottled

如果需要控制的不是一个函数,而是一个响应式变量,可以使用:

  • refDebounced
  • refThrottled

10.1 refDebounced

ts 复制代码
import { ref, refDebounced } from '@vueuse/core'

const keyword = ref('')
const debouncedKeyword = refDebounced(keyword, 300)

keyword 会立即变化:

ts 复制代码
keyword.value = '地图'

debouncedKeyword 会在停止变化 300ms 后更新。

可以直接监听:

ts 复制代码
watch(debouncedKeyword, () => {
  search(debouncedKeyword.value)
})

10.2 refThrottled

ts 复制代码
import { ref, refThrottled } from '@vueuse/core'

const mouseX = ref(0)
const throttledMouseX = refThrottled(mouseX, 100)

即使 mouseX 高频变化,throttledMouseX 也只会按照限制频率更新。

10.3 如何选择函数版和 Ref 版

需求 推荐
控制函数执行频率 useDebounceFnuseThrottleFn
控制响应式变量更新频率 refDebouncedrefThrottled

第二梯队:布局和 DOM 中常见

11. useWindowSize:浏览器窗口尺寸

11.1 基本使用

ts 复制代码
import { useWindowSize } from '@vueuse/core'

const { width, height } = useWindowSize()

返回:

ts 复制代码
width: Ref<number>
height: Ref<number>

11.2 判断屏幕尺寸

ts 复制代码
import { computed } from 'vue'

const isSmallScreen = computed(() => {
  return width.value < 1024
})

11.3 常见应用场景

  • 地图工具栏位置计算
  • 大屏和桌面使用不同业务逻辑
  • 图表重新计算尺寸
  • 弹窗最大高度计算
  • 判断是否进入紧凑布局
  • 根据视口决定渲染数量

11.4 不要滥用

如果只是改变样式,优先使用 CSS 媒体查询:

less 复制代码
@media (max-width: 1024px) {
  .toolbar {
    display: none;
  }
}

推荐原则:

text 复制代码
样式响应式
    → 优先 CSS

业务行为响应式
    → 可以使用 useWindowSize

官方文档:

useWindowSize


12. useElementSize:元素尺寸

12.1 基本使用

vue 复制代码
<script setup lang="ts">
import { ref } from 'vue'
import { useElementSize } from '@vueuse/core'

const containerRef = ref<HTMLElement | null>(null)

const { width, height } = useElementSize(containerRef)
</script>

<template>
  <div ref="containerRef">
    当前尺寸:{{ width }} × {{ height }}
  </div>
</template>

12.2 元素尺寸为什么会变化

元素尺寸变化不一定是浏览器窗口变化导致的,也可能来自:

  • 左侧菜单展开或收起
  • 父容器布局变化
  • 文本内容变化
  • Tab 切换
  • 弹窗尺寸变化
  • 拖动分栏
  • CSS 样式变化

因此,监听容器尺寸时不能只使用 window.resize

12.3 常见应用场景

图表自适应:

ts 复制代码
watch([width, height], () => {
  chart.resize()
})

Cesium 自适应:

ts 复制代码
watch([width, height], () => {
  viewer.resize()
})

动态设置列表高度:

ts 复制代码
const tableHeight = computed(() => {
  return Math.max(height.value - 120, 300)
})

12.4 注意循环更新

如果尺寸变化后修改样式,而新样式又导致尺寸变化,可能形成循环。

text 复制代码
尺寸变化
→ 修改元素高度
→ 元素尺寸再次变化
→ 再次修改高度

因此回调中不要无条件反复修改目标元素尺寸。

官方文档:

useElementSize


13. useResizeObserver:底层尺寸观察

13.1 基本使用

ts 复制代码
import { ref } from 'vue'
import { useResizeObserver } from '@vueuse/core'

const containerRef = ref<HTMLElement | null>(null)

useResizeObserver(containerRef, (entries) => {
  const entry = entries[0]
  const { width, height } = entry.contentRect

  console.log(width, height)
})

13.2 与 useElementSize 的区别

需求 推荐
只需要元素宽高 useElementSize
需要响应式 widthheight useElementSize
需要完整 ResizeObserverEntry useResizeObserver
需要尺寸变化后执行复杂副作用 useResizeObserver
需要观察多个目标 useResizeObserver
需要访问不同 box 尺寸信息 useResizeObserver

一般优先使用:

ts 复制代码
useElementSize()

只有需要底层信息时,再使用:

ts 复制代码
useResizeObserver()

官方文档:

useResizeObserver


14. onKeyStroke:键盘快捷键

14.1 基本使用

ts 复制代码
import { onKeyStroke } from '@vueuse/core'

onKeyStroke('Escape', () => {
  popupVisible.value = false
})

14.2 监听多个按键

ts 复制代码
onKeyStroke(['Enter', ' '], (event) => {
  event.preventDefault()
  submit()
})

14.3 监听组合键

ts 复制代码
onKeyStroke(
  event => event.ctrlKey && event.key === 's',
  (event) => {
    event.preventDefault()
    save()
  },
)

Mac 用户通常使用 Command,可以同时判断:

ts 复制代码
onKeyStroke(
  event =>
    (event.ctrlKey || event.metaKey) &&
    event.key.toLowerCase() === 's',
  (event) => {
    event.preventDefault()
    save()
  },
)

14.4 常见应用场景

  • Escape 关闭弹窗
  • Enter 提交
  • Delete 删除选中元素
  • 方向键移动
  • Ctrl/Cmd + S 保存
  • Ctrl/Cmd + Z 撤销
  • Ctrl/Cmd + Shift + Z 重做

14.5 注意输入框冲突

ts 复制代码
onKeyStroke('Delete', (event) => {
  const target = event.target

  if (
    target instanceof HTMLInputElement ||
    target instanceof HTMLTextAreaElement ||
    target instanceof HTMLSelectElement
  ) {
    return
  }

  deleteSelectedFeature()
})

否则用户在输入框中按 Delete,也可能触发删除地图元素。

14.6 注意长按重复触发

浏览器长按键盘时,会连续触发键盘事件。

ts 复制代码
onKeyStroke('Enter', (event) => {
  if (event.repeat)
    return

  submit()
})

官方文档:

onKeyStroke


第三梯队:定时任务和浏览器状态

15. useTimeoutFn:延迟执行一次

15.1 基本使用

useTimeoutFn 是可控制、可自动清理的 setTimeout

ts 复制代码
import { useTimeoutFn } from '@vueuse/core'

const {
  start,
  stop,
  isPending,
} = useTimeoutFn(() => {
  console.log('3 秒后执行')
}, 3000)

15.2 不立即开始

ts 复制代码
const {
  start,
  stop,
  isPending,
} = useTimeoutFn(
  () => {
    copiedVisible.value = false
  },
  2000,
  {
    immediate: false,
  },
)

启动:

ts 复制代码
start()

取消:

ts 复制代码
stop()

15.3 isPending

ts 复制代码
if (isPending.value) {
  console.log('定时任务正在等待执行')
}

15.4 常见应用场景

  • 延迟关闭提示
  • 复制成功状态保持两秒
  • 延迟加载
  • 延迟隐藏工具栏
  • 短暂成功动画
  • 防止直接维护 timeout 句柄

官方文档:

useTimeoutFn


16. useIntervalFn:周期执行

16.1 基本使用

useIntervalFn 是可暂停、恢复、自动清理的 setInterval

ts 复制代码
import { useIntervalFn } from '@vueuse/core'

const {
  pause,
  resume,
  isActive,
} = useIntervalFn(
  () => {
    refreshTaskStatus()
  },
  5000,
  {
    immediate: false,
  },
)

开始:

ts 复制代码
resume()

暂停:

ts 复制代码
pause()

判断是否正在运行:

ts 复制代码
console.log(isActive.value)

16.2 常见应用场景

  • 任务状态轮询
  • 设备状态刷新
  • 倒计时
  • 定期刷新地图数据
  • 定期检查消息
  • 简单心跳

16.3 防止异步任务重叠

下面的代码可能产生重叠请求:

ts 复制代码
useIntervalFn(async () => {
  await refresh()
}, 1000)

如果一次请求需要三秒,下一次计时可能在上一次完成前开始。

可以增加锁:

ts 复制代码
let refreshing = false

const {
  pause,
  resume,
} = useIntervalFn(async () => {
  if (refreshing)
    return

  refreshing = true

  try {
    await refresh()
  }
  finally {
    refreshing = false
  }
}, 5000)

16.4 复杂轮询需要额外考虑

  • 页面进入后台时是否暂停。
  • 请求失败后是否降低频率。
  • 用户离开页面后是否停止。
  • 登录失效后是否停止。
  • 是否允许请求重叠。
  • 是否需要指数退避。
  • 是否应该改用 WebSocket。

17. useOnline:网络在线状态

17.1 基本使用

ts 复制代码
import { useOnline } from '@vueuse/core'

const online = useOnline()

模板:

vue 复制代码
<template>
  <a-alert v-if="!online">
    当前网络连接已断开
  </a-alert>
</template>

17.2 能说明什么

ts 复制代码
online.value === false

通常说明浏览器检测到设备离线。

但是:

ts 复制代码
online.value === true

只说明浏览器认为存在网络连接,不代表后端服务一定正常。

可能出现:

  • Wi-Fi 已连接,但不能访问互联网。
  • 公司网络正常,但后端服务宕机。
  • DNS 异常。
  • 网关异常。
  • Token 已过期。
  • 当前接口不可用。

因此,useOnline 适合做离线提示,不适合作为后端健康检查。

官方文档:

useOnline


18. usePreferredDark:系统深色模式偏好

18.1 基本使用

ts 复制代码
import { usePreferredDark } from '@vueuse/core'

const prefersDark = usePreferredDark()

它读取系统或浏览器的:

css 复制代码
prefers-color-scheme: dark

18.2 监听系统主题变化

ts 复制代码
watch(prefersDark, (dark) => {
  console.log(
    dark
      ? '系统偏好深色'
      : '系统偏好浅色',
  )
})

18.3 系统偏好不等于应用主题

例如:

text 复制代码
操作系统:深色模式
应用手动设置:浅色模式

此时:

ts 复制代码
prefersDark.value === true

但应用实际主题仍然可能是浅色。

推荐的主题优先级:

text 复制代码
用户手动设置
    ↓ 用户没有设置
系统颜色偏好
    ↓ 系统无法识别
应用默认主题

如果需要完整控制主题,可以进一步了解:

  • useDark
  • useColorMode

官方文档:

usePreferredDark


第四梯队:特定交互场景常用

19. useScroll:滚动位置和滚动状态

19.1 基本使用

ts 复制代码
import { ref } from 'vue'
import { useScroll } from '@vueuse/core'

const containerRef = ref<HTMLElement | null>(null)

const {
  x,
  y,
  isScrolling,
  arrivedState,
  directions,
} = useScroll(containerRef)

19.2 返回内容

  • x:横向滚动距离
  • y:纵向滚动距离
  • isScrolling:当前是否正在滚动
  • arrivedState.top:是否到达顶部
  • arrivedState.bottom:是否到达底部
  • arrivedState.left:是否到达左侧
  • arrivedState.right:是否到达右侧
  • directions.up:是否向上滚动
  • directions.down:是否向下滚动
  • directions.left:是否向左滚动
  • directions.right:是否向右滚动

19.3 触底加载

ts 复制代码
watch(
  () => arrivedState.bottom,
  (arrived) => {
    if (arrived)
      loadMore()
  },
)

还应增加加载锁:

ts 复制代码
watch(
  () => arrivedState.bottom,
  async (arrived) => {
    if (!arrived || loading.value)
      return

    loading.value = true

    try {
      await loadMore()
    }
    finally {
      loading.value = false
    }
  },
)

19.4 回到顶部

ts 复制代码
y.value = 0

19.5 确保容器可以滚动

less 复制代码
.scroll-container {
  height: 500px;
  overflow: auto;
}

如果元素没有固定高度或没有 overflow,可能监听不到预期的容器滚动。

19.6 高频操作配合节流

ts 复制代码
const handleScroll = useThrottleFn(() => {
  updateToolbarPosition(y.value)
}, 100)

watch(y, handleScroll)

官方文档:

useScroll


20. useClipboard:剪贴板

20.1 基本使用

ts 复制代码
import { useClipboard } from '@vueuse/core'

const {
  copy,
  copied,
  text,
  isSupported,
} = useClipboard()

复制:

ts 复制代码
await copy('项目编号:123456')

20.2 显示复制状态

vue 复制代码
<script setup lang="ts">
import { useClipboard } from '@vueuse/core'

const { copy, copied } = useClipboard()

function copyProjectId() {
  copy('project-001')
}
</script>

<template>
  <a-button @click="copyProjectId">
    {{ copied ? '已复制' : '复制项目编号' }}
  </a-button>
</template>

20.3 绑定响应式来源

ts 复制代码
const projectUrl = computed(() => location.href)

const { copy } = useClipboard({
  source: projectUrl,
})

调用:

ts 复制代码
copy()

20.4 注意事项

  • 剪贴板 API 通常要求 HTTPS。
  • 本地开发环境通常也可使用。
  • 某些操作必须由用户点击触发。
  • 浏览器可能拒绝权限。
  • 读取剪贴板比写入限制更多。
  • 使用前可以检查 isSupported
ts 复制代码
if (!isSupported.value) {
  Message.warning('当前浏览器不支持剪贴板操作')
  return
}

await copy(value)

官方文档:

useClipboard


21. useMouse:响应式鼠标坐标

21.1 基本使用

ts 复制代码
import { useMouse } from '@vueuse/core'

const {
  x,
  y,
  sourceType,
} = useMouse()

读取:

ts 复制代码
console.log(x.value, y.value)

21.2 sourceType

sourceType 可以用于判断位置数据来自:

  • 鼠标
  • 触摸操作
  • 其他指针来源

如果只想监听鼠标:

ts 复制代码
const { x, y } = useMouse({
  touch: false,
})

21.3 常见应用场景

  • 地图坐标提示
  • 跟随鼠标的 Tooltip
  • 自定义十字准星
  • 绘图工具
  • 调试鼠标位置
  • 鼠标附近显示操作按钮

21.4 元素内部坐标

useMouse 更偏向全局鼠标位置。

如果需要某个元素内部的相对坐标,可以考虑:

ts 复制代码
useMouseInElement()

21.5 注意性能

鼠标移动事件频率非常高,不要每次变化都执行昂贵计算:

ts 复制代码
watch([x, y], expensiveCalculation)

推荐配合节流:

ts 复制代码
const updatePosition = useThrottleFn(() => {
  updateMapCoordinate(x.value, y.value)
}, 50)

watch([x, y], updatePosition)

官方文档:

useMouse


22. useDraggable:让元素可拖动

22.1 基本使用

vue 复制代码
<script setup lang="ts">
import { ref } from 'vue'
import { useDraggable } from '@vueuse/core'

const panelRef = ref<HTMLElement | null>(null)

const {
  x,
  y,
  style,
} = useDraggable(panelRef, {
  initialValue: {
    x: 100,
    y: 100,
  },
})
</script>

<template>
  <div
    ref="panelRef"
    class="floating-panel"
    :style="style"
  >
    可拖动面板:{{ x }},{{ y }}
  </div>
</template>

<style scoped>
.floating-panel {
  position: fixed;
}
</style>

style 通常会生成类似:

css 复制代码
left: 100px;
top: 100px;

22.2 常见应用场景

  • 地图浮动面板
  • 可移动工具条
  • 小型调试窗口
  • 悬浮视频窗口
  • 临时辅助窗口

22.3 它不是完整拖拽系统

复杂需求仍然需要自己处理:

  • 拖动边界
  • 防止拖出屏幕
  • 吸附效果
  • 碰撞检测
  • 列表排序
  • 跨容器拖放
  • 拖放数据
  • 移动端手势冲突
  • 键盘可访问性
  • 拖动手柄

例如简单边界限制:

ts 复制代码
const maxX = computed(() => {
  return windowWidth.value - panelWidth.value
})

复杂列表拖拽应该考虑专门的拖拽方案,而不是只使用 useDraggable

官方文档:

useDraggable


第五梯队:网络能力,使用前需要考虑项目架构

23. useFetch:响应式 Fetch 请求

23.1 基本使用

useFetch 是对浏览器原生 fetch 的响应式封装。

ts 复制代码
import { useFetch } from '@vueuse/core'

const {
  data,
  error,
  isFetching,
  execute,
  abort,
} = useFetch('/api/projects', {
  immediate: false,
}).get().json()

手动请求:

ts 复制代码
await execute()

取消请求:

ts 复制代码
abort()

23.2 常见能力

useFetch 能提供:

  • 响应式 data
  • 响应式 error
  • isFetching
  • 手动执行
  • 取消请求
  • URL 改变后重新请求
  • 请求前拦截
  • 响应后拦截
  • 创建预配置版本

23.3 响应式 URL

ts 复制代码
const projectId = ref('001')

const url = computed(() => {
  return `/api/projects/${projectId.value}`
})

const {
  data,
  isFetching,
} = useFetch(url).get().json()

23.4 在已有项目中不要随意替换请求层

很多项目已经有统一 API 请求层,里面会处理:

  • Token
  • 基础地址
  • 请求头
  • 错误码
  • 登录失效
  • 消息提示
  • 参数转换
  • 响应解包
  • 请求日志
  • 权限异常

这种情况下,不应在页面里随意使用 useFetch 绕开现有请求封装。

适合使用 useFetch 的情况:

  • 简单原型
  • 独立工具
  • 公开接口
  • 不经过业务网关的请求
  • 项目决定基于 useFetch 建立统一封装

它不是 Axios 的直接增强版,而是原生 Fetch 的响应式封装。

官方文档:

useFetch


24. useWebSocket:WebSocket 连接

24.1 基本使用

ts 复制代码
import { useWebSocket } from '@vueuse/core'

const {
  status,
  data,
  send,
  open,
  close,
  ws,
} = useWebSocket('wss://example.com/ws')

24.2 发送消息

ts 复制代码
send(JSON.stringify({
  type: 'subscribe',
  projectId: 'project-001',
}))

24.3 接收消息

ts 复制代码
watch(data, (message) => {
  if (!message)
    return

  try {
    const payload = JSON.parse(message)
    console.log(payload)
  }
  catch {
    console.warn('WebSocket 消息格式错误')
  }
})

24.4 自动重连

ts 复制代码
useWebSocket(url, {
  autoReconnect: {
    retries: 3,
    delay: 1000,
  },
})

24.5 心跳

ts 复制代码
useWebSocket(url, {
  heartbeat: {
    message: 'ping',
    interval: 30_000,
  },
})

具体心跳消息必须符合后端协议,不能默认认为服务端一定接受字符串 ping

24.6 VueUse 负责什么

VueUse 主要负责:

  • 创建连接
  • 维护连接状态
  • 发送消息
  • 保存最后一条消息
  • 手动打开和关闭
  • 基础自动重连
  • 基础心跳
  • 组件卸载时关闭连接

24.7 业务仍然需要负责什么

  • 消息类型定义
  • JSON 解析
  • 数据校验
  • Token 和鉴权
  • 订阅与取消订阅
  • 消息顺序
  • 消息去重
  • 断线后恢复订阅
  • 后端心跳协议
  • 多页面是否共享连接
  • 连接失败提示

24.8 data 通常只是最后一条消息

ts 复制代码
data.value

通常表示最近收到的一条消息,不是完整消息列表。

如果需要消息历史:

ts 复制代码
const messages = ref<unknown[]>([])

watch(data, (message) => {
  if (!message)
    return

  messages.value.push(JSON.parse(message))
})

如果消息很多,还要限制数组长度:

ts 复制代码
if (messages.value.length > 1000)
  messages.value.shift()

官方文档:

useWebSocket


第六梯队:响应式增强和状态组织

25. useRefHistory:历史记录、撤销和重做

25.1 基本使用

ts 复制代码
import { ref } from 'vue'
import { useRefHistory } from '@vueuse/core'

const form = ref({
  name: '',
  description: '',
})

const {
  history,
  undo,
  redo,
  canUndo,
  canRedo,
  clear,
} = useRefHistory(form, {
  deep: true,
  clone: true,
  capacity: 20,
})

25.2 使用撤销和重做

ts 复制代码
undo()
redo()
clear()

模板:

vue 复制代码
<a-button
  :disabled="!canUndo"
  @click="undo"
>
  撤销
</a-button>

<a-button
  :disabled="!canRedo"
  @click="redo"
>
  重做
</a-button>

25.3 deep 的作用

对于对象内部修改:

ts 复制代码
form.value.name = '新名称'

一般需要:

ts 复制代码
{
  deep: true
}

否则可能无法完整记录对象内部变化。

25.4 clone 的作用

对象是引用类型。

如果多个历史记录指向同一个对象,后续修改可能影响旧记录。

因此对象历史通常需要:

ts 复制代码
{
  clone: true
}

25.5 capacity 的作用

ts 复制代码
{
  capacity: 20
}

表示只保留有限数量的历史记录,防止内存不断增长。

25.6 常见应用场景

  • 表单撤销
  • 编辑器撤销和重做
  • 地图绘制历史
  • 图层样式修改历史
  • 配置面板回退
  • 轻量操作记录

25.7 大对象要谨慎

下面的场景可能非常消耗内存:

ts 复制代码
const hugeGeoJson = ref(...)

如果每次修改都完整深拷贝 GeoJSON,历史记录会快速占用大量内存。

大型地图数据更适合记录"操作命令":

text 复制代码
添加点 A
删除线 B
修改面 C 的属性

而不是每次保存整个数据快照。

25.8 相关能力

  • useDebouncedRefHistory
  • useThrottledRefHistory
  • useManualRefHistory

例如输入框连续输入时,可以用防抖历史,避免每输入一个字符都生成记录。

官方文档:

useRefHistory


26. createGlobalState:轻量全局共享状态

26.1 基本使用

ts 复制代码
// use-map-ui-state.ts
import { ref } from 'vue'
import { createGlobalState } from '@vueuse/core'

export const useMapUiState = createGlobalState(() => {
  const activePanel = ref<string | null>(null)
  const toolbarVisible = ref(true)

  function closePanel() {
    activePanel.value = null
  }

  return {
    activePanel,
    toolbarVisible,
    closePanel,
  }
})

组件 A:

ts 复制代码
const { activePanel } = useMapUiState()

activePanel.value = 'legend'

组件 B:

ts 复制代码
const { activePanel, closePanel } = useMapUiState()

console.log(activePanel.value)

组件 A 和组件 B 得到的是同一份状态。

26.2 常见应用场景

  • 简单 UI 状态
  • 当前打开的地图工具
  • 多个组件共享窗口状态
  • 多个组件共享网络状态
  • 不值得创建 Pinia store 的简单状态

26.3 不适合替代 Pinia 的场景

以下情况通常优先使用 Pinia:

  • 核心业务状态
  • 用户信息
  • 权限信息
  • 复杂 actions
  • 大量接口请求
  • 需要 DevTools 调试
  • 需要明确模块边界
  • 需要插件、持久化或日志

官方文档:

createGlobalState


27. createSharedComposable:共享一个 Composable 实例

27.1 解决什么问题

假设一个组合函数内部创建了多个监听器:

ts 复制代码
function useWindowMonitor() {
  const size = useWindowSize()
  const online = useOnline()

  return {
    ...size,
    online,
  }
}

多个组件分别调用时,可能创建多份响应式作用域和监听。

可以使用:

ts 复制代码
import { createSharedComposable } from '@vueuse/core'

export const useSharedWindowMonitor =
  createSharedComposable(useWindowMonitor)

多个组件调用:

ts 复制代码
const {
  width,
  height,
  online,
} = useSharedWindowMonitor()

它们会共享同一个 composable 结果。

27.2 常见应用场景

  • 多组件共享窗口尺寸监听
  • 多组件共享鼠标位置
  • 多组件共享网络状态
  • 共享同一个媒体查询
  • 避免重复创建昂贵观察器
  • 共享同一个订阅来源

27.3 与 createGlobalState 的区别

能力 侧重点
createGlobalState 共享一份响应式状态
createSharedComposable 共享整个 composable,包括状态和副作用

28. createInjectionState:组件树局部共享状态

28.1 解决什么问题

它封装了 Vue 的:

ts 复制代码
provide()
inject()

适合只在某棵组件树内共享状态。

例如:

text 复制代码
地图页面
├── 图层面板
├── 图例面板
├── 底图面板
└── 属性面板

这些组件需要共享地图上下文,但地图页面之外不应该访问。

28.2 定义上下文

ts 复制代码
import { computed, ref } from 'vue'
import { createInjectionState } from '@vueuse/core'

const [
  useProvideMapContext,
  useMapContext,
] = createInjectionState((projectId: string) => {
  const selectedLayerId = ref<string | null>(null)

  const hasSelection = computed(() => {
    return selectedLayerId.value !== null
  })

  return {
    projectId,
    selectedLayerId,
    hasSelection,
  }
})

28.3 父组件提供状态

ts 复制代码
useProvideMapContext(props.projectId)

28.4 子孙组件使用状态

ts 复制代码
const mapContext = useMapContext()

if (!mapContext)
  throw new Error('必须在地图上下文中使用')

mapContext.selectedLayerId.value = 'layer-001'

28.5 与其他状态方案的区别

需求 推荐
全应用复杂业务状态 Pinia
全应用简单共享状态 createGlobalState
共享 composable 及其副作用 createSharedComposable
只在某棵组件树内共享 createInjectionState

官方文档:

createInjectionState


VueUse 常见能力选型速查表

我想实现的功能 优先使用
注册 DOM 或窗口事件 useEventListener
点击面板外部关闭 onClickOutside
长期保存用户偏好 useLocalStorage
保存当前标签页临时状态 useSessionStorage
搜索输入停止后查询 useDebounceFn
限制滚动或鼠标回调频率 useThrottleFn
延迟更新响应式变量 refDebounced
限制响应式变量更新频率 refThrottled
监听浏览器窗口大小 useWindowSize
获取某个元素宽高 useElementSize
获取底层尺寸变化信息 useResizeObserver
监听快捷键 onKeyStroke
延迟执行一次 useTimeoutFn
周期执行或轮询 useIntervalFn
判断浏览器可能离线 useOnline
获取系统深色偏好 usePreferredDark
获取滚动状态 useScroll
复制文本 useClipboard
获取鼠标位置 useMouse
拖动悬浮面板 useDraggable
简单响应式 HTTP 请求 useFetch
管理 WebSocket 生命周期 useWebSocket
实现撤销和重做 useRefHistory
轻量全局共享状态 createGlobalState
共享一个 composable 实例 createSharedComposable
组件树局部共享状态 createInjectionState

推荐学习顺序

如果刚接触 VueUse,建议按照以下顺序掌握:

text 复制代码
1. useEventListener
2. onClickOutside
3. useLocalStorage / useSessionStorage
4. useDebounceFn / useThrottleFn
5. useWindowSize / useElementSize
6. useTimeoutFn / useIntervalFn
7. onKeyStroke
8. useScroll / useClipboard
9. useMouse / useDraggable
10. useOnline / usePreferredDark
11. useFetch / useWebSocket
12. useRefHistory
13. createGlobalState / createSharedComposable / createInjectionState

使用 VueUse 的判断思路

开发过程中,如果原生代码中出现下面这些 API,可以先检查 VueUse 是否已经提供对应封装:

text 复制代码
addEventListener
removeEventListener
ResizeObserver
IntersectionObserver
setTimeout
setInterval
localStorage
sessionStorage
navigator.clipboard
navigator.onLine
matchMedia
fetch
WebSocket
mousemove
scroll

例如:

text 复制代码
addEventListener
    → useEventListener

ResizeObserver
    → useElementSize / useResizeObserver

setTimeout
    → useTimeoutFn

setInterval
    → useIntervalFn

localStorage
    → useLocalStorage

点击元素外部
    → onClickOutside

窗口尺寸
    → useWindowSize

剪贴板
    → useClipboard

但 VueUse 不是"看到原生 API 就必须替换"。

以下情况可以继续使用原生 API:

  • 逻辑非常简单。
  • 使用范围非常局部。
  • 不需要响应式状态。
  • 不涉及组件生命周期。
  • 项目已经有统一封装。
  • VueUse 封装反而让逻辑更难理解。

核心原则是:

VueUse 用来减少重复的生命周期管理和浏览器 API 样板代码,而不是为了使用 VueUse 而使用 VueUse。

相关推荐
蒜苔肉丝1 小时前
Vue3 动态表单:模块复用 + 动态增删行 + 数据回显
前端·javascript·vue.js
Cxiaomu1 小时前
从 TRTC Demo 到独立音视频服务:React + TRTC Web SDK 服务化实践
前端·react.js·音视频
索西引擎2 小时前
【React】Redux 中间件机制:副作用处理与数据流增强的形式化分析
前端·react.js·中间件
寒草2 小时前
「寒草呈献」工作六年,是否仍有创造未来的勇气 ✨
前端·后端
HackTwoHub2 小时前
解锁 AI 红队全新玩法!Claude-Red 攻防 Skill 库,内置 SQLi、XXE、文件上传等 Web 专项 Skill,一键导入快速落地渗透实战
前端·人工智能·web安全·网络安全·自动化·系统安全
石小石Orz2 小时前
如何设计一个优秀的 Skills
前端·人工智能
程序员爱钓鱼3 小时前
Rust 切片 Slice 详解:安全访问连续数据
前端·后端·rust
alexander0685 小时前
CSS 类选择器组合
前端·css
寅时码10 小时前
React 之死·终章:一个 useRef,把闭包陷阱、依赖数组、漫天 rerender 全送走
前端·react.js·ai编程