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 ra là một khung nhìn trong một khung nhìn.

BRRRRAAAAAAAMMMMMMMMMM

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

BRRRRAAAAM... chờ gì nữa?

Thật đấy, hãy xem thử:

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, cùng với một bản đồ thu nhỏ ở bên phải cho thấy vị trí của các khung nhìn trong trang.

Mọi thứ diễn ra khá thẳng trong khi cuộn thông thường. Vùng màu xanh lục thể hiện khung nhìn bố cục nơi các mục position: fixed gắn vào.

Mọi thứ trở nên kỳ lạ khi tính năng chụm-thu phóng được ra mắt. Hộp màu đỏ đại diện cho khung nhìn trực quan, là một 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 xung quanh trong khi các phần tử position: fixed vẫn giữ nguyên vị trí, được gắn vào khung nhìn bố cục. Nếu chúng ta xoay sang ranh giới của khung nhìn bố cục, thì khung nhìn bố cục sẽ kéo theo.

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

Rất tiếc, API web không nhất quán về khung nhìn mà chúng đề cập đến và cũng không nhất quán trên các trình duyệt.

Ví dụ: element.getBoundingClientRect().y trả về độ lệch trong khung nhìn bố cục. Điều đó thật thú vị, nhưng chúng tôi thường muốn có vị trí trong trang, vì vậy chúng tôi 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à các mã ở trên sẽ bị hỏng 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 sẽ hoạt động ngay cả khi dùng tính năng chụm thu phóng. Trên thực tế, các trình duyệt đang thay đổi dần 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 tài sản mới...

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

API mới hiển thị khung nhìn hình ảnh dưới dạng window.visualViewport. Đây là một thông số kỹ thuật nháp với tính năng phê duyệt trên nhiều trình duyệt và sẽ xuất hiện trên Chrome 61.

console.log(window.visualViewport.width);

Dưới đây là những gì window.visualViewport cung cấp cho chúng ta:

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ùng của khung nhìn hình ảnh 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 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ùng 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 hình ảnh tính bằng pixel CSS.
height Chiều cao của khung nhìn hình ảnh 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 thu phóng, thao tác này sẽ trả về 2. Điều này không chịu ảnh hưởng của devicePixelRatio.

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

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 này trong Chrome 61 trở lên. Video này sử dụng visualViewport để làm cho bản đồ thu nhỏ nằm ở trên cùng bên phải của khung nhìn trực quan và áp dụng tỷ lệ nghịch đảo để video luôn hiển thị cùng một kích thước, bất kể hiện tượng chụm/thu phóng.

Sai ngữ pháp

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

Đây có vẻ như là một điều hiển nhiên, nhưng tôi đã nhận ra khi lần đầu chơi với visualViewport.

Nếu khung nhìn bố cục đổi kích thước nhưng khung nhìn trực quan không đổi kích thước, thì bạn sẽ không nhận được sự kiện resize. Tuy nhiên, sẽ có trường hợp khung nhìn bố cục thay đổi kích thước khi khung nhìn trực quan không thay đổi chiều rộng/chiều cao.

Vấn đề thực sự là cuộn. Nếu thao tác cuộn diễn ra, nhưng khung nhìn hình ảnh vẫn ở trạng thái tĩnh so với khung nhìn bố cục, thì bạn sẽ không thấy sự kiện scroll trên visualViewport, đây là trường hợp phổ biến. Trong khi cuộn tài liệu thông thường, khung nhìn hình ảnh vẫn được khoá ở 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ả cá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 lặp lại công việc với nhiều trình nghe

Tương tự như việc nghe scrollresize trên cửa sổ, do đó, bạn có thể gọi một số loại hàm "cập nhật". Tuy nhiên, nhiều sự kiện trong số này thường xảy ra cùng một lúc. Nếu người dùng đổi kích thước cửa sổ, thì thao tác này sẽ kích hoạt resize, nhưng thường là 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ề quy cách của sự kiện này vì tôi nghĩ có thể có cách tốt hơn, chẳng hạn như một sự kiện update.

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

Do lỗi của Chrome, mã này không hoạt động:

Không nên

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ĩ (là tôi hy vọng) đây là một lỗi khác của Chrome.

offsetLeftoffsetTop được làm tròn, điều này khá không chính xác khi người dùng đã phóng to. Bạn có thể thấy các vấn đề về vấn đề này trong quá trình 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 không thu phóng.

Tỷ lệ sự kiện diễn ra 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 quá trình bản minh hoạ – sau khi chụm thu phóng, bản đồ thu nhỏ sẽ gặp sự cố khi cố định khung nhìn.

Hỗ trợ tiếp cận

Trong bản minh hoạ, tôi đã sử dụng visualViewport để chống lại hiện tượng chụm thu phóng của người dùng. Bản minh hoạ này rất phù hợp, nhưng bạn nên suy nghĩ kỹ trước khi làm bất cứ điều gì ghi đè mong 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 phóng to, bạn có thể chọn ẩn các mục position: fixed trang trí để không ảnh hưởng đến thao tác của người dùng. Nhưng một lần nữa, hãy cẩn thận, không ẩn điều gì đó 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 các trang mà người dùng đang gặp khó khăn ở mức 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ỏ giúp giải quyết các vấn đề về khả năng tương thích.