Giới thiệu về VisualViewport

Jake Archibald
Jake Archibald

Nếu tôi nói với bạn rằng có nhiều khung nhìn thì sao?

BRRRRAAAAAAAMMMMMMMMMM

Và khung nhìn mà bạn đang sử dụng thực sự là một khung nhìn trong một khung nhìn.

BRRRRAAAAAAAMMMMMMMMMM

Đôi khi, dữ liệu mà DOM cung cấp cho bạn đề cập đến một trong các khung nhìn đó và không đề cập đến khung nhìn còn lại.

BRRRRAAAAM… chờ đã, gì cơ?

Đúng vậy, hãy xem:

Khung nhìn bố cục so với khung nhìn hình ảnh

Video ở trên cho thấy một trang web đang được cuộn và thu phóng bằng cách chụm hai ngón tay, cùng với một bản đồ thu nhỏ ở bên phải cho thấy vị trí của khung nhìn trong trang.

Mọi thứ khá đơn giản trong quá trình cuộn thông thường. Khu vực màu xanh lục đại diện cho khung nhìn bố cục mà các mục position: fixed tuân theo.

Mọi thứ sẽ trở nên lạ khi bạn sử dụng tính năng chụm để thu phóng. Hộp màu đỏ đại diện cho khung nhìn hình ảnh, là phần của trang mà chúng ta thực sự có thể nhìn thấy. Khung nhìn này có thể di chuyển trong khi các phần tử position: fixed vẫn ở nguyên vị trí, được đính kèm vào khung nhìn bố cục. Nếu chúng ta kéo ở ranh giới của khung nhìn bố cục, thì thao tác này sẽ kéo theo khung nhìn bố cục.

Cải thiện khả năng tương thích

Rất tiếc, các API web không nhất quán về khung nhìn mà chúng tham chiếu đến, đồng thời cũng không nhất quán trên các trình duyệt.

Ví dụ: element.getBoundingClientRect().y trả về độ dời trong khung nhìn bố cục. Điều đó thật tuyệt, nhưng chúng ta thường muốn vị trí trong trang, vì vậy, chúng ta viết:

element.getBoundingClientRect().y + window.scrollY

Tuy nhiên, nhiều trình duyệt sử dụng khung nhìn trực quan cho window.scrollY, nghĩa là mã trên sẽ bị ngắt khi người dùng chụm để thu phóng.

Chrome 61 thay đổi window.scrollY để tham chiếu đến khung nhìn bố cục, nghĩa là mã ở trên hoạt động ngay cả khi thu phóng bằng cách chụm. Trên thực tế, các trình duyệt đang dần thay đổi tất cả các thuộc tính vị trí để tham chiếu đến khung nhìn bố cục.

Ngoại trừ một thuộc tính mới…

Hiển thị khung nhìn hình ảnh cho tập lệnh

Một API mới hiển thị khung nhìn hình ảnh dưới dạng window.visualViewport. Đây là thông số kỹ thuật nháp, đã được chấp thuận trên nhiều trình duyệt và sẽ ra mắt trong Chrome 61.

console.log(window.visualViewport.width);

window.visualViewport cung cấp cho chúng ta những thông tin sau:

visualViewport cơ sở lưu trú
offsetLeft Khoảng cách giữa cạnh trái của khung nhìn hình ảnh và khung nhìn bố cục, tính bằng pixel CSS.
offsetTop Khoảng cách giữa cạnh trên của khung nhìn trực quan và khung nhìn bố cục, tính bằng pixel CSS.
pageLeft Khoảng cách giữa cạnh trái của khung nhìn hình ảnh và ranh giới bên trái của tài liệu, tính bằng pixel CSS.
pageTop Khoảng cách giữa cạnh trên của khung nhìn hình ảnh và ranh giới trên cùng của tài liệu, tính bằng pixel CSS.
width Chiều rộng của khung nhìn trực quan tính bằng pixel CSS.
height Chiều cao của khung nhìn trực quan tính bằng pixel CSS.
scale Tỷ lệ được áp dụng bằng cách chụm để thu phóng. Nếu nội dung có kích thước gấp đôi do tính năng phóng to, thì hàm này sẽ trả về 2. Điều này không bị ảnh hưởng bởi devicePixelRatio.

Ngoài ra, còn có một vài sự kiện:

window.visualViewport.addEventListener('resize', listener);
visualViewport sự kiện
resize Được kích hoạt khi width, height hoặc scale thay đổi.
scroll Được kích hoạt khi offsetLeft hoặc offsetTop thay đổi.

Bản minh hoạ

