全新推出 visualViewport

Jake Archibald
Jake Archibald

如果我告诉您,有多个视口,您会怎么想?

BRRRRAAAAAAAMMMMMMMMMM

而您现在使用的视口实际上是视口中的视口。

BRRRRAAAAAAAMMMMMMMMMM

有时,DOM 提供给您的数据仅适用于其中一个视口,而非另一个视口。

BRRRRAAAAM… 等等,什么?

没错,请看:

布局视口与视觉视口

上方视频展示了滚动和双指张合缩放网页的过程,右侧的迷你地图显示了视口在网页中的位置。

在正常滚动过程中,一切都很简单。绿色区域表示 position: fixed 项会附着到其中的布局视口

引入双指缩放后,情况变得有点奇怪。红色方框表示视觉视口,即我们实际能看到的网页部分。此视口可以移动,而 position: fixed 元素会保持原位,附加到布局视口。如果我们在布局视口边界处平移,系统会将布局视口一并拖动。

提高兼容性

遗憾的是,Web API 在引用哪个视口方面不一致,并且在不同浏览器之间也不一致。

例如,element.getBoundingClientRect().y 会返回布局视口内的偏移量。这很棒,但我们通常需要网页中的具体位置,因此我们编写以下代码:

element.getBoundingClientRect().y + window.scrollY

不过,许多浏览器都使用视觉视口来处理 window.scrollY,这意味着当用户双指张合缩放时,上述代码会中断。

Chrome 61 会将 window.scrollY 更改为引用布局视口,这意味着即使在双指张合缩放时,上述代码也能正常运行。事实上,浏览器正在慢慢地将所有位置属性更改为引用布局视口。

除了一个新属性外…

将视觉视口公开给脚本

新 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。这不受 devicePixelRatio 影响。

还有一些事件:

window.visualViewport.addEventListener('resize', listener);
visualViewport 个事件
resize widthheightscale 发生变化时触发。
scroll offsetLeftoffsetTop 发生变化时触发。

演示

本文开头的视频是使用 visualViewport 制作的,请在 Chrome 61 及更高版本中观看。该视频使用 visualViewport 将迷你地图固定在视觉视口的右上角,并应用了反向缩放,因此即使通过双指张合缩放,迷你地图也始终显示为相同大小。

注意事项

仅在视觉视口发生变化时触发事件

这似乎是显而易见的,但当我第一次使用 visualViewport 时,却被它吓了一跳。

如果布局视口大小发生变化,但视觉视口没有发生变化,您将不会收到 resize 事件。不过,如果布局视口调整大小,而视觉视口的宽度/高度保持不变,则不太常见。

真正的问题是滚动。如果发生滚动,但视觉视口相对于布局视口保持静态,您将不会在 visualViewport 上收到 scroll 事件,这种情况非常常见。在正常滚动文档期间,视觉视口会保持锁定在布局视口的左上角,因此 scroll 不会在 visualViewport 上触发

如果您想了解视觉视口的所有更改(包括 pageToppageLeft),则还必须监听窗口的滚动事件:

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

避免使用多个监听器重复工作

与在窗口上监听 scrollresize 类似,您可能会调用某种“更新”函数。不过,这些事件通常会同时发生。如果用户调整窗口大小,系统会触发 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 存在一个 bug,以下操作不起作用

错误做法

存在 bug - 使用事件处理脚本

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

相反:

正确做法

有效 - 使用事件监听器

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

偏移值会被舍入

我认为(希望)这是另一个 Chrome 错误

offsetLeftoffsetTop 会被舍入,因此在用户放大后,它们会非常不准确。您可以在演示中看到此问题:如果用户慢慢放大并平移,迷你地图会在未放大的像素之间跳转

事件速率缓慢

与其他 resizescroll 事件一样,这些事件不会在每个帧中触发,尤其是在移动设备上。您可以在演示中看到这一点:在您双指张合缩放后,迷你地图就无法保持锁定到视口的状态。

无障碍

演示中,我使用了 visualViewport 来抵消用户的双指张合缩放。这对于此特定演示来说很有意义,但在执行任何会覆盖用户放大意愿的操作之前,您都应仔细考虑。

visualViewport 可用于提升可访问性。例如,如果用户正在放大,您可以选择隐藏装饰性 position: fixed 项,以免它们妨碍用户。不过,再次提醒您,请务必避免隐藏用户想要仔细查看的内容。

您可以考虑在用户放大时发布到分析服务。这有助于您确定用户在默认缩放级别下难以浏览的网页。

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

这样就大功告成了!visualViewport 是一个非常实用的 API,可解决沿途的兼容性问题。