Clipboard API 實戰:瀏覽器剪貼簿讀寫與安全策略

技术架构(更新於 2026年6月3日)

Clipboard API:現代剪貼簿互動標準

瀏覽器剪貼簿操作經歷了從 document.execCommandAsync Clipboard API 的演進:

對比項 execCommand('copy') Async Clipboard API
標準 已廢棄 W3C 推薦
非同步 否(同步阻塞) 是(Promise)
權限 隱式(需使用者操作) 顯式 Permissions API
富文字 不支援 支援(ClipboardItem)
自訂格式 不支援 支援(web custom formats)
圖片讀寫 不支援 支援
可靠性 依賴選區 直接操作剪貼簿

核心 API 詳解

Clipboard API 介面

interface Clipboard extends EventTarget {
  read(): Promise<ClipboardItem[]>;
  readText(): Promise<string>;
  write(data: ClipboardItem[]): Promise<void>;
  writeText(text: string): Promise<void>;
}

interface ClipboardItem {
  readonly types: string[];
  getType(type: string): Promise<Blob>;
}

文字讀寫

async function copyText(text: string) {
  try {
    await navigator.clipboard.writeText(text);
    showToast('已複製到剪貼簿');
  } catch (err) {
    console.error('複製失敗:', err);
    fallbackCopyText(text);
  }
}

async function pasteText(): Promise<string> {
  try {
    const text = await navigator.clipboard.readText();
    return text;
  } catch (err) {
    console.error('讀取失敗:', err);
    return '';
  }
}

富文字與多格式寫入

ClipboardItem 多格式

async function copyRichText(html: string, plainText: string) {
  const clipboardItem = new ClipboardItem({
    'text/html': new Blob([html], { type: 'text/html' }),
    'text/plain': new Blob([plainText], { type: 'text/plain' }),
  });
  await navigator.clipboard.write([clipboardItem]);
}

copyRichText(
  '<strong>重要通知</strong>:請於 <em>週五</em> 前提交',
  '重要通知:請於週五前提交'
);

圖片複製

async function copyImage(pngBlob: Blob) {
  await navigator.clipboard.write([
    new ClipboardItem({ 'image/png': pngBlob })
  ]);
}

async function copyCanvas(canvas: HTMLCanvasElement) {
  const blob = await new Promise<Blob>((resolve) => {
    canvas.toBlob((b) => resolve(b!), 'image/png');
  });
  await navigator.clipboard.write([
    new ClipboardItem({ 'image/png': blob })
  ]);
}

圖片貼上

document.addEventListener('paste', async (e) => {
  const items = e.clipboardData?.items;
  if (!items) return;

  for (const item of items) {
    if (item.type.startsWith('image/')) {
      const blob = item.getAsFile()!;
      const url = URL.createObjectURL(blob);
      const img = document.createElement('img');
      img.src = url;
      document.body.appendChild(img);
    }
  }
});

自訂 MIME 類型(Web Custom Formats)

async function copyCustomData(data: object) {
  const json = JSON.stringify(data);
  await navigator.clipboard.write([
    new ClipboardItem({
      'web application/json': new Blob([json], { type: 'application/json' }),
      'text/plain': new Blob([json], { type: 'text/plain' }),
    })
  ]);
}

async function pasteCustomData(): Promise<object | null> {
  const items = await navigator.clipboard.read();
  for (const item of items) {
    if (item.types.includes('web application/json')) {
      const blob = await item.getType('web application/json');
      const text = await blob.text();
      return JSON.parse(text);
    }
  }
  return null;
}

web 前綴是自訂格式的安全要求,防止網頁讀取原生剪貼簿格式。


權限與安全策略

Permissions API

async function checkClipboardPermission(): Promise<boolean> {
  try {
    const result = await navigator.permissions.query({
      name: 'clipboard-read' as PermissionName
    });
    return result.state === 'granted';
  } catch {
    return false;
  }
}

安全限制一覽

操作 要求 說明
writeText 使用者啟動 需 click/keydown 等使用者手勢
write 使用者啟動 同上
readText 使用者啟動 + 權限 需使用者授權彈窗
read 使用者啟動 + 權限 需使用者授權彈窗
焦點視窗 文件需聚焦 失焦視窗呼叫會拒絕

