การทำให้การเปิดใช้งานของผู้ใช้สอดคล้องกันในทุก API

Mustaq Ahmed
Joe Medley
Joe Medley

เพื่อป้องกันไม่ให้สคริปต์ที่เป็นอันตรายละเมิด API ที่มีความละเอียดอ่อน เช่น ป๊อปอัป โหมดเต็มหน้าจอ ฯลฯ เบราว์เซอร์จะควบคุมการเข้าถึง API เหล่านั้นผ่านการเปิดใช้งานผู้ใช้ การเปิดใช้งานของผู้ใช้คือสถานะของเซสชันการท่องเว็บที่เกี่ยวข้องกับการดำเนินการของผู้ใช้ โดยสถานะ "ใช้งานอยู่" โดยทั่วไปจะบ่งบอกว่าผู้ใช้กำลังโต้ตอบกับหน้าเว็บ หรือมีการโต้ตอบตั้งแต่มีการโหลดหน้าเว็บ ท่าทางสัมผัสของผู้ใช้เป็นคำยอดนิยมแต่ทำให้เข้าใจผิดในแนวคิดเดียวกัน ตัวอย่างเช่น ท่าทางสัมผัสการปัดหรือสะบัดโดยผู้ใช้ไม่ได้เป็นการเปิดใช้งานหน้าเว็บ ดังนั้น จากมุมมองของสคริปต์จึงไม่ใช่การเปิดใช้งานผู้ใช้

เบราว์เซอร์หลักๆ ในปัจจุบันแสดงพฤติกรรมที่แตกต่างกันไปอย่างกว้างขวางเกี่ยวกับวิธีที่การเปิดใช้งานของผู้ใช้ควบคุม API ที่มีการกำหนดเปิดใช้งาน ใน Chrome การใช้งานนี้อิงตามโมเดลที่ใช้โทเค็นซึ่งมีความซับซ้อนเกินกว่าจะกำหนดลักษณะการทำงานที่สอดคล้องกันใน API ทั้งหมดที่กำหนดให้มีการเข้าถึง ตัวอย่างเช่น Chrome อนุญาตให้เข้าถึง API ที่มีการกำหนดการเปิดใช้งานได้ไม่สมบูรณ์ผ่าน postMessage() และการเรียก setTimeout() และการเปิดใช้งานของผู้ใช้ไม่รองรับ Promises, XHR, การโต้ตอบกับเกมแพด ฯลฯ โปรดทราบว่าข้อมูลบางส่วนต่อไปนี้เป็นข้อบกพร่องที่เป็นที่นิยมแต่มีมาอย่างยาวนาน

ในเวอร์ชัน 72 นั้น Chrome จะจัดส่ง User Activation v2 ซึ่งจะทำให้ความพร้อมใช้งานของการเปิดใช้งานสำหรับผู้ใช้เสร็จสมบูรณ์สำหรับ API ที่มีการกำหนดเปิดใช้งานทั้งหมด วิธีนี้จะแก้ไขความไม่สอดคล้องข้างต้น (และอื่นๆ อีกเล็กน้อย เช่น MessageChannels) ซึ่งเราเชื่อว่าจะช่วยอำนวยความสะดวกในการพัฒนาเว็บเกี่ยวกับการเปิดใช้งานของผู้ใช้ นอกจากนี้ การใช้งานแบบใหม่ยังมีการใช้งานข้อมูลอ้างอิงสำหรับข้อกำหนดใหม่ที่เสนอซึ่งมุ่งที่จะรวมเบราว์เซอร์ทั้งหมดไว้ด้วยกันในระยะยาว

การเปิดใช้งานผู้ใช้ v2 ทำงานอย่างไร

โดย API ใหม่นี้จะรักษาสถานะการเปิดใช้งานของผู้ใช้แบบ 2 บิตที่ออบเจ็กต์ window ทุกๆ ลำดับชั้นของเฟรม ได้แก่ บิตติดหนึบสำหรับสถานะการเปิดใช้งานของผู้ใช้ในอดีต (หากเฟรมเคยเห็นการเปิดใช้งานของผู้ใช้) และบิตชั่วคราวสำหรับสถานะปัจจุบัน (หากเฟรมเห็นว่าผู้ใช้เปิดใช้งานภายใน 1 วินาที) หมุดติดหนึบจะไม่รีเซ็ตระหว่างอายุการใช้งานของเฟรมหลังจากตั้งค่าแล้ว ระบบจะตั้งค่าบิตชั่วคราวสำหรับทุกการโต้ตอบของผู้ใช้ และจะรีเซ็ตหลังจากช่วงเวลาหมดอายุ (ประมาณ 1 วินาที) หรือผ่านการเรียกใช้ API ที่ใช้การเปิดใช้งาน (เช่น window.open())

โปรดทราบว่า API ที่มีการกำหนดเปิดใช้งานที่แตกต่างกันอาศัยการเปิดใช้งานของผู้ใช้ในวิธีที่แตกต่างกัน โดย API ใหม่จะไม่เปลี่ยนแปลงลักษณะการทํางานเฉพาะ API เหล่านี้เลย เช่น ระบบอนุญาตให้มีป๊อปอัปเพียง 1 รายการต่อการเปิดใช้งานของผู้ใช้ เนื่องจาก window.open() ต้องเปิดใช้งานผู้ใช้ตามที่เคยเป็นมา Navigator.prototype.vibrate() จะยังคงมีผลบังคับใช้หากเฟรม (หรือเฟรมย่อยใดๆ ของเฟรมนั้น) เคยเห็นการดำเนินการของผู้ใช้ และอื่นๆ