Video ở đầu bài viết này được tạo bằng visualViewport, hãy xem video đó trong Chrome 61 trở lên. Video này sử dụng visualViewport để gắn bản đồ thu nhỏ vào phía trên cùng bên phải của khung nhìn hình ảnh và áp dụng tỷ lệ nghịch để bản đồ luôn xuất hiện ở cùng kích thước, mặc dù bạn có dùng thao tác chụm để thu phóng.

Các lỗi thường gặp

Sự kiện chỉ kích hoạt khi khung nhìn hình ảnh thay đổi

Có vẻ như đây là điều hiển nhiên, nhưng tôi đã bị nhầm lẫn khi lần đầu sử dụng visualViewport.

Nếu khung nhìn bố cục đổi kích thước nhưng khung nhìn hình ảnh không đổi kích thước, thì bạn sẽ không nhận được sự kiện resize. Tuy nhiên, việc khung nhìn bố cục đổi kích thước mà khung nhìn hình ảnh không thay đổi chiều rộng/chiều cao là điều bất thường.

Điểm khó khăn thực sự là cuộn. Nếu thao tác cuộn xảy ra nhưng khung nhìn hình ảnh vẫn tĩnh so với khung nhìn bố cục, thì bạn sẽ không nhận được sự kiện scroll trên visualViewport. Đây là trường hợp rất phổ biến. Trong quá trình cuộn tài liệu thông thường, khung nhìn hình ảnh vẫn bị khoá ở phía trên cùng bên trái của khung nhìn bố cục, vì vậy scroll không kích hoạt trên visualViewport.

Nếu muốn biết tất cả thay đổi đối với khung nhìn hình ảnh, bao gồm cả pageToppageLeft, bạn cũng phải theo dõi sự kiện cuộn của cửa sổ:

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

Tránh trùng lặp công việc với nhiều trình nghe

Tương tự như việc nghe scrollresize trên cửa sổ, bạn có thể gọi một số loại hàm "cập nhật". Tuy nhiên, thường thì nhiều sự kiện trong số này sẽ xảy ra cùng một lúc. Nếu người dùng đổi kích thước cửa sổ, thao tác này sẽ kích hoạt resize, nhưng cũng thường kích hoạt scroll. Để cải thiện hiệu suất, hãy tránh xử lý thay đổi nhiều lần:

// 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
    });
}

Tôi đã gửi một vấn đề về thông số kỹ thuật cho vấn đề này, vì tôi nghĩ có thể có một cách tốt hơn, chẳng hạn như một sự kiện update duy nhất.

Trình xử lý sự kiện không hoạt động

Do lỗi Chrome, cách này không hoạt động:

Không nên

Bị lỗi – sử dụng trình xử lý sự kiện

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

Thay vào đó:

Nên

Hoạt động – sử dụng trình nghe sự kiện

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

Giá trị chênh lệch được làm tròn

Tôi nghĩ (và hy vọng) đây là một lỗi khác của Chrome.

offsetLeftoffsetTop được làm tròn, khá không chính xác sau khi người dùng phóng to. Bạn có thể thấy các vấn đề liên quan đến vấn đề này trong bản minh hoạ – nếu người dùng phóng to và kéo chậm, bản đồ thu nhỏ sẽ chụp nhanh giữa các pixel chưa phóng to.

Tốc độ sự kiện chậm

Giống như các sự kiện resizescroll khác, các sự kiện này không kích hoạt mọi khung hình, đặc biệt là trên thiết bị di động. Bạn có thể thấy điều này trong bản minh hoạ – sau khi bạn chụm để thu phóng, bản đồ thu nhỏ sẽ gặp sự cố khi cố gắng khoá vào khung nhìn.

Hỗ trợ tiếp cận

Trong bản minh hoạ, tôi đã sử dụng visualViewport để chống lại thao tác chụm để thu phóng của người dùng. Điều này có ý nghĩa đối với bản minh hoạ cụ thể này, nhưng bạn nên suy nghĩ kỹ trước khi làm bất cứ điều gì ghi đè ý muốn phóng to của người dùng.

Bạn có thể dùng visualViewport để cải thiện khả năng hỗ trợ tiếp cận. Ví dụ: nếu người dùng đang phóng to, bạn có thể chọn ẩn các mục position: fixed trang trí để không làm người dùng khó chịu. Nhưng xin nhắc lại, hãy cẩn thận để không che giấu nội dung mà người dùng đang cố gắng xem xét kỹ hơn.

Bạn có thể cân nhắc đăng lên một dịch vụ phân tích khi người dùng phóng to. Điều này có thể giúp bạn xác định những trang mà người dùng gặp khó khăn ở cấp thu phóng mặc định.

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

Chỉ vậy thôi! visualViewport là một API nhỏ gọn và hữu ích giúp giải quyết các vấn đề về khả năng tương thích.