使用者啟動檢查

button.addEventListener('click', async () => {
  // ✅ 在 click 事件中,有使用者啟動
  await navigator.clipboard.writeText('copied!');
});

setTimeout(async () => {
  await navigator.clipboard.writeText('fails'); // ❌ DOMException
}, 1000);

降級方案:execCommand

function fallbackCopyText(text: string): boolean {
  const textarea = document.createElement('textarea');
  textarea.value = text;
  textarea.style.position = 'fixed';
  textarea.style.left = '-9999px';
  document.body.appendChild(textarea);
  textarea.select();

  try {
    const success = document.execCommand('copy');
    document.body.removeChild(textarea);
    return success;
  } catch {
    document.body.removeChild(textarea);
    return false;
  }
}

async function safeCopy(text: string) {
  if (navigator.clipboard?.writeText) {
    try {
      await navigator.clipboard.writeText(text);
      return true;
    } catch {}
  }
  return fallbackCopyText(text);
}

paste 事件深度處理

攔截與轉換貼上內容

editor.addEventListener('paste', (e) => {
  e.preventDefault();
  const clipboardData = e.clipboardData!;

  const html = clipboardData.getData('text/html');
  if (html) {
    const sanitized = sanitizeHTML(html);
    insertHTML(sanitized);
    return;
  }

  const text = clipboardData.getData('text/plain');
  if (text) {
    const linked = text.replace(
      /https?:\/\/\S+/g,
      (url) => `<a href="${url}">${url}</a>`
    );
    insertHTML(linked);
  }
});

剪貼簿資料類型檢測

editor.addEventListener('paste', (e) => {
  const data = e.clipboardData!;
  console.log('類型列表:', [...data.types]);

  const priorityOrder = [
    'text/html',
    'application/json',
    'text/plain',
    'image/png',
  ];

  for (const type of priorityOrder) {
    if (data.types.includes(type)) {
      const content = data.getData(type);
      console.log(`貼上 ${type}:`, content.slice(0, 100));
      break;
    }
  }
});

實戰:複製按鈕元件

class CopyButton {
  private timeoutId: number | null = null;

  constructor(private button: HTMLButtonElement, private getText: () => string) {
    button.addEventListener('click', () => this.handleCopy());
  }

  private async handleCopy() {
    const text = this.getText();
    const success = await safeCopy(text);

    if (success) {
      this.showFeedback('已複製!');
    } else {
      this.showFeedback('複製失敗');
    }
  }

  private showFeedback(message: string) {
    const original = this.button.textContent;
    this.button.textContent = message;
    this.button.disabled = true;

    if (this.timeoutId) clearTimeout(this.timeoutId);
    this.timeoutId = window.setTimeout(() => {
      this.button.textContent = original;
      this.button.disabled = false;
    }, 2000);
  }
}

瀏覽器相容性

API Chrome Firefox Safari Edge
writeText 66+ 63+ 13.1+ 79+
readText 66+ 63+ 13.1+ 79+
write (ClipboardItem) 76+ 87+ 13.1+ 79+
read 76+ 87+ 13.1+ 79+
Web Custom Formats 104+ 不支援 不支援 104+
圖片剪貼簿 76+ 87+ 13.1+ 79+

最佳實踐

  1. 始終提供降級execCommand 作為 Clipboard API 不可用時的後備
  2. 使用者啟動內呼叫:複製操作必須在 click/keydown 等事件中觸發
  3. 回饋必做:複製成功/失敗都應給出視覺回饋
  4. 讀取需謹慎read() 涉及隱私,僅在必要時請求
  5. sanitize 貼上內容:HTML 貼上必須過濾 XSS 風險

總結

Async Clipboard API 是現代瀏覽器剪貼簿操作的標準方案,支援文字、富文字、圖片和自訂格式的讀寫。相比 execCommand,它更可靠、更安全、功能更強大,但需注意使用者啟動要求和權限模型。

推薦使用 剪貼簿檢視器 除錯剪貼簿內容,文字取代 批次處理貼上文字,大小寫轉換 規範化剪貼簿文字。

本站提供瀏覽器本地工具,免註冊即可試用 →

#Clipboard API#剪贴板#异步API#浏览器安全#用户交互