AIRI 实战:用 VueUseuseClipboardItems在 Vue 应用中复制图片、富文本等任意剪贴板内容
【免费下载链接】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
本指南围绕 VueUse 的useClipboardItems组合式函数展开,讲解如何基于原生ClipboardItem在 Vue 3 项目中实现响应式、异步、支持任意内容类型(图片、富文本、HTML、二进制数据)的系统剪贴板读写。文中完整继承 VueUse 官方参考文档的用法与类型声明,并结合 AIRI 仓库中useClipboard的真实调用场景,帮助你理解剪贴板 API 的权限模型、useClipboard与useClipboardItems的差异,以及如何把“复制文本”升级为“复制任意媒体内容”。
背景:AIRI 与 VueUse 剪贴板工具链
AIRI 是一个自托管、面向桌面与 Web 的 AI 伴侣项目(Web / macOS / Windows 均支持),其桌面端与 Web 端界面大量基于 Vue 3 构建。仓库在多个应用与包中统一引入了@vueuse/core(见 apps/stage-tamagotchi/package.json 与 packages/stage-ui/package.json,均通过catalog:从 pnpm workspace 的版本目录解析)。仓库还内置了.agents/skills/vueuse-functions/SKILL.md技能:在 Vue/Nuxt 开发中优先选用合适的 VueUse 组合式函数,而不是手写重复代码,其中useClipboard与useClipboardItems都被归类为Browser类别、调用规则为AUTO(适用时自动使用)。
在这个背景下,剪贴板能力是 AIRI 各端界面(设置页、关于页、开发工具、错误上报)的通用基础设施。useClipboardItems正是这套工具链中面向“非纯文本内容”的那一块拼图。
useClipboardItems是什么
useClipboardItems是对 Clipboard API)。它提供:
- 对剪贴板命令(剪切、复制、粘贴)的响应能力;
- 对系统剪贴板的异步读取与写入;
- 基于 Permissions API 的权限门控——在没有用户授权的情况下,不允许读取或修改剪贴板内容。
关键点在于,它是基于 ClipboardItem 的函数。任何ClipboardItem支持的内容都可以通过它复制,而不仅仅限于字符串。
与useClipboard的核心差异
| 维度 | useClipboard | useClipboardItems |
|---|---|---|
| 数据模型 | 纯文本(string) | ClipboardItem[](ClipboardItems) |
| 返回的“内容”ref | text: Readonly<ShallowRef<string>> | content: Readonly<Ref<ClipboardItems>> |
| 适用场景 | 复制字符串、URL、Token 等 | 复制图片、富文本、HTML、任意二进制 Blob |
| 可选能力 | legacy降级到document.execCommand | 无legacy(原生 API 不支持时直接不可用) |
useClipboard的定位是“text-only”;当需求跨越纯文本、需要复制多种 MIME 类型的内容时,应当选择useClipboardItems。AIRI 仓库中现有的四处useClipboard调用(下面“仓库实践”一节会分析)都符合“复制字符串”的定位,这也反向说明了useClipboardItems的使用边界:一旦复制目标变成ClipboardItem,就该切换到它。
快速上手:复制任意内容的最小示例
参考文档给出了一个可直接运行的 SFC 示例,完整复刻如下(来自 useClipboardItems.md):
<script setup lang="ts"> import { useClipboardItems } from '@vueuse/core' const mime = 'text/plain' const source = ref([ new ClipboardItem({ [mime]: new Blob(['plain text'], { type: mime }), }) ]) const { content, copy, copied, isSupported } = useClipboardItems({ 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>{{ content || 'none' }}</code> </p> </div> <p v-else> Your browser does not support Clipboard API </p> </template>拆解这段代码的四个要点:
ClipboardItem的构造:new ClipboardItem({ [mime]: blob })以 MIME 类型为键、Blob为值构造剪贴板条目。一个ClipboardItem可以携带多种 MIME 表示(例如同时提供text/html与text/plain),由系统按目标应用选择。source是一个 ref:useClipboardItems({ source })接受MaybeRefOrGetter<ClipboardItems>,因此传入ref后内容变更会自动反应到copy()的默认行为中。- 响应式返回值:
content反映最近一次读取/写入的剪贴板条目;copied在复制成功后置为true,默认 1500ms 后自动复位;isSupported用于能力检测与降级展示。 - 权限与安全:整个操作受 Permissions API 门控,必须发生在用户手势(如点击事件)触发的上下文中,否则会被浏览器拒绝。
复制图片到剪贴板
把text/plain换成image/png,就得到了“把图片写进系统剪贴板”的能力,这是useClipboard无法直接做到的:
async function copyImageToClipboard(file: File) { const item = new ClipboardItem({ 'image/png': file, }) await copy([item]) }由于ClipboardItem的 value 接受Blob(File继承自Blob),可以直接把用户选择的文件塞进去。复制后即可在聊天工具、文档编辑器、画图软件中直接粘贴。
复制富文本(HTML + 纯文本双表示)
为同一个ClipboardItem提供多个 MIME 表示,可以让粘贴方智能选择:
const html = '<strong>Hello</strong> AIRI' const item = new ClipboardItem({ 'text/html': new Blob([html], { type: 'text/html' }), 'text/plain': new Blob(['Hello AIRI'], { type: 'text/plain' }), }) await copy([item])这样支持 HTML 的应用粘贴出富文本,只支持纯文本的应用则回退到text/plain。
Options 与返回值:完整类型声明解读
useClipboardItems的完整类型声明(摘自 useClipboardItems.md):
export interface UseClipboardItemsOptions< 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 } export interface UseClipboardItemsReturn<Optional> extends Supportable { content: Readonly<Ref<ClipboardItems>> copied: Readonly<ShallowRef<boolean>> copy: Optional extends true ? (content?: ClipboardItems) => Promise<void> : (text: ClipboardItems) => Promise<void> read: () => void } /** * Reactive Clipboard API. * * @see https://vueuse.org/useClipboardItems * @param options * * @__NO_SIDE_EFFECTS__ */ export declare function useClipboardItems( options?: UseClipboardItemsOptions<undefined>, ): UseClipboardItemsReturn<false> export declare function useClipboardItems( options: UseClipboardItemsOptions<MaybeRefOrGetter<ClipboardItems>>, ): UseClipboardItemsReturn<true>选项参数表
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
read | boolean | false | 是否启用对剪贴板内容的读取(会监听 copy/cut 等事件并刷新content) |
source | MaybeRefOrGetter<ClipboardItems> | — | 调用copy()且不传参时使用的默认剪贴板内容 |
copiedDuring | number | 1500 | copied置为true后多少毫秒复位为false |
此外UseClipboardItemsOptions继承自ConfigurableNavigator,意味着可以像其他 VueUse Browser 函数一样通过window注入自定义的 navigator 环境(如测试中的 mock)。
返回值解析
| 属性 | 类型 | 说明 |
|---|---|---|
content | Readonly<Ref<ClipboardItems>> | 最近一次读取/写入的剪贴板条目(ClipboardItem[]),启用read后随事件刷新 |
copied | Readonly<ShallowRef<boolean>> | 复制成功后为true,copiedDuring毫秒后自动复位 |
copy | (content?: ClipboardItems) => Promise<void> | 异步写入剪贴板;传入source时参数可选 |
read | () => void | 手动触发一次剪贴板读取,刷新content |
重载的实战含义
两个重载签名值得注意:
- 不传
source时返回UseClipboardItemsReturn<false>,此时copy必须显式传入ClipboardItems; - 传入
source(MaybeRefOrGetter<ClipboardItems>)时返回UseClipboardItemsReturn<true>,此时copy参数可选,缺省使用source。
这与useClipboard的重载设计一脉相承(对比 useClipboard.md 中的UseClipboardOptions/UseClipboardReturn),TypeScript 会据此在编译期约束你的调用方式,避免漏传参数。
读取剪贴板:read与content
默认read: false时,useClipboardItems只负责写入,不主动触碰剪贴板内容(这既符合最小权限原则,也避免无谓的权限弹窗)。需要“读取并响应剪贴板变化”时,开启read:
const { content, read } = useClipboardItems({ read: true }) // 手动触发一次读取 read()content会持有读取到的ClipboardItem[]。要注意:在多数浏览器中,读取剪贴板同样要求文档处于聚焦状态、且页面获得clipboard-read权限;而写入通常只需要clipboard-write。实际产品中建议把“读取”做成显式用户操作(如点击“粘贴预览”按钮后再read()),而不是在挂载时自动读取。
用useClipboard对照理解权限模型
useClipboard的参考文档(useClipboard.md)同样强调:访问剪贴板内容受 Permissions API 门控,无用户授权时不允许读取或改写。两个函数共享同一套浏览器安全模型,差异只在数据形态:useClipboard返回text: Readonly<ShallowRef<string>>,useClipboardItems返回content: Readonly<Ref<ClipboardItems>>。此外useClipboard独有legacy选项,可在原生 Clipboard API 缺失时降级到document.execCommand('copy')(@default false);useClipboardItems依赖ClipboardItem本身,无法降级,因此isSupported的能力检测在这里更加关键。
AIRI 仓库中的剪贴板实践:从useClipboard到升级路径
仓库虽然没有直接调用useClipboardItems,但多处useClipboard的真实用法恰好勾勒出“何时需要升级到useClipboardItems”的决策边界:
场景一:复制格式化错误报告(useClipboard)
apps/stage-tamagotchi/src/renderer/pages/about.vue 在“关于/更新”页收集 Bug 报告:
const { copy: copyToClipboard, isSupported: isClipboardSupported } = useClipboard() async function onBugReportSubmit(payload: BugReportDialogSubmitPayload) { bugReportSending.value = true try { if (!isClipboardSupported.value) throw new Error('Clipboard API is unavailable') await copyToClipboard(payload.formattedReport) showBugReportDialog.value = false } catch (error) { bugReportSubmitError.value = error } finally { bugReportSending.value = false } }这是useClipboard的标准形态:复制对象是“格式化后的文本报告”,属于纯字符串场景。若未来希望错误报告同时携带 Markdown/HTML 表示,就能顺势改用useClipboardItems构造多 MIME 的ClipboardItem。
场景二:复制认证 Token(useClipboard+legacy)
apps/stage-tamagotchi/src/renderer/pages/settings/connection/index.vue 在服务器连接设置页复制 WebSocket 认证 Token:
import { refDebounced, useClipboard } from '@vueuse/core' const authTokenInput = shallowRef(authToken.value) const authTokenInputDebounced = refDebounced(authTokenInput, 500) const { copied: authTokenCopied, copy: copyAuthToken, isSupported: isClipboardSupported } = useClipboard({ source: authTokenInput, legacy: true }) const canCopyAuthToken = computed(() => isClipboardSupported.value && authTokenInput.value.length > 0)三个值得学习的细节:
source传 ref:authTokenInput是shallowRef,Token 变化后copy()无参调用也能复制最新值;legacy: true:桌面端若运行在较旧的 WebView 环境中、原生 Clipboard API 缺失,会降级到execCommand保持复制可用;isSupported驱动按钮禁用:canCopyAuthToken同时检查能力与内容长度,避免空复制。
Token 同样是字符串,因此用useClipboard而非useClipboardItems。
场景三:复制图片 Data URL(useClipboard)
packages/stage-pages/src/pages/devtools/image.vue 是开发工具页,读取用户选择的图片后复制其 Data URL:
const image = ref<File>() const imageDataURL = ref<string>('') const { copy } = useClipboard({ source: imageDataURL }) async function handleFileChange(event: Event) { const target = event.target as HTMLInputElement const file = target.files?.[0] if (file) { image.value = file const dataURL = await readAsDataURL(file) imageDataURL.value = dataURL } }注意:这里复制的是Data URL 字符串,因此useClipboard够用。但如果需求变成“把图片本身复制到剪贴板,粘贴后得到图片文件”,就必须升级为useClipboardItems——这正是前面“复制图片到剪贴板”一节展示的new ClipboardItem({ 'image/png': file })写法。同一个上传图片的交互,useClipboard复制“文本形式的 URL”,useClipboardItems复制“二进制形式的图片”,两者并不互斥,而是按粘贴目标选择。
场景四:渲染错误页兜底复制(useClipboard)
packages/stage-ui/src/components/scenes/stage-render-error.vue 同样使用useClipboard({ legacy: true })提供错误信息复制能力,与场景一/二同属文本复制。
小结:AIRI 现有四处调用全部命中useClipboard的“纯文本”定位;当仓库未来需要“复制图片/富文本/混合媒体”时,useClipboardItems是唯一合适的选择。两者的选择标准可以概括为一句决策规则:
复制目标是字符串 →
useClipboard(可加legacy降级);复制目标是ClipboardItem(图片、HTML、Blob、混合 MIME)→useClipboardItems。
实践建议与注意事项
- 先做能力检测:始终用
isSupported包裹功能入口,参考文档的示例模板(v-if="isSupported")就是推荐姿势;不支持时给出“Your browser does not support Clipboard API”之类的降级文案。 - 写入必须处于用户手势上下文:
copy()返回的 Promise 在权限不足或脱离用户激活时会 reject,务必try/catch并向用户反馈(参考 AIRI 关于页的onBugReportSubmit错误处理模式)。 - 读取权限更严格:默认不开
read;确需读取时,用显式用户操作触发,并处理clipboard-read权限被拒的情况。 copiedDuring可调:默认 1500ms 复位copied;若复制成功后需要展示更长的成功反馈,可自行调大。- 多 MIME 条目:一个
ClipboardItem可携带多个表示(text/html+text/plain+image/png等),让不同粘贴目标拿到最适合的格式。 - 与
useClipboard共用决策矩阵:参考 .agents/skills/vueuse-functions/SKILL.md 的指导原则——能用 VueUse 组合式函数实现的需求优先复用现成函数,保持代码简洁、可维护、高性能。
参考资料
- 本文主文档:.agents/skills/vueuse-functions/references/useClipboardItems.md
- 姊妹函数文档:.agents/skills/vueuse-functions/references/useClipboard.md
- 技能总览:.agents/skills/vueuse-functions/SKILL.md
- 仓库使用案例:
- apps/stage-tamagotchi/src/renderer/pages/about.vue(Bug 报告复制)
- apps/stage-tamagotchi/src/renderer/pages/settings/connection/index.vue(Token 复制 + legacy 降级)
- packages/stage-pages/src/pages/devtools/image.vue(图片 Data URL 复制)
- packages/stage-ui/src/components/scenes/stage-render-error.vue(渲染错误兜底复制)
【免费下载链接】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),仅供参考