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

Mustaq Ahmed
โจ เมดเลย์
โจ้ เมดเลย์

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

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

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

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

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

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

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

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

หากไม่เปิดใช้งานผู้ใช้ 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);
});

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

ซึ่งใช้ได้กับการเปิดใช้งานผู้ใช้เวอร์ชัน 2 ทั้งในรูปแบบเดิมและในเชน