กำลังเลิกบล็อกการเข้าถึงคลิปบอร์ด

การเข้าถึงคลิปบอร์ดที่ปลอดภัยยิ่งขึ้นสำหรับข้อความและรูปภาพ

Jason Miller
Jason Miller
Thomas Steiner
Thomas Steiner

วิธีดั้งเดิมในการเข้าถึงคลิปบอร์ดของระบบคือผ่าน 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);
  }
}

การสนับสนุนเบราว์เซอร์

  • 66
  • 79
  • 63
  • 13.1

แหล่งที่มา

เขียน()

จริงๆ แล้ว 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);
}

การสนับสนุนเบราว์เซอร์

  • 66
  • 79
  • 13.1

แหล่งที่มา

เหตุการณ์การคัดลอก

ในกรณีที่ผู้ใช้เริ่มคัดลอกคลิปบอร์ดและไม่เรียกใช้ 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:

การสนับสนุนเบราว์เซอร์

  • 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

แหล่งที่มา

อ่าน()

นอกจากนี้เมธอด 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+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());
});

การสนับสนุนเบราว์เซอร์

  • 3
  • 12
  • 3.6
  • 4

แหล่งที่มา

เหตุการณ์การวาง

ตามที่ได้แจ้งไว้ก่อนหน้านี้ เรามีแผนที่จะเปิดตัวกิจกรรมให้ใช้ร่วมกับ Clipboard 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 หลายประเภท

การติดตั้งใช้งานส่วนใหญ่จะวางรูปแบบข้อมูลหลายรูปแบบในคลิปบอร์ดเพื่อดำเนินการตัดหรือคัดลอกเพียงครั้งเดียว ซึ่งมี 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 / หรือรูปภาพระเบิดการทำลายไปยังคลิปบอร์ดอย่างเงียบๆ

ข้อความแจ้งของเบราว์เซอร์จะขอสิทธิ์ใช้คลิปบอร์ดจากผู้ใช้
ข้อความแจ้งสิทธิ์สำหรับ Clipboard API

การให้สิทธิ์อ่านคลิปบอร์ดแก่หน้าเว็บโดยอิสระไปที่คลิปบอร์ดนั้นเป็นเรื่องยากกว่าที่เคย ผู้ใช้มักคัดลอกข้อมูลที่ละเอียดอ่อน เช่น รหัสผ่านและรายละเอียดส่วนตัวไปยังคลิปบอร์ด ซึ่งหน้าเว็บอื่นๆ จะอ่านได้โดยที่ผู้ใช้ไม่รู้ตัว

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