การเข้าถึงคลิปบอร์ดที่ปลอดภัยยิ่งขึ้นสำหรับข้อความและรูปภาพ
วิธีดั้งเดิมในการเข้าถึงคลิปบอร์ดของระบบคือผ่าน document.execCommand()
เพื่อโต้ตอบกับคลิปบอร์ด แม้จะมีการรองรับอย่างแพร่หลาย แต่วิธีตัดและวางแบบนี้มีต้นทุนสูง การเข้าถึงคลิปบอร์ดเกิดขึ้นพร้อมกัน และสามารถอ่านและเขียนไปยัง DOM ได้เท่านั้น
ข้อความสั้นๆ ถือว่ายอมรับได้ แต่มีหลายกรณีที่การบล็อกหน้าเว็บเพื่อโอนคลิปบอร์ดนั้นทำให้ผู้ใช้ได้รับประสบการณ์ที่ไม่ดี คุณอาจต้องทำความสะอาดหรือถอดรหัสรูปภาพซึ่งใช้เวลานานก่อนที่จะวางเนื้อหาได้อย่างปลอดภัย เบราว์เซอร์อาจต้องโหลดหรือแทรกทรัพยากรที่ลิงก์ภายในหน้าจากเอกสารที่วาง ซึ่งจะบล็อกหน้าเว็บขณะที่รอในดิสก์หรือเครือข่าย ลองจินตนาการถึงการเพิ่มสิทธิ์ลงในมิกซ์ โดยกำหนดให้เบราว์เซอร์บล็อกหน้าเว็บในขณะที่ขอสิทธิ์เข้าถึงคลิปบอร์ด ในขณะเดียวกัน สิทธิ์ที่มีเกี่ยวกับ document.execCommand()
สำหรับการโต้ตอบกับคลิปบอร์ดจะได้รับการกำหนดอย่างคร่าวๆ และแตกต่างกันไปในแต่ละเบราว์เซอร์
Async Clipboard 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);
}
}
เขียน()
จริงๆ แล้ว writeText()
เป็นเพียงวิธีอำนวยความสะดวกสําหรับเมธอด write()
ทั่วไป ซึ่งให้คุณคัดลอกรูปภา��ไปยังคลิปบอร์ดได้ด้วย ในลักษณะเดียวกับ writeText()
จะเป็นอะซิงโครนัสและแสดงผล Promise
หากต้องการเขียนรูปภาพไปยังคลิปบอร์ด คุณต้องมีรูปภาพเป็น blob
วิธีหนึ่งในการทำเช่นนี้คือการขอรูปภาพจากเซิร์ฟเวอร์โดยใช้ fetch()
แล้วเรียกใช้ blob()
ในคำตอบ
การขอรูปภาพจากเซิร์ฟเวอร์อาจไม่เป็นที่ต้องการหรือไม่สามารถทำได้ด้วยเหตุผลหลายประการ คุณยังสามารถวาดรูปภาพลงใน Canvas และเรียกใช้เมธอด toBlob()
ของ Canvas ได้ด้วย
ถัดไป ให้ส่งอาร์เรย์ของออบเจ็กต์ ClipboardItem
เป็นพารามิเตอร์ไปยังเมธอด write()
ปัจจุบันคุณสามารถส่งผ่านรูปภาพได้ทีละ 1 ภาพเท่านั้น แต่เราหวังว่าจะเพิ่มการรองรับรูปภาพหลายๆ รูปได้ในอนาคต 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
จะว่างเปล่า
ลองพิจารณาหน้าที่มีข้อความและรูปภาพ และเมื่อผู้ใช้เลือกทั้งหมดและเริ่มคัดลอกคลิปบอร์ด โซลูชันที่กำหนดเองควรทิ้งข้อความและคัดลอกเฉพาะรูปภาพเท่านั้น ซึ่งทำได้ดังที่แสดงในตัวอย่างโค้ด��้������่าง
����่��ที่��ม่ครอบคลุมในตัวอย่างนี้คือวิธีกลับไปใช้ 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);
}
}
อ่าน()
นอกจากนี้เมธอด 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 หลายประเภท
การติดตั้งใช้งานส่วนใหญ่จะวางรูปแบบข้อมูลหลายรูปแบบในคลิปบอร์ดเพื่อดำเนินการตัดหรือคัดลอกเพียงครั้งเดียว ซึ่งมี 2 เหตุผลคือ ในฐานะนักพัฒนาแอป คุณจะไม่ทราบความสามารถของแอปที่ผู้ใช้ต้องการคัดลอกข้อความหรือรูปภาพ และแอปพลิเคชันจำนวนมากรองรับการวางข้อมูลที่มีโครงสร้างเป็นข้อความธรรมดา ซึ่งโดยปกติจะแสดงแก่ผู้ใช้ที่มีรายการในเมนูแก้ไขที่มีชื่อ เช่น วางและจับคู่รูปแบบหรือวางโดยไม่มีการจัดรูปแบบ
ตัวอย่างต่อไปนี้แสดงวิธีการดำเนินการ ตัวอย่างนี้ใช้ 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 /
หรือรูปภาพระเบิดการทำลายไปยังคลิปบอร์ดอย่างเงียบๆ
![ข้อความแจ้งของเบราว์เซอร์จะขอสิทธิ์ใช้คลิปบอร์ดจากผู้ใช้](https://cdn.statically.io/img/web.dev/static/articles/async-clipboard/image/browser-prompt-asking-us-40b4cf43bf01.png?hl=th)
การให้สิทธิ์อ่านคลิปบอร์ดแก่หน้าเว็บโดยอิสระไปที่คลิปบอร์ดนั้นเป็นเรื่องยากกว่าที่เคย ผู้ใช้มักคัดลอกข้อมูลที่ละเอียดอ่อน เช่น รหัสผ่านและรายละเอียดส่วนตัวไปยังคลิปบอร์ด ซึ่งหน้าเว็บอื่นๆ จะอ่านได้โดยที่ผู้ใช้ไม่รู้ตัว
API คลิปบอร์ดรองรับหน้าเว็บที่แสดงผ่าน HTTPS เท่านั้นเช่นเดียวกับ API ใหม่ๆ จำนวนมาก ระบบจะอนุญาตให้เข้าถึงคลิปบอร์ดเมื่อหน้าเว็บเป็นแท็บที่ใช้งานอยู่เท่านั้นเพื่อช่วยป้องกันการละเมิด หน้าเว็บในแท็บที่ใช้งานอยู่สามารถเขียนไปยังคลิปบอร์ดได้โดยไม่ต้องขอสิทธิ์ แต่การอ่านจากคลิปบอร์ดจะต้องใช้สิทธิ์เสมอ
เพิ่มสิทธิ์สำหรับการคัดลอกและวางใน 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 นั้นอิงตามคำสัญญา ข้อมูลนี้จึงโปร่งใสโดยสมบูรณ์ และผู้ใช้ที่ถูกปฏิเสธสิทธิ์ของคลิปบอร์ดจะทำให้สัญญาปฏิเสธที่จะปฏิเสธเพื่อให้หน้าเว็บตอบสนองได้อย่างเหมาะสม
เนื่องจากเบราว์เซอร์อนุญาตให้เข้าถึงคลิปบอร์ดได้เฉพาะเมื่อหน้าเว็บเป็นแท็บที่ใช้งานอยู่ คุณจะพบว่าตัวอย่างบางรายการที่นี่ไม่ทำงานหากวางลงในคอนโซลของเบราว์เซอร์โดยตรง เนื่องจากเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์เองก็เป็นแท็บที่ใช้งานอยู่ มีเคล็ดลับอยู่บ้าง ซึ่งก็คือการเลื่อนสิทธิ์เข้าถึงคลิปบอร์ดโดยใช้ setTimeout()
จากนั้นคลิกภายในหน้าเว็บอย่างรวดเร็วเพื่อโฟกัสก่อนที่จะมีการเรียกใช้ฟังก์ชัน ดังนี้
setTimeout(async () => {
const text = await navigator.clipboard.readText();
console.log(text);
}, 2000);
การผสานรวมนโยบายสิทธิ์
หากต้องการใช้ API ใน iframe คุณต้องเปิดใช้ API โดยใช้นโยบายสิทธิ์ ซึ่งจะกำหนดกลไกที่อนุญาตให้เลือกเปิดใช้และปิดใช้ฟีเจอร์และ API ต่างๆ ของเบราว์เซอร์ได้ โดยเฉพาะอย่างยิ่ง คุณต้องผ่าน clipboard-read
หรือ clipboard-write
อย่างใดอย่างหนึ่งหรือทั้ง 2 อย่าง ทั้งนี้ขึ้นอยู่กับความต้องการของแอป
<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 คุณสามารถรีมิกซ์การสาธิตข้อความหรือการสาธิตรูปภาพเพื่อทำการทดสอบได้
ตัวอย่างที่ 1 แสดงให้เห็นการย้ายข้อความและการออกจากคลิปบอร์ด
ใช้การสาธิตนี้เพื่อลองใช้ API กับรูปภาพ อย่าลืมว่ามีเพียง PNG เท่านั้นที่รองรับ และใช้ได้ในไม่กี่เบราว์เซอร์เท่านั้น
ลิงก์ที่เกี่ยวข้อง
ข้อความแสดงการยอมรับ
Darwin Huang และ Gary Kačmarčík นำ Asynchronous Clipboard API มาใช้งาน ดาร์วินยังจัดให้มีการสาธิตดังกล่าวด้วย ขอขอบคุณ Kyarik และ Gary Kačmarčík อีกครั้งสำหรับการตรวจสอบส่วนต่างๆ ของบทความน��้
รูปภาพหลักของ markus Winkler ใน Unsplash