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

Mustaq Ahmed
Joe Medley
Joe Medley

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

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

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

User Activation v2 ทํางานอย่างไร

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

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

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

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

ตัวอย่างความสอดคล้องใน API ที่เปิดใช้งาน

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

การโทร setTimeout() ครั้งแบบต่อเนื่อง

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

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

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

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

หากไม่มีการเปิดใช้งานผู้ใช้ 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 แม้แต่ข้อความแรกก็จะไม่ผ่านการตรวจสอบหาก "เชื่อมโยง" กับเฟรมอื่นที่มาจากแหล่งที่มาอื่น (กล่าวคือ หากผู้รับรายแรกส่งต่อข้อความให้กับอีกรายหนึ่ง)

การดำเนินการนี้ใช้ได้กับการเปิดใช้งานผู้ใช้ v2 ทั้งในรูปแบบเดิมและแบบเชน