移动 Web 开发就要在“螺蛳壳里做道场”。移动设备限于屏幕尺寸,不得已左支右绌,既要多呈现内容,又要保证功能不要缺失。普通内容类网页还好,自然往下滚就行了;开发 Web App 的时候,当我们因为某种原因,需要限制滚动区域的时候,就很难处理。
这篇文章会分享我的一些经验,希望能节省大家摸索的时间。
无用单位:vh
, svh
, dvh
, lvh
很早以前,我们就有了 vw
,vh
单位,分别指代视窗宽高的 1%。需要注意的是,移动浏览器的视口包含被各种组件占据的部分,所以 vw
就比较好用,因为没有干扰;但是高度上,被通知栏、地址栏等占用的“临时”空间就会成为我们的麻烦。
为了妥善利用屏幕空间,在我们上下滚屏的时候,大多数手机浏览器都会把地址栏、工具栏、或者通知栏隐藏起来,这就导致浏览器的可视面积其实会不断变化。原本就没用的 vh
便更没用了……于是后面新增了 svh
,dvh
和 lvh
三种 长度单位,但其实帮助不大,因为当我们需要限制容器高度的时候,通常来说就不能让页面自由滚动。
因为这几个长度单位过于没用,所以我就不详细介绍了。感兴趣的同学可以看下 TailwindCSS 里的演示:https://tailwindcss.com/docs/height#viewport-height
虚拟键盘则让这个问题雪上加霜,因为虚拟键盘的显示和隐藏都不会影响这几个长度单位,所以当我们需要手动控制容器高度、位置的时候,就会很难做。
最佳实践:常规页面,交给浏览器
首先我们要信任浏览器,能够留给浏览器处理的,尽量交给浏览器原生处理。
比如,常规页面,长一点,留给浏览器自然滚动。文本框输入的时候,浏览器会自动聚焦和滚动,通常情况下没什么问题,基本体验有保证。
最佳实践:输入框文字不小于 16px
如果文本框 font-size
小于 16px,iOS Safari 下,当文本框获得焦点,Safari 会自动放大整个页面;而失去焦点的时候,页面并不会自动缩小到 100%,所以就很蛋痛。
解决方案有几个:
- 取消缩放。会使得可用性评价恶化,不推荐。
blur
时自动恢复 100%。增加特性就是增加 bug 的可能,我觉得能不用就不用。- 保持字体大小。应该大部分时候都更简单有效。
最佳实践:使用 dvh
并解决兼容问题
虽然但是,当我们需要固定高度的时候,表示视窗净高度的 dvh
仍然是我们最佳选择。
不过,在我写文章的现在,dvh
的兼容性不是很好,所以必须做好兼容性配置。我建议用 JS 结合 CSS 变量来做。在 <head>
里插入这段 JS:
// 首先,判断是否支持 dvh 单位
if (!CSS.supports('height', '100dvh')) {
// 如果不支持,就定义 --app-height 为视口高度,即 window.innerHeight
document.body.style.setProperty('--app-height', window.innerHeight + 'px');
// 当屏幕缩放时,改变内容高度。因为 resize 事件触发很频繁,所以使用节流减少性能损耗
let timeout;
function onResize() {
clearTimeout(timeout);
timeout = setTimeout(() => {
clearTimeout(timeout);
document.body.style.setProperty('--app-height', window.innerHeight + 'px');
}, 500);
}
window.addEventListener('resize', onResize);
}
然后定义 CSS 样式:
:root {
--app-height: 100dvh;
}
.h-dvh-app {
height: var(--app-height);
}
如果使用 TailwindCSS,那么在配置文件里增加配置即可:
export default {
theme: {
extend: {
spacing: {
'dvh-app': 'var(--app-height)',
},
},
},
}
最佳实践:使用 CSS 变量解决虚拟键盘
只是限制高度为 100dvh
,当虚拟键盘弹出之后,因为视口缩小,很可能会出现问题。此时 window.resize
事件也不会触发,所以我们应该侦听文本框的 focus
事件,动态改变容器高度;并在文本框 blur
之后,恢复高度。
此时,我们可以借助 CSS 变量的“默认值”功能,即 var(--custom-value, --default-value)
来处理。当我们需要暂时的高度以应对虚拟键盘时,设置 --custom-value
;之后,移除 —custom-value
,恢复到预定义的 --app-height
。
首先,修改 css,定义 --input-height: initial
,这个值会被认为是空值而忽略。
:root {
--input-height: initial;
--app-height: 100dvh;
}
.h-dvh-app {
height: var(--input-height, var(--app-height));
}
然后侦听输入框的 focus
和 blur
事件:
async function onTextareaFocus(): Promise<void> {
// 桌面端忽略这个需求
if (window.innerWidth > 640) return;
// 给虚拟键盘弹出一些时间
await sleep(300);
const { innerHeight } = window;
document.body.style.setProperty('--input-height', `${innerHeight}px`);
// 需要的话,可以在这里插入一个滚动
}
async function onTextareaBlur(event: FocusEvent): Promise<void> {
// 同样,也给虚拟键盘收起留一些时间
await sleep(250);
document.body.style.removeProperty('--input-height');
}
总结
至此,遵守以上最佳实践之后,基本上我们可以妥善处理移动网页里的浏览器高度。当然,并不完美,比如,iOS Safari 在输入 position: sticky
里的文本框时,会凭空多出一大块空白,很烦,但是没办法解决。可以绕开,但是我觉得绕开的方案更难用。
希望这篇文章对大家有用。如果你对移动网页开发有什么问题,欢迎留言讨论。
欢迎吐槽,共同进步