移动端滚动布局指南

You, web development
Back

前言

最近几个月在负责一款 H5 + native 混合开发的 APP,在开发过程中一直被 H5 页面的滚动问题折磨着。在经过三番四次的踩坑之后,我总结了一些我认为的最佳实践,希望能给大家一点帮助。

核心法则

确定只使用 body滚动 作为页面滚动

一般来说移动端的滚动布局分为以下两种

  1. 方案一:使用 overflow: auto(不推荐)。 即 body 不可滚动,但 body 下容器的内容将是可以滚动的。
  2. 方案二:使用 body 滚动(推荐)。即保持固定的标题区域,而其它的一切都是可以滚动的。

方案一 overflow: auto 踩坑指北(不推荐)

下面介绍了为什么不用方案一,在项目的开始之初我选用了方案一,结果被测试同学提了一箩筐的 bug😭😭😭,以下为方案一简单实现及出现的问题

简单实现

<html>
  <style>
    /* 禁止body滚动 */
    body {
      overflow: hidden;
    }

    /* fixed保证根容器撑满屏幕 */
    #root {
      position: fixed;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      overflow: hidden;
      display: flex;
      flex-direction: column;
    }

    header {
      height: 50px;
      background: red;
    }

    /* 子容器自动撑满父容器 */
    #scroll-container {
      flex: 1;
      overflow-y: auto;
      -webkit-overflow-scrolling: touch;
    }
  </style>

  <body>
    <div id="root">
      <header></header>
      <div id="scroll-container"></div>
      <footer></footer>
    </div>
  </body>
</html>

实现思路

div#scroll-container 使用flex: 1;可以自动把标题栏 header 和底部栏 footer 撑到页面头部和底部, 这样当 div#scroll-container 中内容滚动时 header 和 footer 的位置还是固定的。 如果有二级标题栏 header.subtitle 的时候也可以在 div#scroll-container 中再嵌套个 flex 盒子(如下),div.nested-scroll-container 再利用flex: 1;撑开,这样可以保证滚动容器可以占满页面剩余位置。如下例所示,可以一直无限套娃下去~

<body>
  <div id="root">
    <header></header>
    <div id="scroll-container">
      <div
        class="box"
        style="
            height: 100%;
            overflow: hidden;
            display: flex;
            flex-direction: column;
          "
      >
        <header class="subtitle"></header>
        <div
          class="nested-scroll-container"
          style="flex:1;overflow-y:auto;"
        ></div>
      </div>
    </div>
    <footer></footer>
  </div>
</body>

缺点

  1. 必须使用position: fixed;撑满屏幕 为什么要使用fixed撑满屏幕:iOS 版本 12 及以下,只有使用 fixed 才能保证元素相对视口固定,否则当你滚动页面时将会发现标题栏可被滚动。但是如果使用 fixed 撑满的话又将会导致接下来的问题2
  2. 在Safari中iPhone 11的机型很大概率会出现触摸滚动白屏的现象,iOS其它机型小概率出现。 为什么会出现这个问题:查询了很多资料后发现position:fixed, -webkit-overflow-scrolling:touch在某些特殊的情况下也会产生合成层,而 webkit 渲染时只渲染视口可见范围,则当我们手指触摸滚动页面时,触发分层的渲染时机有可能错开了,从而导致白屏。 解决方案: 1. 滚动元素加上transform:translateZ(0px)后可以消除大部分白屏,仍然会有小概率出现 2. fixed 去掉改为方案二body滚动后则没有发现这个问题了。
  3. 在Safari中滑动卡住 复现路径:在滚动容器中使用了-webkit-overflow-scrolling: touch;后,如果其任一父容器可以滚动则有很大概率出现 解决方案:1. 确保父容器都增加了overflow: hidden; 2. 改为方案二body滚动

方案二 body 滚动指南(推荐)

当你踩完上面的坑后 相信我这一定是你的最佳选择

简单实现

<html>
  <style>
    body {
      -webkit-overflow-scrolling: touch;
    }

    .header {
      height: 50px;
    }

    .header-content {
      position: fixed;
      left: 0;
      right: 0;
      top: 0;
      height: 50px;
    }
  </style>

  <body>
    <div id="root">
      <header class="header">
        <div class="header-content"></div>
      </header>

      <section1> </section1>
      <section2> </section2>
      <section3> </section3>
    </div>
  </body>
</html>

实现思路

保持固定的标题区域,而其它的一切都是可以滚动的。

怎么固定标题栏?

使用 CSS 样式 position: fixed; top: 0;设置标题部分.header-content,因为这将使标题相对于视口有效固定,滚动时不会对其位置产生影响。但当你使用 fixed 固定时,标题栏将会从文档流中删除,这意味着所有剩余的内容都将在其下方滑动。所以你需要提供一个和标题栏相同高度占位元素来修复此行为(即 .header{height: 50px};

进阶:怎么固定多个标题栏?

因为 position: fixed;当不设置 top 时会相对于父元素定位。我们可以利用这一特性,即有多个标题栏的时候可以不用设置 top 的值(例:.header2-content), 这样就可以不用考虑之前的标题栏的位置,只需要保证其父元素在文档流中的位置就可以了

<html>
  <style>
    body {
      -webkit-overflow-scrolling: touch;
    }

    .header {
      height: 50px;
      background: red;
      z-index: 10;
    }

    .header-content {
      height: 50px;
      position: fixed;
      left: 0;
      right: 0;
      top: 0;
    }

    .header2 {
      height: 50px;
      background: blue;
      z-index: 10;
    }

    .header2-content {
      height: 50px;
      position: fixed;
      left: 0;
      right: 0;
    }
  </style>

  <body>
    <div id="root">
      <header class="header">
        <div class="header-content"></div>
      </header>

      <header class="header2">
        <div class="header2-content"></div>
      </header>

      <section1> </section1>
      <section2> </section2>
      <section3> </section3>
    </div>
  </body>
</html>

进阶:怎么才能不手动设定固定高度?

useLayoutEffect 中获取元素高度,可在查看以下实现中 Affix 组件的实现 优点 :

  1. 将由 js 在首次 dom 更新时获取元素高度,不用再为两个盒子设置同样的高度,减少心智负担
  2. 这对不确定高度的标题栏将会很有帮助,例如刘海屏幕适配等等 缺点:useLayoutEffect 会阻塞页面渲染,如果需要极致体验,建议还是在开发时手动设定两个盒子的高度

codesanbox 例子

© liaoliao666.