Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

纯 CSS 解决H5布局中的吸顶吸底 #64

Open
closertb opened this issue Oct 25, 2020 · 0 comments
Open

纯 CSS 解决H5布局中的吸顶吸底 #64

closertb opened this issue Oct 25, 2020 · 0 comments
Labels
CSS about CSS

Comments

@closertb
Copy link
Owner

closertb commented Oct 25, 2020

演示Demo地址(手机端打开):https://closertb.site/Klotski/index.html#demo
演示Demo源码:https://github.com/closertb/klotski

哪些想啥提啥的产品们

最近做了一个需求,准确说是迭代需求:加了一个头部概览(类似下图),以更好的让用户观察到营销变化,故事的开头就这样悄悄的埋下了伏笔。

以前这个页面只是一个评价列表(可上拉加载),为了数据更易读,列表的头采用了固定布局。然而加了这个概览时,产品没提,我就简单粗暴的将这个列表头换成了相对布局,ok,提测。
20201017180423
但第二天,我发现上拉加载数据多了,列表头部被顶上去之后,想再做筛选,就要再把列表上滑才能看到,这个体验非常之差。于是同事就说要不问问产品,要不把概览加概览做成固定。

我第一反应就是,恐怕提了之后,产品会让我把筛选列表头部做成固定,注意那个只。

然后就有了下面的对话:

20201017175848

果然怕什么,来什么。但就像同事说的,自己问的需求,含着泪也要接下。

后面经评论提点,又加入了sticky的方案,确实是最优解。

局部吸顶

以下代码是页面的dom结构

<div id="demo" className={style.demo}>
  <h3 id="title" className="title">这是一个概览头部</h3>
  <div id="content" className="content">
    <div className="filter-bar">
      <h3>这是列表头部</h3>
      <h3>可筛选</h3>
      <h3>下面是滚动列表</h3>
    </div>
    <ul className="list">
      {arr.map(({ key, label }) => <li key={key}>{label}</li>)}
    </ul>
  </div>
</div>

JS 实现

因为页面本身就有scroll事件监听,所以第一个念头是用JS完成,但当时已经下班,又是周五,感觉5分钟内搞不定,所以我就跑了。

现在来尝试用JS实现,先理一下思路:

  • 监听页面的滚动,当ul元素顶部距离页面顶部大于title 高度时,添加一个css类使筛选头部吸顶;
  • 当ul元素距离顶部小于等于title 高度时,删除添加的类,取消筛选头部吸顶

JS 代码

useEffect(() => {
  const demo = document.querySelector('#demo');
  const content = document.querySelector('#content');
  const titleHeight = document.querySelector('#title').clientHeight;
  let fixed = false;
  demo.addEventListener('scroll', (e) => {
    // 添加吸顶
    if (!fixed && e.target.scrollTop >= titleHeight) {
      fixed = true;
      content.classList.add('with-fixed');
    }
    // 取消吸顶
    if (fixed && e.target.scrollTop < titleHeight - 5) {
      content.classList.remove('with-fixed');
      fixed = false;
    }
  });
}, []);

看起也不难,但其实离代码上线,还有很大优化的空间,后面会分析补充。

CSS Viewport实现

JS 看似很简单,但就像那句热门句子:这突如其来的噩耗,让本不富裕的家庭雪上加霜。在这种有下拉加载的页面,我们本来就在监听里面做了很多逻辑处理,所以能用CSS实现的,就尽量不要再去麻烦JS了。

首先理一下思路,深挖产品的需求:

  • 保持筛选头在可视范围之内(吸顶), 保证可筛选;
  • 当列表数据多时,尽可能多展示列表,即概览头部就没必要看到了;
  • 列表是上拉加载的;

当理清上面思路时,我们发现,其实就是当列表很长时,隐藏概览头部,简单用伪代码表示就是(vh是视口单位 ,100vh代表整个屏幕可视高度):

   if (titleHeight + filterBarHeight + listHeight > 100vh) {
     title.hide();
   }

那又怎样实现概览头部隐藏,而筛选头和列表又正好出于视口呢?

 filterBarHeight + listHeight = 100vh

当用户往上划,只需要内容(筛选头和列表)正好是一个视口高度(100vh)时,概览头就恰好被隐藏,而筛选头又正好吸顶,用CSS实现就是类似这样的:

