标签: mobile web

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

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

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

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

  • 破pure

    pure是我用过的最差的前端框架:1. 布局不兼容老版flex-box,大量Android手机用不了;2. 外层框架字间距缩小,还得单独恢复;3. .pure-button里一堆莫名其妙的属性,用Android手机自带的WebKit无法正常tap。小是小,没带来啥价值,毛病又多,所以我一直不喜欢雅虎系的东西。