ขอแนะนำ VisualViewport

จะเกิดอะไรขึ้นหากเราบอกว่ามีวิวพอร์ตมากกว่า 1 รายการ

BRRRRAAAAAAAMMMMMMMMMM

และวิวพอร์ตที่คุณใช้อยู่ตอนนี้คือวิวพอร์ตภายในวิวพอร์ต

BRRRRAAAAAAAMMMMMMMMMM

และบางครั้งข้อมูลที่ DOM แสดงให้คุณเห็นจะอ้างอิงถึงวิวพอร์ตใดวิวพอร์ตหนึ่งเท่านั้น

BRRRRAAAAM… เดี๋ยวก่อน อะไรนะ

เป็นเรื่องจริง โปรดดูตัวอย่างต่อไปนี้

วิวพอร์ตเลย์เอาต์กับวิวพอร์ตภาพ

วิดีโอด้านบนแสดงการเลื่อนหน้าเว็บและการซูมเข้า/ออกด้วยสองนิ้ว รวมถึงแผนที่ขนาดเล็กทางด้านขวาที่แสดงตำแหน่งของวิวพอร์ตภายในหน้า

ทุกอย่างค่อนข้างตรงไปตรงมาในระหว่างการเลื่อนปกติ พื้นที่สีเขียวแสดงวิวพอร์ตเลย์เอาต์ ซึ่งรายการ position: fixed ยึดตาม

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

การปรับปรุงความเข้ากันได้

ขออภัย Web API จะใช้วิวพอร์ตใดก็ตามที่อ้างอิง และจะไม่สอดคล้องกันในแต่ละเบราว์เซอร์

เช่น element.getBoundingClientRect().y จะแสดงผลออฟเซตภายในวิวพอร์ตเลย์เอาต์ เยี่ยม แต่บ่อยครั้งที่เราต้องการตำแหน่งภายในหน้าเว็บ เราจึงเขียนดังนี้

element.getBoundingClientRect().y + window.scrollY

อย่างไรก็ตาม เบราว์เซอร์จํานวนมากใช้วิวพอร์ตภาพสําหรับ window.scrollY ซึ่งหมายความว่าโค้ดข้างต้นจะใช้งานไม่ได้เมื่อผู้ใช้ใช้การซูมด้วยสองนิ้ว

Chrome 61 เปลี่ยน window.scrollY เพื่ออ้างอิงวิวพอร์ตเลย์เอาต์แทน ซึ่งหมายความว่าโค้ดข้างต้นจะใช้งานได้แม้ขณะที่ซูมด้วยสองนิ้ว อันที่จริงแล้ว เบราว์เซอร์กําลังค่อยๆ เปลี่ยนพร็อพเพอร์ตี้ตําแหน่งทั้งหมดให้อ้างอิงถึงวิวพอร์ตของเลย์เอาต์

ยกเว้นพร็อพเพอร์ตี้ใหม่ 1 แห่ง…

การแสดงวิวพอร์ตภาพต่อสคริปต์

API ใหม่จะแสดงวิวพอร์ตภาพเป็น window.visualViewport นี่เป็นข้อกำหนดฉบับร่างที่มีการอนุมัติข้ามเบราว์เซอร์ และกำลังจะเปิดตัวใน Chrome 61

console.log(window.visualViewport.width);

window.visualViewport แสดงข้อมูลต่อไปนี้