// 不是完整代码,详情请看demo:
.demo {
 :global {
   .title {
     height: 15vh;
     line-height: 15vh;
     text-align: center;
     border-bottom: 1PX solid #eee;
     background-color: #fff;
   }
   .filter-bar {
     height: 15vw;
     background-color: #888;
     display: flex;
     align-items: center;
   }
   .list {
     max-height: calc(100vh - 15vw);; // 这里的设置很重要
     overflow: scroll;
     background-color: rgba(127, 255, 212, .8);
   }

最优实现 CSS Sticky

在css中的定位(position)属性值中有个不常用的:sticky

MDN官方文档摘录:
元素根据正常文档流进行定位,然后相对它的最近滚动祖先和最近块级祖先,包括table-related元素,基于top, right, bottom, 和 left的值进行偏移。粘性定位可以被认为是相对定位和固定定位的混合。元素在跨越特定阈值前为相对定位,之后为固定定位。

这里我们在沿用JS的方案上进行更改,只需要将filter-bar 的定位属性改为粘性定位,就可以去除对 JS 的依赖;

.demo {
  max-height: 100%;
  overflow: scroll;
  .filter-bar {
    position: sticky;
    top: 0;
  }
}

demo类作用于最外层dom(<div className={style.demo}>)上,其视高为100vh,当内容超出高度时为滚动;filter-bar 元素采用粘性定位,当高度距离demo元素大于0时,其采用相对定位,即以正常文档流的形式定位;当高度小于等于0时,其采用固定定位,就达到吸顶的效果。

对比

是不是感觉CSS很简单,稍微设置一下即搞定,只是要想到内容高度正好是100vh需要一点经(yun)验(qi),经常写H5的,sticky的方案相信也是新手黏来。其实不光简便,对比JS至少还有两个个优点:

  • JS 如果只是上面那样,直接将筛选头的定位改成固定定位,眼力好的人,其实是能感觉到列表有跳变的一瞬间,就是列表会突然上移filterBar高度,来填补筛选头离开正常文档流;(解决方案就是在筛选头外多套一层dom,并给一个固定高度,这样筛选头脱离正常文档流,但高度依旧还在);
  • 当用JS来操作Dom元素重排时,这每年面试官说的那些重绘重拍我就不多说了,这消耗的性能肯定高于CSS实现;

当然,viewport方案还有个ios手机的兼容性问题,由于safari的头部和底部滑动时可见性会改变,所以当Bar可见时,实际的100vh高于屏幕可见高度,就会导致吸顶头部被遮挡。到目前为止,虽然网上有很多说height: -webkit-fill-available;,但针对这种场景是无效的;但只要依赖100vh,都面临这种困局,safari太奇葩,下一个IE就是它了.

20201024155007

经过上面分析,100vh在IOS safari上的致命问题,会让这种100vh这种纯CSS的方案褪色。但PC页面,或者你和我一样,要编写的页面是运行在APP中(即没有bar存在),那这种方案就是可行的。所有的方案都要具体场景,具体分析,没有谁出生就是完美。这里只是提高一种思考方式,长点见识。

而sticky方案不依赖于100vh,其可以用100%的写法,所以没有这个担忧,所以相比之下,最优解就是sticky; 但height: 100% 是个无底洞,你需要从html 标签写起,一直写到具有滚动的容器元素。

如果对重绘重排有兴趣,建议观看Chrome的官方博文: 浏览器四部曲

弹性吸底

说完局部弹性吸顶,再说一个常见的,选择性吸底:在页面内容不足100vh时,我们希望Footer是吸底的,当页面内容大于100vh时,Footer处于正常文档流,让内容可视区域更大,而又不会因为内容太少影响美观,见图:
20201025095027

像第一张图那样不做定位的还是大有人在,因为他们坚信自己网站的内容不会出现不够的时候,但以前更常见做法是底部固定定位。

弹性吸底利用min-height 加绝对定位,其实现很简单。核心代码不超过5行css:

body{
  position: relative;
  min-height: 100vh;
}

footer {
  width: 100%;
  position: absolute;
  bottom: 0;
}

原理就是内容区域最低高度为一个屏幕,然后底部相对屏幕进行绝对定位;当内容变多时,高度大于100vh,由于是依赖bottom: 0;,所以会一直吸底,其巧妙之处就在于此。

针对于这个场景,height: -webkit-fill-available 就是有效的。
20201025102807

更多关于-webkit-fill-available, 参见[https://allthingssmitty.com/2020/05/11/css-fix-for-100vh-in-mobile-webkit/];

总结

vh 确实是个好东西,可以解决移动端的适配问题, ;sticky 这种非热门属性(经常写PC的人)值得花时间去学习。我个人觉得作为一个合格的前端,CSS 仍然是必备技能,偶尔看看张旭鑫,还是有必要的;不要对JS产生太多的依赖,不是不可以,而是好钢要用在刀刃上。

@closertb closertb added the CSS about CSS label Dec 5, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CSS about CSS
Projects
None yet
Development

No branches or pull requests

1 participant