正在解除封鎖剪貼簿

強化文字和圖片的剪貼簿存取權,安全性更高

存取系統剪貼簿的傳統方法是透過 document.execCommand() 進行剪貼簿互動。雖然目前廣泛支援這種切割與貼上的方法,但這是一項成本高低的:剪貼簿存取是同步的,且只能讀取及寫入 DOM。

對於一小段文字來說這是正常現象,但在許多情況下,禁止頁面以剪貼簿轉移會帶來不良體驗。但可能需要耗費時間進行掃除或圖片解碼,才能安全貼上內容。瀏覽器可能需要從貼上的文件載入或內嵌連結資源。這樣會在等待磁碟或網路時封鎖頁面。想像一下,在組合中新增權限,要求瀏覽器在要求剪貼簿存取權時封鎖網頁。同時,針對剪貼簿互動在 document.execCommand() 周圍設置的權限並未定義,且因瀏覽器而異。

Async Clipboard API 可以解決這些問題,提供妥善定義的權限模型,不會封鎖該網頁。Async Clipboard API 可以在多數瀏覽器中處理文字和圖片,但支援各有不同。請務必針對以下各節仔細研究瀏覽器相容性總覽。

複製:將資料寫入剪貼簿

writeText()

如要將文字複製到剪貼簿,請呼叫 writeText()。由於這個 API 為非同步,writeText() 函式會傳回 Promise,並根據傳遞的文字是否成功複製,判斷要解析或拒絕的 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);
  }
}

瀏覽器支援

  • 66
  • 79
  • 63
  • 13.1

資料來源

寫入()

實際上,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 物件。在這個模式中,您必須事先瞭解資料的 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);
}

瀏覽器支援

  • 66
  • 79
  • 13.1

資料來源

複製事件

如果使用者啟動剪貼簿複製作業,但「未」呼叫 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 事件:

瀏覽器支援

  • 1
  • 12
  • 22
  • 3

資料來源

針對 ClipboardItem

瀏覽器支援

  • 76
  • 79
  • 13.1

資料來源

貼上:讀取剪貼簿中的資料

readText()

如要讀取剪貼簿中的文字,請呼叫 navigator.clipboard.readText(),並等待傳回的承諾使用解析完成:

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);
  }
}

瀏覽器支援

  • 66
  • 79
  • 13.1

資料來源

read()

navigator.clipboard.read() 方法也為非同步性質,且會傳回承諾。如要讀取剪貼簿中的圖片,請取得 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);
  }
}

瀏覽器支援

  • 66
  • 79
  • 13.1

資料來源

使用貼上的檔案

如果使用者能夠使用剪貼簿鍵盤快速鍵 (例如 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());
});

瀏覽器支援

  • 3
  • 12
  • 3.6
  • 4

資料來源

貼上事件

如前文所述,我們計劃推出事件來與剪貼簿 API 搭配使用,但目前您可以使用現有的 paste 事件。可搭配新的非同步方法讀取剪貼簿文字。和 copy 事件一樣,不要忘記呼叫 preventDefault()

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

瀏覽器支援

  • 1
  • 12
  • 22
  • 3

資料來源

處理多種 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 /解壓縮炸彈圖片複製到剪貼簿。

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

想要對網頁授予不間斷的讀取權限,更是一大麻煩。使用者經常將機密資訊 (例如密碼和個人詳細資料) 複製到剪貼簿,供使用者在不知情的情況下由任何網頁讀取。

和許多新的 API 一樣,剪貼簿 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 選項,控管是否需要使用者手勢才能叫用剪下或貼上操作。這個值的預設值會因瀏覽器而異,因此建議您一律加入。

在本例中,剪貼簿 API 的非同步特性很實用:在嘗試讀取或寫入剪貼簿資料時,若尚未授予權限,會自動提示使用者授予權限。由於這個 API 採承諾制,因此這完全是公開透明的,當使用者拒絕剪貼簿權限會導致承諾拒絕時,頁面才能正確回應。

由於瀏覽器只會在使用中的分頁時允許存取剪貼簿,因此您會發現某些範例如果直接貼到瀏覽器的控制台中,便無法執行,因為開發人員工具本身就是使用中的分頁。在此提供一個訣竅:使用 setTimeout() 延後存取剪貼簿,然後在呼叫函式之前快速按一下頁面中的內容,將焦點移至該項目:

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

權限政策整合

如要在 iframe 中使用 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);
});

但不是全盤了。在 Async Clipboard 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 進行遊戲。您可以在 Glitch 中重混示範文字示範圖片,藉此嘗試不同內容。

第一個範例說明如何在剪貼簿上移入及移出文字。

如要試用含有圖片的 API,請使用這個示範。提醒您,只有少數瀏覽器支援 PNG 格式。

特別銘謝

Async Clipboard API 是由 Darwin HuangGary Kačmarčík 實作。Darwin 也提供了示範內容。感謝 Kyarik 與 Gary Kačmarčík 關心本文所述情況的評語。

Markus WinklerUnsplash 上提供的主頁橫幅。