更安全地存取剪貼簿的文字和圖片
傳統上,您可以透過 document.execCommand()
存取系統剪貼簿,以便與剪貼簿互動。雖然這項功能受到廣泛支援,但這種剪貼方式的成本很高:剪貼簿存取是同步的,只能讀取及寫入 DOM。
即使插入少量文字也沒問題,但在許多情況下,封鎖網頁以使用剪貼簿傳輸功能並不會帶來不良體驗。您可能需要進行耗時的清理或圖片解碼作業,才能安全地貼上內容。瀏覽器可能需要從貼上的文件中載入或內嵌連結的資源。這樣會在等待磁碟或網路時阻斷頁面。試想一下,如果您在混合中加入權限,要求瀏覽器在要求剪貼簿存取權時封鎖網頁,同時,document.execCommand()
的權限 (用於剪貼簿互動) 定義較為寬鬆,且不同瀏覽器的定義也不同。
非同步剪貼簿 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);
}
}
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
物件。對於這種模式,您必須事先知道資料的 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);
}
複製事件
如果使用者啟動剪貼簿複製作業,但「未」呼叫 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
事件:
針對 ClipboardItem
:
貼上:讀取剪貼簿中的資料
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);
}
}
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);
}
}
使用貼上的檔案
使用者可以使用剪貼簿鍵盤快速鍵,例如 ctrl + c 和 ctrl + 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());
});
貼上事件
如先前所述,我們計劃推出事件,以便與 Clipboard API 搭配運作,但目前您可以使用現有的 paste
事件。它與用於讀取剪貼簿文字的新非同步方法搭配使用時,運作良好。如同 copy
事件,別忘了呼叫 preventDefault()
。
document.addEventListener('paste', async (e) => {
e.preventDefault();
const text = await navigator.clipboard.readText();
console.log('Pasted text: ', text);
});
處理多個 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 僅支援透過 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 是基於承諾,因此這項作業完全透明,使用者拒絕剪貼簿權限會導致承諾遭到拒絕,讓網頁可以適當回應。
由於瀏覽器只允許在網頁為有效分頁時存取剪貼簿,因此如果直接將開發人員工具貼到瀏覽器的控制台,您會發現其中一些範例無法執行,因為開發人員工具本身就是有效分頁。以下提供一個訣竅:您可以使用 setTimeout()
延後剪貼簿存取權,然後在頁面中快速按一下,在呼叫函式前將焦點移至該項目:
setTimeout(async () => {
const text = await navigator.clipboard.readText();
console.log(text);
}, 2000);
權限政策整合
如要在 iframe 中使用 API,您必須透過權限政策啟用 API,該政策定義了一種機制,可讓您有選擇地啟用及停用各種瀏覽器功能和 API。具體來說,您需要傳遞 clipboard-read
或 clipboard-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 剪貼簿 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。
相關連結
特別銘謝
非同步剪貼簿 API 由 Darwin Huang 和 Gary Kačmarčík 實作。Darwin 也提供了示範。感謝 Kyarik 與 Gary Kačmarčík 致謝,以便我們審查本文的部分內容。
主頁橫幅由 Markus Winkler 提供,並位於 Unsplash 網站上。