移动网页高度自适应最佳实践

移动 Web 开发就要在“螺蛳壳里做道场”。移动设备限于屏幕尺寸,不得已左支右绌,既要多呈现内容,又要保证功能不要缺失。普通内容类网页还好,自然往下滚就行了;开发 Web App 的时候,当我们因为某种原因,需要限制滚动区域的时候,就很难处理。

这篇文章会分享我的一些经验,希望能节省大家摸索的时间。

无用单位:vh, svh, dvh, lvh

很早以前,我们就有了 vwvh 单位,分别指代视窗宽高的 1%。需要注意的是,移动浏览器的视口包含被各种组件占据的部分,所以 vw 就比较好用,因为没有干扰;但是高度上,被通知栏、地址栏等占用的“临时”空间就会成为我们的麻烦。

为了妥善利用屏幕空间,在我们上下滚屏的时候,大多数手机浏览器都会把地址栏、工具栏、或者通知栏隐藏起来,这就导致浏览器的可视面积其实会不断变化。原本就没用的 vh 便更没用了……于是后面新增了 svhdvhlvh 三种 长度单位,但其实帮助不大,因为当我们需要限制容器高度的时候,通常来说就不能让页面自由滚动。

因为这几个长度单位过于没用,所以我就不详细介绍了。感兴趣的同学可以看下 TailwindCSS 里的演示:https://tailwindcss.com/docs/height#viewport-height

虚拟键盘则让这个问题雪上加霜,因为虚拟键盘的显示和隐藏都不会影响这几个长度单位,所以当我们需要手动控制容器高度、位置的时候,就会很难做。

最佳实践:常规页面,交给浏览器

首先我们要信任浏览器,能够留给浏览器处理的,尽量交给浏览器原生处理。

比如,常规页面,长一点,留给浏览器自然滚动。文本框输入的时候,浏览器会自动聚焦和滚动,通常情况下没什么问题,基本体验有保证。

最佳实践:输入框文字不小于 16px

如果文本框 font-size 小于 16px,iOS Safari 下,当文本框获得焦点,Safari 会自动放大整个页面;而失去焦点的时候,页面并不会自动缩小到 100%,所以就很蛋痛。

解决方案有几个:

  1. 取消缩放。会使得可用性评价恶化,不推荐。
  2. blur 时自动恢复 100%。增加特性就是增加 bug 的可能,我觉得能不用就不用。
  3. 保持字体大小。应该大部分时候都更简单有效。

最佳实践:使用 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));
}

然后侦听输入框的 focusblur 事件:

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 里的文本框时,会凭空多出一大块空白,很烦,但是没办法解决。可以绕开,但是我觉得绕开的方案更难用。

希望这篇文章对大家有用。如果你对移动网页开发有什么问题,欢迎留言讨论。

如果您觉得文章内容对您有用,不妨支持我创作更多有价值的分享:


已发布

分类

来自

标签:

评论

《“移动网页高度自适应最佳实践”》 有 1 条评论

  1. Alex 的头像

    还有一种方案是nextjs的classname包含css,每次单独追踪起来容易一些,需要的话就标准件话,不需要每次手搓也没影响😄

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据