AIRI 项目中的 VueUse useClipboard 实战指南:响应式剪贴板读写与降级方案
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
useClipboard 是 VueUse 提供的响应式 Clipboard API 封装,它让 Vue 组件能够以声明式方式响应剪切、复制、粘贴命令,并异步读写系统剪贴板。在 AIRI(self-hosted 的 AI 伴侣桌面应用,Web / macOS / Windows 多端支持)中,它被广泛用于"一键复制鉴权 Token""复制 Bug 报告内容""复制图片 Data URL"等高频交互场景。阅读完本文,你将掌握 useClipboard 的全部选项、返回值、legacy 降级方案与组件式用法,并看到它在 AIRI 真实源码中的落地模式。
useClipboard 解决了什么问题
浏览器原生 Clipboard API 提供了navigator.clipboard.writeText()与navigator.clipboard.readText()等能力,但使用起来存在几个痛点:
- 权限门槛:剪贴板内容的读写被 Permissions API 门控,未经用户授权,读取或修改剪贴板内容是不被允许的;
- 非响应式:复制是否成功、当前剪贴板内容是什么,原生 API 不会以响应式状态暴露给 UI;
- 兼容性差异:部分浏览器或嵌入式 WebView(如 Electron 旧版本渲染进程)不支持 Clipboard API,需要回退方案;
- 重复样板:每个需要复制的按钮都要重复编写 try/catch、状态复位定时器等代码。
useClipboard 将这些全部收敛为一个 composable:返回text、copied、copy、isSupported等响应式状态与操作函数,并在 1.5 秒后自动将copied复位为false,让"复制成功"的 UI 反馈天然响应式。
在 AIRI 仓库中,@vueuse/core通过 pnpm workspace 的 catalog 统一管理,版本为^14.4.0(见 pnpm-workspace.yaml),各应用与包如 stage-ui 的 package.json 均从 catalog 引入,保证多端行为一致。
基础用法
在<script setup>中引入并解构使用:
<script setup lang="ts"> import { useClipboard } from '@vueuse/core' const source = ref('Hello') const { text, copy, copied, isSupported } = useClipboard({ source }) </script> <template> <div v-if="isSupported"> <button @click="copy(source)"> <!-- by default, `copied` will be reset in 1.5s --> <span v-if="!copied">Copy</span> <span v-else>Copied!</span> </button> <p>Current copied: <code>{{ text || 'none' }}</code></p> </div> <p v-else> Your browser does not support Clipboard API </p> </template>要点说明:
copy(source)传入的source可以是 ref 或 getter,调用时会取当前值写入剪贴板;- 模板中用
v-if="isSupported"做能力探测分支,避免在不支持的浏览器中渲染不可用的按钮; copied在复制成功后自动置true,默认 1.5 秒后复位,用于按钮文案从 "Copy" 切换到 "Copied!"。
Options 选项详解
| Option | Type | Default | Description |
|---|---|---|---|
source | MaybeRefOrGetter<string> | — | 当copy()无参数调用时默认复制的内容 |
read | boolean | false | 是否在 copy/cut 事件时启用剪贴板内容读取 |
copiedDuring | number | 1500 | copied复位为false前的毫秒数 |
legacy | boolean | false | Clipboard API 不可用时回退到document.execCommand |
对每个选项的深入解读:
source:配置后,copy()不传参也能复制。它接受 ref 或 getter,因此可以与表单输入框的v-modelref 直接绑定——AIRI 的连接设置页正是利用这一点复制动态变化的 Token(见下文源码实例)。read:设为true后,composable 会监听浏览器的 copy/cut 事件并读取剪贴板内容到textref,适合构建"粘贴板预览"类功能。默认false是为了避免无谓的权限请求。copiedDuring:控制成功反馈的持续时间,单位毫秒。如设为0则copied几乎立即复位,可用于纯触发式场景。legacy:关键兼容性开关。启用后,当navigator.clipboard不可用时,会改用document.execCommand('copy')+ 临时隐藏文本域的方式完成复制,保证 Electron 旧内核或老浏览器中功能不缺失。
返回值说明
| Property | Type | Description |
|---|---|---|
isSupported | ComputedRef<boolean> | 剪贴板是否受支持(原生或 legacy 模式) |
text | Ref<string> | 当前剪贴板内容(仅在read: true时有效) |
copied | Ref<boolean> | 复制成功后为true,自动复位 |
copy | (text?: string) => Promise<void> | 将文本复制到剪贴板 |
其中isSupported在 legacy 模式下会在原生 API 缺失时依然返回true(因为走 execCommand 回退),这与它"native or legacy"的语义一致。copy是异步函数,务必await并捕获 rejection——权限被拒绝、文档未聚焦等情况都会导致 Promise 失败。
Legacy 模式:老环境的兜底方案
当 Clipboard API 不可用(例如某些非安全上下文、旧版 WebView)时,设置legacy: true即可保留复制能力:
const { copy, isSupported } = useClipboard({ legacy: true })内部实现会用document.execCommand('copy')作为回退。需要说明的是,execCommand 方案存在历史局限(依赖用户手势、可能影响选区),因此它只应作为兼容兜底,而不是默认首选。AIRI 在渲染错误上报与鉴权 Token 复制两处都显式启用了legacy: true,正是因为这些场景跨 Web 与 Electron 桌面端运行,必须保证所有环境下"复制"按钮都可用。
Component 用法:无脚本式复制按钮
如果不想写任何<script setup>逻辑,可以使用随 VueUse 导出的<UseClipboard>组件,通过 scoped slot 拿到copy与copied:
<template> <UseClipboard v-slot="{ copy, copied }" source="copy me"> <button @click="copy()"> {{ copied ? 'Copied' : 'Copy' }} </button> </UseClipboard> </template>source作为组件 prop 传入,slot 内部直接调用copy()即可复制。适合纯展示型组件或 Storybook / Histoire 场景快速演示。
类型声明解读
完整类型声明如下:
export interface UseClipboardOptions<Source> extends ConfigurableNavigator { /** * Enabled reading for clipboard * * @default false */ read?: boolean /** * Copy source */ source?: Source /** * Milliseconds to reset state of `copied` ref * * @default 1500 */ copiedDuring?: number /** * Whether fallback to document.execCommand('copy') if clipboard is undefined. * * @default false */ legacy?: boolean } type ClipboardValue = string | (() => Promise<string | undefined>) export interface UseClipboardReturn<Optional> extends Supportable { text: Readonly<ShallowRef<string>> copied: Readonly<ShallowRef<boolean>> copyPending: Readonly<ShallowRef<boolean>> copy: Optional extends true ? (text?: ClipboardValue) => Promise<void> : (text: ClipboardValue) => Promise<void> } export declare function useClipboard( options?: UseClipboardOptions<undefined>, ): UseClipboardReturn<false> export declare function useClipboard( options: UseClipboardOptions<MaybeRefOrGetter<string>>, ): UseClipboardReturn<true>几个值得注意的细节:
UseClipboardOptions<Source>继承ConfigurableNavigator,因此还支持通过window选项自定义全局对象(便于测试注入);copyPending是额外暴露的"复制进行中"状态,可用于按钮 loading;- 通过泛型
Optional区分重载:传入source时copy的文本参数变为可选,未传source时copy(text)必须显式传参——这种条件类型设计让"默认复制源"在编译期就得到约束; text与copied均为Readonly<ShallowRef>,只读且浅层响应,避免外部误改。
AIRI 仓库中的真实落地
以下四个源码位置展示了 useClipboard 在不同业务场景中的典型用法,均可在当前仓库直接查阅。
场景一:渲染错误上报(legacy 兜底 + 异步复制)
在 stage-render-error.vue 中,舞台渲染出错时提供"复制报告并提交"的能力:
const { copy, isSupported: isClipboardSupported } = useClipboard({ legacy: true }) async function submitBugReport(payload: BugReportDialogSubmitPayload) { bugReportSending.value = true try { if (!isClipboardSupported.value) throw new Error('Clipboard API is unavailable') await copy(payload.formattedReport) showBugReportDialog.value = false } catch (error) { bugReportSubmitError.value = error } finally { bugReportSending.value = false } }这段代码示范了规范写法:先校验isClipboardSupported,再await copy(),并将失败原因捕获后回显到弹窗中,配合finally复位 loading 状态。桌面端「关于页」的更新错误上报采用了几乎相同的模式(见 about.vue),构建信息、更新状态、错误信息被拼装为多行文本后一键复制,方便用户粘贴到反馈渠道。
场景二:复制鉴权 Token(source 动态绑定 + 图标反馈)
在 settings/connection/index.vue 中,服务器连接页的鉴权 Token 输入框与useClipboard直接联动:
const { copied: authTokenCopied, copy: copyAuthToken, isSupported: isClipboardSupported } = useClipboard({ source: authTokenInput, legacy: true }) const canCopyAuthToken = computed(() => isClipboardSupported.value && authTokenInput.value.length > 0)模板中复制按钮的图标随authTokenCopied切换(从 copy 图标变为 check 图标),同时通过canCopyAuthToken在 Token 为空或环境不支持时禁用按钮:
<IconButton type="button" :icon="authTokenCopied ? 'i-solar:check-circle-bold-duotone' : 'i-solar:copy-line-duotone'" :disabled="!canCopyAuthToken" >const imageDataURL = ref<string>('') const { copy } = useClipboard({ source: imageDataURL })配合@click="() => copy()"即可把超长的 Base64 Data URL 一键复制到剪贴板,用于把调试图片粘贴到其他工具中分析。
最佳实践小结
结合官方文档与 AIRI 源码实践,推荐以下使用准则:
- 先探测后使用:渲染前用
isSupported做v-if分支,或配合canCopy计算属性禁用按钮,避免无效点击; - 始终 await copy:
copy返回 Promise,必须在 try/catch 中等待并处理权限拒绝等失败路径,AIRI 的错误上报组件即为其范本; - 跨端场景开启 legacy:只要应用可能运行在 Electron、老 WebView 或非安全上下文,就传
legacy: true保住复制能力底线; - 让 source 跟随数据:把可变的复制内容声明为 ref 并绑定到
source,调用copy()时自动取最新值,减少显式传参带来的同步成本; - 利用 copied 做轻量反馈:默认 1.5 秒自动复位,无需手写定时器;若需更长展示时间,用
copiedDuring调整。
AIRI 的源码证据表明,一个十几行的小 composable 就能在错误上报、Token 复制、调试工具等多个模块中提供一致、可靠、跨端的剪贴板能力,这正是 VueUse 组合式函数"以响应式方式包装浏览器 API"理念的最佳注脚。
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考