移动端滚动布局指南
前言
最近几个月在负责一款 H5 + native 混合开发的 APP,在开发过程中一直被 H5 页面的滚动问题折磨着。在经过三番四次的踩坑之后,我总结了一些我认为的最佳实践,希望能给大家一点帮助。
核心法则
确定只使用 body滚动 作为页面滚动
一般来说移动端的滚动布局分为以下两种
- 方案一:使用 overflow: auto(
不推荐
)。 即 body 不可滚动,但 body 下容器的内容将是可以滚动的。 - 方案二:使用 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>
缺点
必须使用position: fixed;撑满屏幕
为什么要使用fixed撑满屏幕
:iOS 版本 12 及以下,只有使用 fixed 才能保证元素相对视口固定,否则当你滚动页面时将会发现标题栏可被滚动。但是如果使用 fixed 撑满的话又将会导致接下来的问题2
。在Safari中iPhone 11的机型很大概率会出现触摸滚动白屏的现象,iOS其它机型小概率出现。
为什么会出现这个问题
:查询了很多资料后发现position:fixed, -webkit-overflow-scrolling:touch
在某些特殊的情况下也会产生合成层,而 webkit 渲染时只渲染视口可见范围,则当我们手指触摸滚动页面时,触发分层的渲染时机有可能错开了,从而导致白屏。解决方案
: 1. 滚动元素加上transform:translateZ(0px)
后可以消除大部分白屏,仍然会有小概率出现 2. fixed 去掉改为方案二body
滚动后则没有发现这个问题了。在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
组件的实现
优点 :
- 将由 js 在首次 dom 更新时获取元素高度,不用再为两个盒子设置同样的高度,减少心智负担
- 这对不确定高度的标题栏将会很有帮助,例如刘海屏幕适配等等 缺点:useLayoutEffect 会阻塞页面渲染,如果需要极致体验,建议还是在开发时手动设定两个盒子的高度