ที่พัก visualViewport แห่ง
offsetLeft ระยะห่างระหว่างขอบด้านซ้ายของวิวพอร์ตภาพกับวิวพอร์ตเลย์เอาต์เป็นพิกเซล CSS
offsetTop ระยะห่างระหว่างขอบบนของวิวพอร์ตภาพกับวิวพอร์ตเลย์เอาต์เป็นพิกเซล CSS
pageLeft ระยะห่างระหว่างขอบด้านซ้ายของวิวพอร์ตภาพกับขอบด้านซ้ายของเอกสารเป็นพิกเซล CSS
pageTop ระยะห่างระหว่างขอบบนของวิวพอร์ตภาพกับขอบบนของเอกสารเป็นพิกเซล CSS
width ความกว้างของวิวพอร์ตภาพในพิกเซล CSS
height ความสูงของวิวพอร์ตภาพในพิกเซล CSS
scale มาตราส่วนที่เกิดจากการบีบนิ้วเพื่อซูม หากเนื้อหามีขนาดใหญ่ขึ้น 2 เท่าเนื่องจากการซูม ผลลัพธ์ที่ได้จะเป็น 2 การดำเนินการนี้ไม่ได้รับผลกระทบจาก devicePixelRatio

นอกจากนี้ยังมีกิจกรรมอีก 2 รายการ ได้แก่

window.visualViewport.addEventListener('resize', listener);
visualViewport เหตุการณ์
resize เริ่มทํางานเมื่อ width, height หรือ scale มีการเปลี่ยนแปลง
scroll เริ่มทํางานเมื่อ offsetLeft หรือ offsetTop มีการเปลี่ยนแปลง

สาธิต

วิดีโอตอนต้นของบทความนี้สร้างขึ้นโดยใช้ visualViewportลองดูใน Chrome 61 ขึ้นไป โดยจะใช้ visualViewport เพื่อทำให้แผนที่ขนาดเล็กติดอยู่ที่ด้านขวาบนของวิดเจ็ตภาพ และจะใช้การแปลงสเกลแบบกลับกันเพื่อให้แผนที่ปรากฏในขนาดเดิมเสมอ แม้จะซูมด้วยสองนิ้ว

ข้อควรระวัง

เหตุการณ์จะทริกเกอร์ก็ต่อเมื่อวิวพอร์ตภาพมีการเปลี่ยนแปลงเท่านั้น

ดูเหมือนว่าจะเป็นเรื่องชัดเจนที่ต้องระบุ แต่ฉันก็เจอปัญหานี้เมื่อเล่นกับ visualViewport เป็นครั้งแรก

หากวิดเจ็ตในหน้าเว็บปรับขนาด แต่วิดเจ็ตภาพไม่ปรับขนาด คุณจะไม่ได้รับการเรียกเหตุการณ์ resize อย่างไรก็ตาม การที่วิวพอร์ตของเลย์เอาต์ปรับขนาดโดยไม่ทำให้วิวพอร์ตภาพมีการเปลี่ยนแปลงความกว้าง/ความสูงด้วยนั้นเป็นเรื่องผิดปกติ

สิ่งที่น่ารำคาญจริงๆ คือการสไลด์ หากมีการเลื่อน แต่วิวพอร์ตภาพยังคงนิ่งเมื่อเทียบกับวิวพอร์ตเลย์เอาต์ คุณจะไม่ได้รับการจัดกิจกรรม scroll ใน visualViewport ซึ่งกรณีนี้พบได้บ่อยมาก ในระหว่างการเลื่อนเอกสารตามปกติ วิวพอร์ตภาพจะยังคงล็อกอยู่ที่ด้านซ้ายบนของวิวพอร์ตเลย์เอาต์ ดังนั้น scroll จะไม่ทํางานใน visualViewport

หากต้องการทราบการเปลี่ยนแปลงทั้งหมดในวิวพอร์ตภาพ ซึ่งรวมถึง pageTop และ pageLeft คุณจะต้องฟังเหตุการณ์การเลื่อนของหน้าต่างด้วย

visualViewport.addEventListener('scroll', update);
visualViewport.addEventListener('resize', update);
window.addEventListener('scroll', update);

หลีกเลี่ยงการทํางานซ้ำกับผู้ฟังหลายราย

