全新推出 visualViewport

杰克·阿奇博尔德
Jake Archibald

如果我告诉你,有多个视口,该怎么办?

BRRRRAAAAAAAMMMMMMMM

而您当前使用的视口实际上是视口内的一个视口。

BRRRRAAAAAAAMMMMMMMM

有时,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,以下操作无法执行

错误做法

错误 - 使用事件处理程序

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,可以在此过程中解决兼容性问题。