更安全地存取剪貼簿的文字和圖片
傳統的存取系統剪貼簿的方法是透過
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);
}
}
write()
事實上,writeText()
只是一般 write()
方法的便利方法,您也可以使用這個方法將圖片複製到剪貼簿。喜歡「writeText()
」,它
並傳回 Promise
如要將圖片寫入剪貼簿,您需要將圖片設為 blob
。這麼做的方法之一,是使用 fetch()
向伺服器要求圖片,然後在回應中呼叫 blob()
。
基於各種原因,從伺服器要求圖片可能不切實際或無法執行。幸好,只要將圖片繪製到畫布上
呼叫畫布」
toBlob()
方法。
接著,將 ClipboardItem
物件的陣列做為參數傳遞至 write()
方法。目前您只能一次傳送一張圖片,但我們希望日後能支援多張圖片。ClipboardItem
採用的
圖片的 MIME 類型做為鍵,並將 blob 做為值。適用於 blob
從 fetch()
或 canvas.toBlob()
(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 剪貼簿 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。