เช่นเดียวกับการฟัง scroll และ resize ในหน้าต่าง คุณมีแนวโน้มที่จะเรียกใช้ฟังก์ชัน "อัปเดต" บางประเภท อย่างไรก็ตาม เป็นเรื่องปกติที่เหตุการณ์เหล่านี้หลายรายการจะเกิดขึ้นพร้อมกัน หากผู้ใช้ปรับขนาดหน้าต่าง ระบบจะทริกเกอร์ resize แต่ทริกเกอร์ scroll บ่อยครั้งด้วย หลีกเลี่ยงการจัดการการเปลี่ยนแปลงหลายครั้งเพื่อปรับปรุงประสิทธิภาพ

// Add listeners
visualViewport.addEventListener('scroll', update);
visualViewport.addEventListener('resize', update);
addEventListener('scroll', update);

let pendingUpdate = false;

function update() {
    // If we're already going to handle an update, return
    if (pendingUpdate) return;

    pendingUpdate = true;

    // Use requestAnimationFrame so the update happens before next render
    requestAnimationFrame(() => {
    pendingUpdate = false;

    // Handle update here
    });
}

เราได้ยื่นเรื่องปัญหาเกี่ยวกับข้อกำหนดสำหรับเรื่องนี้แล้ว เนื่องจากเราคิดว่าอาจมีวิธีอื่นที่ดีกว่า เช่น เหตุการณ์ update รายการเดียว

ตัวแฮนเดิลเหตุการณ์ไม่ทํางาน

เนื่องจากข้อบกพร่องของ Chrome การดำเนินการนี้จึงไม่ทำงาน

ไม่ควรทำ

มีข้อบกพร่อง – ใช้เครื่องจัดการเหตุการณ์

visualViewport.onscroll = () => console.log('scroll!');

ให้ดำเนินการต่อไปนี้แทน

ควรทำ

ใช้งานได้ – ใช้ Listener เหตุการณ์

visualViewport.addEventListener('scroll', () => console.log('scroll'));

ค่าออฟเซ็ตมีการปัดเศษ

เราคิดว่า (หวังว่า) นี่จะเป็นข้อบกพร่องอีกอย่างหนึ่งของ Chrome

offsetLeft และ offsetTop จะปัดเศษ ซึ่งจะค่อนข้างไม่ถูกต้องเมื่อผู้ใช้ซูมเข้า คุณจะเห็นปัญหานี้ในการสาธิต หากผู้ใช้ซูมเข้าและเลื่อนไปช้าๆ แผนที่ขนาดเล็กจะกระโดดไปมาระหว่างพิกเซลที่ไม่ได้ซูม

อัตราเหตุการณ์ช้า

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

การช่วยเหลือพิเศษ

ในการสาธิต เราใช้ visualViewport เพื่อโต้ตอบกับการซูมเข้า/ออกด้วยสองนิ้วของผู้ใช้ การดำเนินการนี้เหมาะสมกับการแสดงตัวอย่างนี้ แต่คุณควรพิจารณาอย่างรอบคอบก่อนที่จะทำสิ่งใดก็ตามที่ลบล้างความต้องการของผู้ใช้ในการซูมเข้า

visualViewport สามารถใช้เพื่อปรับปรุงการช่วยเหลือพิเศษได้ เช่น หากผู้ใช้กำลังซูมเข้า คุณอาจเลือกซ่อนรายการposition: fixedตกแต่งเพื่อไม่ให้รบกวนผู้ใช้ แต่อย่าลืมตรวจสอบว่าคุณไม่ได้ซ่อนสิ่งที่ผู้ใช้พยายามดูให้ใกล้ขึ้น

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

visualViewport.addEventListener('resize', () => {
    if (visualViewport.scale > 1) {
    // Post data to analytics service
    }
});

เพียงเท่านี้ก็เรียบร้อยแล้ว visualViewport เป็น API เล็กๆ ที่ยอดเยี่ยมซึ่งช่วยแก้ปัญหาความเข้ากันได้ได้