Sticky Header
在实际业务中经常碰到页头固定在浏览器的顶部,而在移动端上使用position:fixed
坑多难搞。记得 EFE 团队分享过一篇《Web 移动端 Fixed 布局的解决方案》博文,就是介绍如何解决移动端上实现页头固定的技术方案。除了文章中介绍的方案之外,@Brad Frost 也推荐了几个 JavaScript 的解决方案,比如 iScroll 4 和 Scrollability。使用fixed
是一种固定页头的,但很多时候是希望实现Sticky Header的效果,说到这里大家可能会想起position
新增的属性值sticky
。虽然这个能实现我们想要的效果,但这个属性的支持性还是需要等待一段时间。
更多精彩内容请看 web 前端中文站
http://www.lisa33xiaoq.net 可按 Ctrl + D 进行收藏
sticky 正常的使用方法
position:sticky
正常的使用方法,非常的简单:
<div class="header">Sticky Headers</div> .sticky { position: sticky; top: 15px; }
元素sticky
距离浏览器顶部15px
,该元素就固定在那了。很多时候这个配合 JavaScript 的scroll
事件一起使用。
var header = document.querySelector('.header'); var origOffsetY = header.offsetTop; function onScroll(e) { window.scrollY >= origOffsetY ? header.classList.add('sticky') : header.classList.remove('sticky'); } document.addEventListener('scroll', onScroll, false);
看上去是不是非常的简单。刚才也说了,sticky
的支持度还是需要等待一段时间。可以通过 caniuse.com 来查阅。
有关于position:sticky
的相关资源也可以阅读下面几篇文章:
- Specification
- geddski article: Examples and Gotchas
- HTML5Rocks
- Mozilla Developer Network (MDN) documentation – CSS position
- WebPlatform Docs
- IE platform status: Preview Release
- Chrome platform status:
- WebKit platform status: Supported
当然也有对应的 Polyfill。比如这个和这个。不过@Jeff Wainwright 前几天在 CSS-Tricks 网站上分享了一篇文章使用Stickybits来替换position:sticky
的 Polyfills 方案:
如果对 Stickybits 感兴趣的同学,可以仔细阅读这篇文章《Stickybits: an alternative to position: sticky
polyfills》或其官网查阅相关文档。
虽然上面的方案都可以解决Sticky Headers效果,但我更对@Remy Sharp 分享的几篇文章更感兴趣:
- Sticky headers (#1/3)
- Smooth scroll & sticky navigation (#2/3)
- CSS sticky nav & smooth scroll (#3/3)
下面的内容是根据上面三篇文章整理而来的,不过英文好的同学建议直接阅读上面三篇文章。
Sticky Headers
以前实现 Sticky Headers 效果,很多时候都是借助于 JS(更早有很多 jQuery 插件)。比如像下面这样的代码:
var toggleHeaderFloating = function() { // Floating Header if ($window.scrollTop() > 80) { $('.header-section').addClass('floating'); } else { $('.header-section').removeClass('floating'); } } $window.on('scroll', toggleHeaderFloating);
上面的代码检查每次浏览器垂直滚动条滚动的位置超过80px
的时候给元素.header-section
添加floating
类名,反之则删除floating
类名。
其中$window
是一个window
对象。但事实上,这段代码有很多禁忌。不过这不是一个大问题,问题是我们应该要理解如何避免一些小障碍。
几年前@Paul Irish 分享过一篇滚动性能相关的文章,文章虽然介绍的是scroll
事件,但也可能适用于wheel
和mousemove
事件。文章中建议使用scroll
事件时应该尽量避免接触 DOM(Touching DOM),避免引发布局(也称为回流)。@Paul 也搜集了一些,怎样才会触发回流。
如果我们在scroll
事件上什么都不做,我们又能做些什么呢?我们可以使用requestAnimationFrame
以防反跳(Debounce)。当用户滚动滚动条时,我们会使用一个函数来检查滚动条的位置,但如果用户快速滚动滚动条,那么我们最好先避免Scroll Jank。
// used to only run on raf call var rafTimer; $window.on('scroll', function(){ cancelAnimationFrame(rafTimer); rafTimer = requestAnimationFrame(toggleHeaderFloating); });
上面的 jQuery 代码有两件事情一直困扰我:
- 每次
scroll
事件都会触发运行一个 jQuery 选择器 - 查询每一个元素
诚然getElementsByClassName
(jQuery 或 Sizzle 选择一个类)已经优化得很好,这不是很大的问题。然而,我们在每一个滚动勾子时不需要构造一个新的 jQuery 对象。
在 Chrome Devtools 中运行下面的代码,每次滚动页面将会记录运行的次数:
window.onscroll = () => console.count('scroll') // or monitorEvents('scroll')
整体代码:
var $headerSection = $('.header-section'); var toggleHeaderFloating = function () { // Float Header if ($window.scrollTop() > 80) { $headerSection.addClass('floating'); } else { $headerSection.removeClass('floating'); } } var rafTimer; $window.on('scroll', function(){ cancelAnimationFrame(rafTimer); rafTimer = requestAnimationFrame(toggleHeaderFloating); });
事实上,当滚动条位置超过一定的阈值时,header-section
只需要改变一个场景。
另一种选择是使用classList
检查这个类是否需要改变。
var rafTimer; window.onscroll = function (event) { cancelAnimationFrame(rafTimer); rafTimer = requestAnimationFrame(toggleHeaderFloating); } function toggleHeaderFloating() { // does cause layout/reflow: https://git.io/vQCMn if (window.scrollY > 80) { document.body.classList.add('sticky'); } else { document.body.classList.remove('sticky'); } }
组件问题
预期的效果是,当我滚动滚动条时,导航会固定在页头。哪果我点击导航菜单项时,将平滑地滚动到对应的位置。
实际效果,你可以点浏览 2016.ffconf.org 网站查看效果。
要实现此效果,还有些问题有待解决:
Sticky Element
Sticky Element 元素是导航部分,它一直固定在页面的顶部会更简单,因为它总是有一个position:fixed
样式设置。虽然,导航元素只有超过一个阈值才会粘在顶部。
最初考虑我们是否能使用IntersectionObserver
,一种逆向方法,但它不适合。
下面的代码跟踪和应用sticky
类名来解决导航元素的位置。请注意,我把sticky
类名用在body
元素上。至于为什么,稍后再聊。
// 获取 Sticky Element,这里指的是`sticky-header`元素 var stickyHeader = document.getElementById('sticky-header'); // 记录当前的位置,当超过这个阈值时添加`sticky`类名,反则删除 var boundary = stickyHeaderRef.offsetHeight; // 当页面滚动时,尽可能少,在这种情况下注册一个 rAF 回调`checkSticky` window.onscroll = function (event) { requestAnimationFrame(checkSticky); } function checkSticky() { // 收集当前滚动条位置 var y = window.scrollY + 2; // 检测 body 元素是否包含`sticky`类 var isSticky = document.body.classList.contains('sticky'); if (y > boundary) { // 当前滚动条位置超过阈值 // body 元素并没有`sticky`类; 如果没有包含,添加该类名 if(!isSticky) { document.body.classList.add('sticky'); } } else if (isSticky) { document.body.classList.remove('sticky'); } }
只有上面的 JavaScript 代码还是不够的,还需要配合一些 CSS 代码:
#sticky-header { top: 0; } body.sticky { padding-top: 100px; } body.stick #sticky-header { position: fixed; }
对应的原理就不用做更多的阐述吧。这里有两个点比较重要,当滚动条位置超过预定阈值时,body
元素会添加一个sticky
类名,并且给body.sticky
添加padding-top:100px
。同时 Sticky 元素的position:static
变成了position:fixed
。给body
添加一个padding-top:100px
主要是让内容不会被 Sticky 元素固定在顶部是遮住内容。
链接到锚点位置
这里指的是,当你点击导航栏的菜单项时,到达到页面的指定位置。也就是对应的锚点位置。
现在我们实现了导航粘在浏览器顶部,但我们单击导航中链接时页面会跳转,但导航元素遮盖了标题,这不是我们想要的效果:
为了解决这个问题,给目标元素添加一个height
值,来抵消导航元素的高度,在我们的示例中是100px
:
:target:before { content: ''; display: block; height: 100px; }
这里采用了 CSS 选择器:target
和伪类选择器:before
配合。
平滑滚动
另一个有待解决的问题是,当我们点击导航到达指定位置时需要一个平滑滚动效果。这个让事情变得有点复杂。不过我发现一个较好的 JavaScript 库,但发现的有点晚。
虽然用已有的 JavaScript 库来解决这个效果,但我还是决定自己来强撸这个功能。部分功能是我所能预料到的,但部分功能是我想得太简单了。比如说,我想用一个简单的 Tweening 函数,但我自己还是不能独立完成,最后还是选择了@Soledad Penadés 的 tween 库。
具体代码如下,代码中有一些简单的注释:
// 监听 body 元素的点击事件 document.body.addEventListener('click', function(event){ var node = event.target; var location = window.location; // ignore non-links elements being clicked if (node.nodeName !== 'A') { return; } // ignore cmd+click etc if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey) { return; } // only hook local URLs to the page if (node.origin !== location.origin || node.pathname !== location.pathname){ return; } event.preventDefault(); window.history.pushState(null, null, node.hash); var target = document.querySelector(node.hash); var fromY = window.scrollY; var coords = { x: 0, y: fromY } var y = target.offsetTop; if (fromY < y) { y -= 100; // offset for the padding-top } var running = true; var tween = new TWEEN.Tween(coords) .to({x: 0, y: y}, 500) .easing(TWEEN.Easing.Quadratic.Out) .onUpdate(function(){ window.scrollTo(this.x, this.y); if (this.y === y) { running = false; } }) .start(); requestAnimationFrame(animate); function animate(time) { if (running) { requestAnimationFrame(animate); TWEEN.update(time) } } });
原生的 CSS 方案
第二部分主要介绍的是使用 JavaScript 来实现 Sticky 元素和平滑滚动的效果。但随着 CSS 发展,可以使用 CSS 来实现。
html { scroll-behavior: smooth; } #masthead { position: sticky; top: calc(-100% + 100px); /* make sure stick above images */ z-index: 1; /* tweaks to the ffconf design to keep the height right */ display: flex; flex-direction: column; } .logo-wrapper { flex-basis: 85vh; }
具体的 DEMO 可以点击这里查看效果。
遗憾的是,到目前为止仅只有 Firefox 浏览器同时支持position:sticky
和scroll-behavior
。但在其他的浏览器中也能看到较好的效果,比如 Chrome 浏览器。
回到最初,如果你在项目中想直接使用position:sticky
和scroll-behavior
。你可以在支持的浏览器中直接使用这两个属性,在不支持的浏览器中使用对应的 Polyfill。
- Sticky 的 Polyfill:Stickybits
- scroll-behavior 的 Polyfill:Smooth Scroll Polyfill
总结
这篇文章简单的介绍了如何在项目中实现positon:sticky
和平滑滚动条效果。除了借助 JavaScript 之外,我们更期待使用纯 CSS 来实现。我们的宗旨是:能使用 CSS 实现的效果绝不使用 JavaScript。但碍于浏览器对其兼容性的原因,在实际项目中,可以考虑采用对应的 Polyfill。这个时候,你可能会说,这样一来还不是使用 JavaScript 吗?事实是这样,但很多时候,咱们还可以考虑 Houdini 来实现。如果你对 Houdini 从未了解过,建议阅读@Philip Walton 在 2017 CSS Day 分享的主题《Houdini & Polyfilling CSS》。这是一个很有意思的话题。
【注:本文源自网络文章资源,由站长整理发布】