正在解除封鎖剪貼簿

更安全地存取剪貼簿中的文字和圖片,且不會遭到封鎖

傳統上,存取系統剪貼簿的方式是透過 document.execCommand() 進行剪貼簿互動。雖然廣受支援,但這種剪下及貼上方法有其代價:剪貼簿存取是同步的,而且只能讀取及寫入 DOM。

這對小段文字來說沒問題,但如果為了轉移剪貼簿內容而封鎖網頁,使用者體驗就會很差。可能需要耗費時間進行清除或圖片解碼,才能安全地貼上內容。瀏覽器可能需要載入或內嵌貼上文件中的連結資源。這樣會導致頁面在等待磁碟或網路時遭到封鎖。假設您加入權限,要求瀏覽器在要求存取剪貼簿時封鎖網頁,同時,針對剪貼簿互動設定的 document.execCommand() 權限定義寬鬆,且因瀏覽器而異。

非同步剪貼簿 API 可解決這些問題,提供定義完善的權限模型,不會封鎖網頁。在大多數瀏覽器上,Async Clipboard API 只能處理文字和圖片,但支援程度因瀏覽器而異。請務必仔細研究下列各節的瀏覽器相容性總覽。

複製:將資料寫入剪貼簿

writeText()

如要將文字複製到剪貼簿,請呼叫 writeText()。由於這個 API 是非同步,writeText() 函式會傳回 Promise,並視傳遞的文字是否成功複製而解析或拒絕:

async function copyPageUrl() {
  try {
    await navigator.clipboard.writeText(location.href);
    console.log('Page URL copied to clipboard');
  } catch (err) {
    console.error('Failed to copy: ', err);
  }
}

Browser Support

  • Chrome: 66.
  • Edge: 79.
  • Firefox: 63.
  • Safari: 13.1.

Source

write()

事實上,writeText() 只是通用 write() 方法的便利方法,後者也允許您將圖片複製到剪貼簿。與 writeText() 相同,這個函式是非同步函式,會傳回 Promise。

如要將圖片寫入剪貼簿,您需要以 blob 形式提供圖片。其中一種做法是使用 fetch() 向伺服器要求圖片,然後對回應呼叫 blob()

基於各種原因,從伺服器要求圖片可能不理想或不可行。幸好,您也可以將圖片繪製到畫布上,並呼叫畫布的 toBlob() 方法。

接著,將 ClipboardItem 物件陣列做為參數傳遞至 write() 方法。目前一次只能傳遞一張圖片,但我們希望日後能支援多張圖片。ClipboardItem 會將圖片的 MIME 類型做為鍵,Blob 做為值,並以物件形式傳遞。如果是從 fetch()canvas.toBlob() 取得的 Blob 物件,blob.type 屬性會自動包含圖片的正確 MIME 類型。

try {
  const imgURL = '/images/generic/file.png';
  const data = await fetch(imgURL);
  const blob = await data.blob();
  await navigator.clipboard.write([
    new ClipboardItem({
      // The key is determined dynamically based on the blob's type.
      [blob.type]: blob
    })
  ]);
  console.log('Image copied.');
} catch (err) {
  console.error(err.name, err.message);
}

或者,您也可以對 ClipboardItem 物件編寫 Promise。如要使用這個模式,您必須事先知道資料的 MIME 類型。

try {
  const imgURL = '/images/generic/file.png';
  await navigator.clipboard.write([
    new ClipboardItem({
      // Set the key beforehand and write a promise as the value.
      'image/png': fetch(imgURL).then(response => response.blob()),
    })
  ]);
  console.log('Image copied.');
} catch (err) {
  console.error(err.name, err.message);
}

Browser Support

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 127.
  • Safari: 13.1.

Source

複製活動

如果使用者啟動剪貼簿複製作業,但呼叫 preventDefault(),則 copy 事件會包含 clipboardData 屬性,其中已包含正確格式的項目。如要實作自己的邏輯,請呼叫 preventDefault(),防止預設行為發生,改為採用自己的實作方式。在此情況下,clipboardData 會是空白。 假設某個頁面包含文字和圖片,當使用者選取所有內容並啟動剪貼簿複製作業時,自訂解決方案應捨棄文字,只複製圖片。如要達成這個目標,請參閱下方的程式碼範例。 本範例未涵蓋的內容,是當系統不支援 Clipboard API 時,如何回復使用較早的 API。

