浅谈 100vh 在移动端中的差异及解决方案

前言

在做活动页的时候,第一页需要整屏显示,使用了 100vh 来作为第一页的高度,但在 ios safari 中效果却不是我所期望的那样。遂写篇博文来记录下问题的产生和解决及原因。

存在的差异

首先 100vh 在 Chrome 中模拟手机视图是正常的,如下:

chrome 模拟

在安卓真机 UC Turbo 显示也是正常的:

UC Turbo


但在 ios safari 中,底部被 safari 的菜单栏挡住了:

safari mobile


出现的原因

这个问题出现的原因是:Safari 故意这样设计的,为此还专门做了很多的处理,因为它可以防止出现一些其他的问题。具体参见 Benjamin Poulain 回答的 bug 清单 ,其大意是:Safari 在滚动时可视高度会动态变化,如果在滚动的过程中更新 css 的视口高度,则会在滚动的过程中触发重新布局,这样做的话是非常糟糕的。

Safari 在向下滚动的过程中,顶部的工具栏和底部的菜单栏会隐藏,这样一来页面的视口高度就会发生改变。此时 safari 有两个选择:第一个是在视口高度变化时动态的更新 css 的视口高度,但这样做页面会在高度变化的时候重绘回流,重新布局,不仅会影响滚动的性能,还会影响交互体验。第二个则是在变化前的小视口和变化后的大视口来做为一个统一的视口,显然 Safari 的开发人员选择了大的视口作为标准视口,这样就会出现上面提到的问题:css 中设置高度为 100vh 时,底部的菜单栏会挡住一部分页面,但却避免了更多的问题。

解决方案

对于上面的这个问题,目前来说,比较完美的方案就是利用 window.innerHeightresize 事件中动态的设置高度,对于使用 vh 来说,可以使用 window.innerHeight 配合 css 的自定义属性(变量)来适配。

如在 vue 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mounted() {
this.vhCheck()
window.addEventListener('resize', this.vhCheck)
this.$once('hook:beforeDestroy', () => {
window.removeEventListener('resize', this.vhCheck)
})
},

methods: {
vhCheck() {
// 模拟 vh
let vh = window.innerHeight * 0.01
// 设置 css 自定义属性
document.documentElement.style.setProperty('--vh', `${vh}px`)
}
}

这样一来,在 css 中便可以这样写:

1
2
3
4
5
.page {
/* 对不支持 css var 的浏览器做降级处理 */
height: 100vh;
height: calc(var(--vh, 1vh) * 100);
}

使用第三方库

推荐一个解决类似问题的 npm 库 vh-check

1
npm install vh-check

main.js:

1
2
3
import vhCheck from 'vh-check'

vhCheck('browser-address-bar')

css:

1
2
3
4
.page {
height: 100vh;
height: calc(100vh - var(--browser-address-bar, 0px));
}

vh-check 的核心原理也是上面所说的 window.innerHeight 结合 css var()

最后页面在 safari 中的表现总算比较正常了,此类问题,目前来说没有完美的方案,姑且算是比较完美吧。

safari mobile fix

PS: 最近接入了 gitTalk , 也可以手动提交 issues 进行讨论,我会在第一时间进行回复。
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×