ขอแนะนำ VisualViewport

เจค อาร์ชิบาลด์
เจค อาร์ชิบาลด์

ถ้าฉันบอกนะ มีวิวพอร์ตมากกว่า 1 วิวพอร์ต

BRRRRAAAAAAAMMMMMMMM

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

BRRRRAAAAAAAMMMMMMMM

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

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