<!-- The image we want on the clipboard. -->
<img src="kitten.webp" alt="Cute kitten.">
<!-- Some text we're not interested in. -->
<p>Lorem ipsum</p>
document.addEventListener("copy", async (e) => {
  // Prevent the default behavior.
  e.preventDefault();
  try {
    // Prepare an array for the clipboard items.
    let clipboardItems = [];
    // Assume `blob` is the blob representation of `kitten.webp`.
    clipboardItems.push(
      new ClipboardItem({
        [blob.type]: blob,
      })
    );
    await navigator.clipboard.write(clipboardItems);
    console.log("Image copied, text ignored.");
  } catch (err) {
    console.error(err.name, err.message);
  }
});

copy 活動:

Browser Support

  • Chrome: 1.
  • Edge: 12.
  • Firefox: 22.
  • Safari: 3.

Source

針對 ClipboardItem

Browser Support

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 127.
  • Safari: 13.1.

Source

貼上:從剪貼簿讀取資料

readText()

如要從剪貼簿讀取文字,請呼叫 navigator.clipboard.readText() 並等待傳回的 Promise 解析:

async function getClipboardContents() {
  try {
    const text = await navigator.clipboard.readText();
    console.log('Pasted content: ', text);
  } catch (err) {
    console.error('Failed to read clipboard contents: ', err);
  }
}

Browser Support

  • Chrome: 66.
  • Edge: 79.
  • Firefox: 125.
  • Safari: 13.1.

Source

read()

navigator.clipboard.read() 方法也是非同步,並會傳回 Promise。如要從剪貼簿讀取圖片,請取得物件清單 ClipboardItem,然後逐一疊代。

每個 ClipboardItem 都能以不同類型保存內容,因此您需要再次使用 for...of 迴圈,逐一查看類型清單。針對每種型別,請呼叫 getType() 方法,並將目前的型別做為引數,取得對應的 Blob。與先前一樣,這個代碼與圖片無關,而且適用於其他未來的檔案類型。

async function getClipboardContents() {
  try {
    const clipboardItems = await navigator.clipboard.read();
    for (const clipboardItem of clipboardItems) {
      for (const type of clipboardItem.types) {
        const blob = await clipboardItem.getType(type);
        console.log(URL.createObjectURL(blob));
      }
    }
  } catch (err) {
    console.error(err.name, err.message);
  }
}

Browser Support

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 127.
  • Safari: 13.1.

Source

使用貼上的檔案

使用者可以透過 Ctrl+CCtrl+V 等剪貼簿鍵盤快速鍵,輕鬆複製及貼上檔案。 Chromium 會將剪貼簿中的唯讀檔案公開,如下所述。 當使用者按下作業系統的預設貼上快速鍵,或在瀏覽器的選單列中依序點選「編輯」和「貼上」時,就會觸發這項事件。不需要進一步的管道程式碼。

document.addEventListener("paste", async e => {
  e.preventDefault();
  if (!e.clipboardData.files.length) {
    return;
  }
  const file = e.clipboardData.files[0];
  // Read the file's contents, assuming it's a text file.
  // There is no way to write back to it.
  console.log(await file.text());
});

Browser Support

  • Chrome: 3.
  • Edge: 12.
  • Firefox: 3.6.
  • Safari: 4.

Source

貼上事件

如先前所述,我們計畫推出可搭配 Clipboard API 使用的事件,但目前您可以使用現有的 paste 事件。這項功能可與新的非同步方法搭配使用,讀取剪貼簿文字。與 copy 事件一樣,別忘了呼叫 preventDefault()

document.addEventListener('paste', async (e) => {
  e.preventDefault();
  const text = await navigator.clipboard.readText();
  console.log('Pasted text: ', text);
});

Browser Support

  • Chrome: 1.
  • Edge: 12.
  • Firefox: 22.
  • Safari: 3.

Source

處理多個 MIME 類型

大多數實作方式會將多種資料格式放在剪貼簿中,以供單一剪下或複製作業使用。原因有二:應用程式開發人員無法得知使用者要將文字或圖片複製到哪個應用程式,而且許多應用程式都支援將結構化資料貼為純文字。通常會向使用者顯示「編輯」選單項目,並提供「貼上並比對樣式」或「貼上時不套用格式」等名稱。

以下範例說明如何執行這項操作。本範例使用 fetch() 取得圖片資料,但圖片資料也可能來自 <canvas>File System Access API

async function copy() {
  const image = await fetch('kitten.png').then(response => response.blob());
  const text = new Blob(['Cute sleeping kitten'], {type: 'text/plain'});
  const item = new ClipboardItem({
    'text/plain': text,
    'image/png': image
  });
  await navigator.clipboard.write([item]);
}

安全性和權限

剪貼簿存取權向來是瀏覽器的安全隱憂。如果沒有適當的權限,網頁可能會在使用者不知情的情況下,將各種惡意內容複製到剪貼簿,貼上後會造成災難性的結果。假設某個網頁在您不知情的情況下,將 rm -rf /解壓縮炸彈圖片複製到剪貼簿。