สิ่งที่เปลี่ยนแปลงไป

  • การเปิดใช้งานผู้ใช้เวอร์ชัน 2 ทำให้แนวคิดของระดับการเข้าถึงการเปิดใช้งานของผู้ใช้เป็นทางการมากขึ้นภายในขอบเขตของเฟรม กล่าวคือ การโต้ตอบของผู้ใช้กับเฟรมหนึ่งๆ จะเปิดใช้งานทุกเฟรมที่มีเฟรมนั้น (และเฉพาะเฟรมเหล่านั้น) โดยไม่คำนึงถึงต้นทาง (ใน Chrome 72 เรามีวิธีแก้ปัญหาชั่วคราวเพื่อขยายการแสดงผลไปยังเฟรมต้นทางเดียวกันทั้งหมด เราจะนำวิธีหลีกเลี่ยงปัญหานี้ออกเมื่อมีวิธีส่งการเปิดใช้งานผู้ใช้ไปยังเฟรมย่อยอย่างชัดแจ้ง)
  • เมื่อมีการเรียก API ที่มีการระบุถึงการเปิดใช้งานจากเฟรมที่เปิดใช้งาน แต่จากภายนอกโค้ดเครื่องจัดการเหตุการณ์จะทำงานตราบเท่าที่สถานะการเปิดใช้งานของผู้ใช้เป็น "ใช้งานอยู่" (เช่น ยังไม่หมดอายุหรือไม่มีการใช้งาน) ก่อนการเปิดใช้งานผู้ใช้เวอร์ชัน 2 การทำงานจะล้มเหลวโดยไม่มีเงื่อนไข
  • การโต้ตอบของผู้ใช้ที่ไม่ได้ใช้งานหลายครั้งภายในช่วงเวลาหมดอายุจะรวมกันเป็นการเปิดใช้งานครั้งเดียวที่สัมพันธ์กับการโต้ตอบสุดท้าย

ตัวอย่างของความสม่ำเสมอของ API ที่มีการกำหนดเปิดใช้งาน

ต่อไปนี้เป็น 2 ตัวอย่างที่มีหน้าต่างป๊อปอัป (เปิดโดยใช้ window.open()) ซึ่งแสดงวิธีที่การเปิดใช้งานผู้ใช้เวอร์ชัน 2 ทำให้ลักษณะการทำงานของ API ที่มีการเปิดใช้เป้าหมายสอดคล้องกัน

การโทร setTimeout() สายที่มีโซ่

ตัวอย่างนี้มาจากการสาธิต setTimeout() ของเรา หากเครื่องจัดการ click พยายามเปิดป๊อปอัปภายใน 1 วินาที ก็คาดว่าจะสำเร็จไม่ว่าโค้ดจะ "เขียน" ความล่าช้าอย่างไร การเปิดใช้งานผู้ใช้เวอร์ชัน 2 เป็นไปตามความคาดหวังนี้ ดังนั้นตัวแฮนเดิลเหตุการณ์ต่อไปนี้จึงเปิดป๊อปอัปใน click (โดยมีความล่าช้า 100 มิลลิวินาที)

function popupAfter100ms() {
  setTimeout(callWindowOpen, 100);
}

function asyncPopupAfter100ms() {
  setTimeout(popupAfter100ms, 0);
}

someButton.addEventListener('click', popupAfter100ms);
someButton.addEventListener('click', asyncPopupAfter100ms);

หากไม่มี User Activate v2 เครื่องจัดการเหตุการณ์ที่ 2 จะล้มเหลวในทุกเบราว์เซอร์ที่เราทดสอบ (หรือแม้แต่ข้อแรกก็ล้มเหลว ในบางกรณี)

การเรียก postMessage() ข้ามโดเมน

นี่คือตัวอย่างจากการสาธิต postMessage() ของเรา สมมติว่าตัวแฮนเดิล click ในเฟรมย่อยแบบข้ามต้นทางส่งข้อความ 2 ข้อความไปยังเฟรมหลักโดยตรง เฟรมระดับบนสุดควรเปิดป๊อปอัปได้เมื่อได้รับข้อความใดข้อความหนึ่งต่อไปนี้ (แต่ไม่ใช่ทั้ง 2 ข้อความ)

// Parent frame code
window.addEventListener('message', e => {
  if (e.data === 'open_popup' && e.origin === child_origin)
    window.open('about:blank');
});

// Child frame code:
someButton.addEventListener('click', () => {
  parent.postMessage('hi_there', parent_origin);
  parent.postMessage('open_popup', parent_origin);
});

หากไม่มีการเปิดใช้งานผู้ใช้ v2 เฟรมระดับบนสุดจะไม่สามารถเปิดป๊อปอัปเมื่อได้รับข้อความที่ 2 แม้แต่ข้อความแรกจะล้มเหลวหากมีการ "เชื่อมโยง" กับเฟรมแบบข้ามต้นทางอื่น (กล่าวคือ หากผู้รับรายแรกส่งต่อข้อความไปยังเฟรมอื่น)

ซึ่งใช้งานได้กับ User Activation v2 ทั้งในรูปแบบเดิมและที่ใช้เชน