在实际业务中经常碰到页头固定在浏览器的顶部,而在移动端上使用position:fixed
坑多难搞。记得EFE团队分享过一篇《Web移动端Fixed布局的解决方案》博文,就是介绍如何解决移动端上实现页头固定的技术方案。除了文章中介绍的方案之外,@Brad Frost也推荐了几个JavaScript的解决方案,比如iScroll 4和Scrollability。使用fixed
是一种固定页头的,但很多时候是希望实现Sticky Header的效果,说到这里大家可能会想起position
新增的属性值sticky
。虽然这个能实现我们想要的效果,但这个属性的支持性还是需要等待一段时间。
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
以前实现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。
总结
这篇文章简单的介绍了如何在项目中实现positon:sticky
和平滑滚动条效果。除了借助JavaScript之外,我们更期待使用纯CSS来实现。我们的宗旨是:能使用CSS实现的效果绝不使用JavaScript。但碍于浏览器对其兼容性的原因,在实际项目中,可以考虑采用对应的Polyfill。这个时候,你可能会说,这样一来还不是使用JavaScript吗?事实是这样,但很多时候,咱们还可以考虑Houdini来实现。如果你对Houdini从未了解过,建议阅读@Philip Walton在2017 CSS Day分享的主题《Houdini & Polyfilling CSS》。这是一个很有意思的话题。
如需转载,烦请注明出处:https://www.w3cplus.com/css/sticky-headers.html