瀏覽器提示,要求使用者授予剪貼簿權限。
Clipboard API 的權限提示。

如果網頁能不受限制地讀取剪貼簿內容,問題就更大了。使用者經常將密碼和個人詳細資料等私密資訊複製到剪貼簿,而任何網頁都可能在使用者不知情的情況下讀取這些資訊。

與許多新 API 一樣,Clipboard API 僅支援透過 HTTPS 提供的網頁。為防範濫用行為,只有在頁面為使用中的分頁時,才能存取剪貼簿。作用中分頁中的網頁可以寫入剪貼簿,不必要求權限,但一律需要權限才能讀取剪貼簿內容。

複製及貼上作業的權限已新增至 Permissions API。當頁面是有效分頁時,系統會自動授予 clipboard-write 權限。您必須要求 clipboard-read 權限,方法是嘗試從剪貼簿讀取資料。下列程式碼顯示後者:

const queryOpts = { name: 'clipboard-read', allowWithoutGesture: false };
const permissionStatus = await navigator.permissions.query(queryOpts);
// Will be 'granted', 'denied' or 'prompt':
console.log(permissionStatus.state);

// Listen for changes to the permission state
permissionStatus.onchange = () => {
  console.log(permissionStatus.state);
};

您也可以使用 allowWithoutGesture 選項,控制是否需要使用者手勢才能叫用剪下或貼上功能。這個值的預設值因瀏覽器而異,因此請務必加入。

這時,Clipboard API 的非同步特性就派上用場了:嘗試讀取或寫入剪貼簿資料時,如果使用者尚未授予權限,系統會自動提示使用者授予權限。由於 API 是以 Promise 為基礎,因此這完全是透明的,使用者拒絕剪貼簿權限會導致 Promise 遭到拒絕,因此網頁可以適當回應。

由於瀏覽器只允許在網頁為有效分頁時存取剪貼簿,因此您會發現,如果直接將某些範例貼到瀏覽器的控制台中,這些範例不會執行,因為開發人員工具本身就是有效分頁。訣竅是使用 setTimeout() 延遲剪貼簿存取權,然後在函式呼叫前,快速點選網頁內將焦點移至網頁:

setTimeout(async () => {
  const text = await navigator.clipboard.readText();
  console.log(text);
}, 2000);

整合權限政策

如要在 iframe 中使用 API,您需要透過權限政策啟用 API。這項政策定義的機制可選擇性啟用及停用各種瀏覽器功能和 API。具體來說,您需要視應用程式的需求,傳遞 clipboard-readclipboard-write,或兩者皆傳遞。

<iframe
    src="index.html"
    allow="clipboard-read; clipboard-write"
>
</iframe>

特徵偵測

如要在支援所有瀏覽器的情況下使用 Async Clipboard API,請測試 navigator.clipboard,並改用較早的方法。舉例來說,以下說明如何實作貼上功能,以便納入其他瀏覽器。

document.addEventListener('paste', async (e) => {
  e.preventDefault();
  let text;
  if (navigator.clipboard) {
    text = await navigator.clipboard.readText();
  }
  else {
    text = e.clipboardData.getData('text/plain');
  }
  console.log('Got pasted text: ', text);
});

但這並非全貌。在非同步剪貼簿 API 推出前,各個網頁瀏覽器採用的複製及貼上實作方式不盡相同。在大多數瀏覽器中,可以使用 document.execCommand('copy')document.execCommand('paste') 觸發瀏覽器本身的複製和貼上功能。如果要複製的文字是 DOM 中沒有的字串,則必須將該字串插入 DOM 並選取:

button.addEventListener('click', (e) => {
  const input = document.createElement('input');
  input.style.display = 'none';
  document.body.appendChild(input);
  input.value = text;
  input.focus();
  input.select();
  const result = document.execCommand('copy');
  if (result === 'unsuccessful') {
    console.error('Failed to copy text.');
  }
  input.remove();
});

示範

您可以在下列範例中試用 Async Clipboard API。第一個範例示範如何將文字移至剪貼簿,以及從剪貼簿移出。

如要使用圖片試用 API,請使用這個試用版。請注意,系統僅支援 PNG 格式,且僅適用於少數瀏覽器

特別銘謝

非同步剪貼簿 API 由 Darwin HuangGary Kačmarčík 實作。Darwin 也提供示範。 感謝 Kyarik 和 Gary Kačmarčík 再次審查本文部分內容。

主頁橫幅圖片來源:Markus Winkler,取自 Unsplash