剪貼簿操作 (Clipboard API)
在網頁應用程式中,「複製到剪貼簿」是最常見的互動需求之一。從複製優惠碼、分享連結,到程式碼片段的取用,這個看似簡單的功能背後,其實經歷了瀏覽器 API 的重大演進。
本篇將帶你深入了解現代瀏覽器的 Clipboard API,並實作一個相容性極佳的 Vue 3 useClipboard 組合函式。
一、 剪貼簿 API 的演進
在過去,我們依賴 document.execCommand('copy') 來實作複製功能。雖然它相容性極佳,但它是同步操作(可能阻塞主執行緒),且使用起來相當繁瑣(需要先選取文字)。
現代瀏覽器推出了全新的 Async Clipboard API,提供更安全、非同步的剪貼簿存取方式。
1.1 新舊對比
| 特性 | execCommand (舊) | Clipboard API (新) |
|---|---|---|
| 語法 | document.execCommand('copy') | navigator.clipboard.writeText() |
| 執行緒 | 同步 (Synchronous) | 非同步 (Asynchronous / Promise) |
| 安全性 | 較低 | 需 HTTPS (localhost 除外) |
| 權限 | 自動允許 (由使用者操作觸發) | 寫入自動允許,讀取需授權 |
| 狀態 | 已棄用 (Deprecated) | 推薦使用 |
1.2 為什麼需要降級方案 (Fallback)?
雖然 Clipboard API 是未來趨勢,但在某些舊版瀏覽器或非安全情境(HTTP)下可能無法使用。為了確保功能在所有環境下都能運作,我們的 useClipboard 必須具備「自動降級」的能力。
二、 設計 Composable 介面
我們參考 VueUse 的設計理念,定義 useClipboard 的介面:
typescript
// types/useClipboard.ts
import type { Ref, ComputedRef } from "vue";
export interface UseClipboardOptions {
/**
* 複製後的重置時間 (毫秒)
* @default 1500
*/
copiedDuring?: number;
}
export interface UseClipboardReturn {
/** 是否支援 Clipboard API */
isSupported: ComputedRef<boolean>;
/** 當前剪貼簿內容 */
text: Ref<string>;
/** 是否剛剛完成複製 (用於顯示成功提示) */
copied: Ref<boolean>;
/** 複製函式 */
copy: (text: string) => Promise<void>;
/** 讀取剪貼簿函式 */
read: () => Promise<void>;
}三、 實作 useClipboard Composable
3.1 核心實作
typescript
// composables/useClipboard.ts
import { ref, computed } from "vue";
import type {
UseClipboardOptions,
UseClipboardReturn,
} from "@/types/useClipboard";
export function useClipboard(
options: UseClipboardOptions = {},
): UseClipboardReturn {
const { copiedDuring = 1500 } = options;
const isSupported = computed(
() => typeof navigator !== "undefined" && "clipboard" in navigator,
);
const text = ref("");
const copied = ref(false);
// 計時器參考
let timer: any = null;
// 處理複製成功後的狀態重置
function handleCopied(content: string) {
text.value = content;
copied.value = true;
// 清除舊的計時器
if (timer) clearTimeout(timer);
// 設定新的計時器重置狀態
timer = setTimeout(() => {
copied.value = false;
}, copiedDuring);
}
// 舊版 API 實作 (execCommand)
function legacyCopy(content: string) {
const textArea = document.createElement("textarea");
textArea.value = content;
// 避免影響版面
textArea.style.position = "fixed";
textArea.style.left = "-9999px";
textArea.style.top = "0";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand("copy");
handleCopied(content);
} catch (e) {
console.error("Legacy copy failed", e);
throw e;
} finally {
document.body.removeChild(textArea);
}
}
// 複製函式
async function copy(content: string) {
if (isSupported.value) {
try {
await navigator.clipboard.writeText(content);
handleCopied(content);
} catch (e) {
// 如果新 API 失敗,嘗試降級到舊 API
console.warn("Clipboard API failed, falling back to execCommand", e);
legacyCopy(content);
}
} else {
legacyCopy(content);
}
}
// 讀取函式
async function read() {
if (!isSupported.value) return;
try {
const content = await navigator.clipboard.readText();
text.value = content;
} catch (e) {
console.error("Failed to read clipboard", e);
}
}
return {
isSupported,
text,
copied,
copy,
read,
};
}NOTE
HTTPS 限制:navigator.clipboard 只能在安全環境(HTTPS 或 localhost)下使用。如果您的應用部署在 HTTP 環境,程式會自動降級使用 legacyCopy。
四、 實際應用範例
4.1 一鍵複製按鈕
這是最常見的使用情境,使用者點擊按鈕後將文字複製,並顯示「已複製!」提示。
vue
<script setup lang="ts">
import { useClipboard } from "@/composables/useClipboard";
const source = "npm install @vueuse/core";
const { copy, copied, isSupported } = useClipboard();
</script>
<template>
<div class="copy-box">
<code>{{ source }}</code>
<button @click="copy(source)">
<!-- 根據狀態切換顯示圖示/文字 -->
<span v-if="!copied">📋 複製</span>
<span v-else class="success">✅ 已複製!</span>
</button>
</div>
<p class="hint">
API 支援度:
{{ isSupported ? "支援 (Clipboard API)" : "降級 (execCommand)" }}
</p>
</template>
<style scoped>
.copy-box {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
background: #f5f5f5;
border-radius: 8px;
}
.success {
color: #10b981;
font-weight: bold;
}
</style>4.2 讀取剪貼簿 (貼上並解析)
除了寫入,有時我們也需要讀取剪貼簿內容,例如「貼上分享碼」功能。
WARNING
權限要求:readText() 會觸發瀏覽器的權限詢問視窗,使用者必須明確「允許」後才能讀取內容。
vue
<script setup lang="ts">
import { useClipboard } from "@/composables/useClipboard";
const { text, read, isSupported } = useClipboard();
async function handlePaste() {
await read();
// text.value 自動更新為剪貼簿內容
console.log("剪貼簿內容:", text.value);
}
</script>
<template>
<div v-if="isSupported">
<button @click="handlePaste">📋 從剪貼簿貼上</button>
<div v-if="text" class="preview">讀取到的內容: {{ text }}</div>
</div>
</template>總結
| 概念 | 說明 |
|---|---|
| Navigator.clipboard | 現代瀏覽器的非同步剪貼簿 API |
| writeText() | 寫入文字,通常不需要額外權限(需由使用者觸發) |
| readText() | 讀取文字,必須經過使用者授權 |
| 降級處理 (Fallback) | 在 API 不可用時,退回使用 document.execCommand |
| 使用者體驗 | 複製後應提供視覺回饋 (如 1.5秒後復原狀態) |
TIP
權限最佳實踐:永遠不要在頁面載入時直接呼叫 readText(),這會導致糟糕的使用者體驗(突兀的權限請求)。請務必將其綁定在明確的按鈕點擊事件上(例如「貼上」按鈕)。
進階挑戰
- 複製圖片:研究
clipboard.write()方法,嘗試複製圖片 Blob 到剪貼簿。 - 富文本複製:實作複製 HTML 格式的內容(如帶有樣式的表格)。
- 剪貼簿監聽:使用
paste事件監聽器,實作攔截使用者的貼上行為並過濾內容。