如果我告诉您,有多个视口,您会怎么想?
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
|
在 width 、height 或 scale 发生变化时触发。
|
scroll
|
在 offsetLeft 或 offsetTop 发生变化时触发。
|
演示
本文开头的视频是使用 visualViewport
制作的,请在 Chrome 61 及更高版本中观看。该视频使用 visualViewport
将迷你地图固定在视觉视口的右上角,并应用了反向缩放,因此即使通过双指张合缩放,迷你地图也始终显示为相同大小。
注意事项
仅在视觉视口发生变化时触发事件
这似乎是显而易见的,但当我第一次使用 visualViewport
时,却被它吓了一跳。
如果布局视口大小发生变化,但视觉视口没有发生变化,您将不会收到 resize
事件。不过,如果布局视口调整大小,而视觉视口的宽度/高度保持不变,则不太常见。
真正的问题是滚动。如果发生滚动,但视觉视口相对于布局视口保持静态,您将不会在 visualViewport
上收到 scroll
事件,这种情况非常常见。在正常滚动文档期间,视觉视口会保持锁定在布局视口的左上角,因此 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 存在一个 bug,以下操作不起作用:
存在 bug - 使用事件处理脚本
visualViewport.onscroll = () => console.log('scroll!');
相反:
有效 - 使用事件监听器
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,可解决沿途的兼容性问题。