고정 구성 요소는 일반적으로 웹 페이지가 특정 영역에서 스크롤될 때 사용자가 기능을 빠르게 수행할 수 있도록 페이지 상단이나 하단에 고정되어 있습니다. 그러한 요소에 의해 제공됩니다. 이 기사에서는 이 구성 요소의 구현 아이디어를 소개하고 상단 또는 하단에 고정 요소를 고정하는 것을 지원하는 특정 구현을 제공합니다. 이 구성 요소는 웹 사이트에서 매우 일반적이므로 구현 방법을 숙지해야 합니다. 그때그때 그 아이디어를 바탕으로 더 많은 기능을 갖춘 컴포넌트가 작성될 예정입니다.
상단에 고정된 데모 효과(sticky-top.html에 해당):
하단에 고정된 데모 효과(sticky-bottom.html에 해당):
1. 구현 아이디어
이 컴포넌트 구현의 핵심은 요소가 수정되는 시점과 수정되지 않는 시점의 임계점을 찾는 것입니다. 이 임계점을 찾으려면 먼저 이전 데모의 변경 프로세스를 자세히 살펴봐야 합니다. 이전 데모에는 고정 여부를 제어하려는 요소인 탐색 모음 요소가 있습니다. 웹의 목록 콘텐츠를 표시하는 데 사용되는 요소도 있습니다. 이 목록 요소 뒤에는 고정 요소가 기능적으로 관련되어 있습니다. 왜냐하면 고정 요소는 이 목록 요소가 제공하는 콘텐츠를 정확하게 탐색하기 때문입니다. 이 기사에서 고정 구성 요소의 기능을 소개하기 시작했을 때 웹 페이지가 특정 영역으로 스크롤할 때 고정된 구성 요소가 발생합니다. 이 영역을 벗어나면 고정되지 않습니다. 이 스크롤 영역 또는 스크롤 범위는 목록 요소에 의해 결정되므로 이 목록 요소는 임계점을 찾는 열쇠입니다. 나중에 참고할 수 있도록 이 요소를 대상 요소라고 부릅니다. 이전 데모의 변경 과정을 자세히 살펴보겠습니다. 하단에 고정하는 구현 아이디어와 상단에 고정하는 구현 아이디어는 동일하므로 상단에 고정하는 구현 원리를 이해한다면 여러분도 그럴 것이라고 믿습니다. 하단의 구현 원리를 이해할 수 있으므로 길이를 줄이고 효율성을 높이기 위해 상단에 고정하는 상황만 소개합니다.
sticky 요소와 대상 요소의 초기 상태는 다음과 같습니다.
스크롤바가 천천히 아래쪽으로 이동하여 웹페이지가 위쪽으로 스크롤되면 이 상태가 될 때까지 고정 요소와 대상 요소의 상태가 특정 스크롤 거리 내에서 변경되지 않습니다(스크롤바의 스크롤 거리는 573px). :
이 상태에서 스크롤바가 1px 아래로 스크롤되는 한 고정 요소는 상단에 고정됩니다(스크롤바 스크롤 거리는 574px).
즉, 대상 요소의 상단과 브라우저 상단 사이의 거리가 0보다 작은 경우(대상 요소의 상단이 브라우저 상단을 초과하지 않는 경우 거리는 0으로 간주됩니다. 0보다 큼) 끈적한 요소가 고정되므로 이것이 우리가 찾고 있는 첫 번째 중요한 점입니다. 그런 다음 스크롤 막대는 계속해서 아래로 스크롤됩니다. 대상 요소가 브라우저의 표시 영역 내에 있는 한 고정 요소는 고정된 상태로 유지됩니다.
이 상태까지(스크롤바 스크롤 거리는 1861px):
이 상태에서 스크롤바를 1px 아래로 스크롤하면 고정 요소가 상단에서 고정 해제됩니다(스크롤바 스크롤 거리는 1862px).
분명히 이것이 우리가 찾고 있는 두 번째 중요한 점이지만 판단 조건은 대상 요소의 하단과 브라우저 상단 사이의 거리가 끈적한 요소의 높이보다 작을 때 끈적끈적한 요소입니다. 요소는 고정 해제됩니다. 여기서 높이가 0보다 작은 것이 아니라 끈끈한 요소의 높이보다 작은 이유는 0보다 작은 임계점을 기반으로 개발된 컴포넌트의 경우 대상 요소가 브라우저의 가시 영역에서 거의 사라지지만 끈끈한 요소의 높이보다 작기 때문입니다. 요소는 여전히 고정되어 있습니다. 효과:
sticky는 바닥글의 내용도 포함합니다. 원래는 사용자 작업을 용이하게 하기 위한 것이지만 결과적으로 사용자 작업에 영향을 미치게 되므로 고정 해제의 임계점을 높여야 하며, 끈적끈적 요소의 높이가 가장 적절합니다. .
이전의 데모 변경 프로세스 해체를 통해 스크롤 막대가 완전히 아래로 스크롤될 때 고정 상태 변경의 두 가지 중요한 지점을 얻었습니다.
1) 대상 요소 상단과 브라우저 상단 사이의 거리가 0보다 작으면 고정 요소가 고정됩니다.
2) 대상 요소의 하단과 브라우저 상단 사이의 거리가 고정 요소의 높이보다 작으면 고정 요소가 고정 해제됩니다.
이 두 가지 중요한 점을 토대로 스크롤 막대가 아래로 스크롤될 때 고정 요소의 고정 스크롤 범위에 대한 판단 조건은 대상 요소의 상단과 대상 요소의 상단 사이의 거리라는 결론을 내릴 수 있습니다. browser 는 0보다 작고 대상 요소의 하단은 브라우저 상단으로부터의 거리가 고정 요소의 높이보다 큽니다. 그리고 이 판단 조건은 스크롤 바가 위로 스크롤되는 상황에도 적용됩니다. 왜냐하면 스크롤 바가 계속 위로 스크롤될 때 끈적한 상태 변화의 임계점은 다음과 같습니다.
1) 대상 요소의 하단과 브라우저 상단 사이의 거리가 고정 요소의 높이보다 크면 고정 요소가 고정됩니다.
2) 대상 요소 상단과 브라우저 상단 사이의 거리가 0보다 크면 고정 요소가 고정 해제됩니다.
(이 두 가지 중요한 사항은 실제로 스크롤바를 아래로 스크롤할 때 언급된 두 가지 중요한 사항과 동일한 의미를 갖지만 사실과 정반대입니다)
따라서 [대상 요소 상단과 브라우저 상단 사이의 거리], [대상 요소 하단과 브라우저 상단 사이의 거리], [ 스티키 요소의 높이]를 사용하면 기본적으로 이 구성 요소를 구현할 수 있습니다. 이 세 가지 값 중 끈끈한 요소의 높이는 디자인 도면에 의해 결정되며, 컴포넌트를 정의할 때 외부에서 전달할 수도 있습니다. 또한 js에서도 얻을 수 있습니다. 다른 두 값(대상 요소 상단과 브라우저 상단 사이의 거리)과 [대상 요소 하단 사이의 거리]를 추가로 계산할 필요는 없습니다. 및 브라우저 상단]은 DOM에서 제공하는 메소드를 사용하여 얻을 수 있습니다. 이 메소드는 getBoundingClientRect이며 호출 메소드는
입니다.var target = document.getElementById('main-container'); var rect = target.getBoundingClientRect(); console.log(rect);
ClientRect 객체를 반환합니다. 이 객체는 너비, 높이, 요소 상자의 위쪽 및 아래쪽 가장자리와 브라우저 위쪽 가장자리(상단) 사이의 거리 등 요소 상자 모델에 대한 일부 정보를 저장합니다. 및 하단), 왼쪽과 오른쪽 사이의 거리(왼쪽과 오른쪽):
상단과 하단은 정확히 우리가 얻고자 하는 [대상 요소의 상단과 브라우저 상단 사이의 거리], [대상 요소의 하단과 브라우저 상단 사이의 거리]입니다. 박스의 상단이나 하단이 브라우저의 상단을 넘지 않는 경우 상단과 하단이 모두 0보다 큰 값이고, 박스의 상단이나 하단이 브라우저 상단을 초과하는 경우 상단과 하단은 0보다 작은 값:
[타겟 요소의 상단과 브라우저 상단 사이의 거리], [타겟 요소의 하단과 브라우저 상단 사이의 거리], [높이]의 세 가지 값을 찾으면 끈적한 요소], 이전 판단 조건을 설명하기 위해 코드를 사용할 수 있습니다.
rect.top < 0 && (rect.bottom - stickyHeight) > 0;
(rect表示target元素调用getBoundingClientRect返回的对象,stickyHeight表示sticky元素的高度)
最后为了让实现思路更加完整,虽然不详细介绍固定在底部的情况的变化过程,我还是把这种情况的临界点跟判断方式补充进来,它的临界点是(这里列的是滚动条向下滚动时的临界点):
1)当target元素的顶部离浏览器顶部的距离 + sticky元素的高度 小于浏览器可视区域的高度时,sticky元素被固定;
2)当target元素的底部离浏览器的顶部的距离小于浏览器可视区域的高度时,sticky元素被取消固定。
浏览器可视区域的高度,可用document.documentElement.clientHeight来获取,这个属性也是没有兼容性问题的,判断代码为:
var docClientWidth = document.documentElement.clientHeight; rect.bottom > docClientWidth && (rect.top + stickyHeight) < docClientWidth;
2. 实现细节
1)html结构
固定在顶部的html结构:
<div class="container-fluid sticky-wrapper"> <ul id="sticky" data-target="#main-container" class="sticky nav nav-pills"> <li role="presentation" class="active"><a href="#">Home</a></li> <li role="presentation"><a href="#">Profile</a></li> <li role="presentation"><a href="#">Messages</a></li> </ul> </div> <div id="main-container" class="container-fluid"> <div class="row"> ... </div> ... </div>
固定在底部的html结构:
<div id="main-container" class="container-fluid"> <div class="row"> ... </div> ... </div> <div class="container-fluid sticky-wrapper"> <ul id="sticky" data-target="#main-container" class="sticky nav nav-pills"> <li role="presentation" class="active"><a href="#">Home</a></li> <li role="presentation"><a href="#">Profile</a></li> <li role="presentation"><a href="#">Messages</a></li> </ul> </div>
以上#main-container就是我们的target元素,#sticky就是我们的sticky元素,还需要注意两点:
a. 顺序问题,两种结构中,target元素与sticky的父元素顺序位置是反的;
b. sticky元素外面必须包裹一层元素,而且还得给这一层元素设置height属性:
.sticky-wrapper { margin-bottom: 10px; height: 52px; }
这是因为当sticky元素被固定的时候,它会脱离普通文档流,所以要利用它的父元素把sticky元素的高度在普通文档流中撑起来,以免在固定效果出现的时候,target元素的内容出现跳动的情况。
2)固定效果
让一个元素固定在浏览器的某个位置,当然是通过position: fixed来弄,所以可以用两个css类来实现固定在顶部和固定在底部的效果:
.sticky--in-top,.sticky--in-bottom { position: fixed; z-index: 1000; } .sticky--in-top { top: 0; } .sticky--in-bottom { bottom: 0; }
当我们判断元素需要被固定在顶部的时候,就给它添加.sticky--in-top的css类;当我们判断元素需要被固定在底部的时候,就给它添加.sticky--in-bottom的css类。
3)滚动回调
控制sticy元素固定的逻辑显然要写在window的scroll事件回调中(有了前面对实现思路以及判断条件的说明,相信理解下面这段代码应该会很容易):
固定在顶部的回调逻辑:
$(window).scroll(function() { var rect = $target[0].getBoundingClientRect(); if (rect.top < 0 && (rect.bottom - stickyHeight) > 0) { !$elem.hasClass('sticky--in-top') && $elem.addClass('sticky--in-top').css('width', stickyWidth + 'px'); } else { $elem.hasClass('sticky--in-top') && $elem.removeClass('sticky--in-top').css('width', 'auto'); } });
其中:$target是target元素的jq对象,$elem是sticky元素的jq对象,stickyHeight是sticky元素的高度,stickyWidth是sticky元素的宽度。由于sticky元素固定时,脱离原来的文档流,需要设置宽度才能显示跟固定前一样的宽度。
固定在底部的回调逻辑:
$(window).scroll(function() { var rect = $target[0].getBoundingClientRect(), docClientWidth = document.documentElement.clientHeight; if (rect.bottom > docClientWidth && (rect.top + stickyHeight) < docClientWidth) { !$elem.hasClass('sticky--in-bottom') && $elem.addClass('sticky--in-bottom').css('width', stickyWidth + 'px'); } else { $elem.hasClass('sticky--in-bottom') && $elem.removeClass('sticky--in-bottom').css('width', 'auto'); } });
这里是为了把回调逻辑说的更清楚才把代码分成两份,最后给的实现会把这两个代码合并成一份:)
4)函数节流
函数节流通常应用于window的scroll事件,resize事件以及普通元素的mousemove事件,因为这些事件由于鼠标或滚轮操作很频繁,会导致回调连续触发,如果回调里面含有DOM操作,这种连续调用就会影响页面的性能,所以很有必要控制这类回调的执行次数,函数节流就是做这个的,我这里提供了一个很简单的函数节流实现:
function throttle(func, wait) { var timer = null; return function() { var self = this, args = arguments; if (timer) clearTimeout(timer); timer = setTimeout(function() { return typeof func === 'function' && func.apply(self, args); }, wait); } }
这个函数可以控制func所指定的函数,执行的间隔指定为wait指定的毫秒数,利用它,我们可以把前面的滚动回调改动一下,比如固定在顶部的情况改成:
$(window).scroll(throttle(function() { var rect = $target[0].getBoundingClientRect(), docClientWidth = document.documentElement.clientHeight; if (rect.bottom > docClientWidth && (rect.top + stickyHeight) < docClientWidth) { !$elem.hasClass('sticky--in-bottom') && $elem.addClass('sticky--in-bottom').css('width', stickyWidth + 'px'); } else { $elem.hasClass('sticky--in-bottom') && $elem.removeClass('sticky--in-bottom').css('width', 'auto'); } }, 50);
其实真正处理回调的是throttle返回的函数,这个返回的函数逻辑少,而且没有DOM操作,它是会被连续调用的,但是不影响页面性能,而我们真正处理逻辑的那个函数,也就是传入throttle的那个函数因为throttle创建的闭包的作用,不会被连续调用,这样就实现了控制函数执行次数的目的。
5)resize的问题
window resize总是在定义组件的时候带来问题,因为页面可视区域的宽高度发生了变化,sticky元素的父容器宽度也可能发生了变化,而且resize的时候不会触发scroll事件,所以我们需要在resize回调内,刷新sticky元素的宽度以及重新调用固定效果的逻辑,这个相关的代码就不贴出来了,后面直接看整体实现吧,否则我怕放出来会影响理解。总之resize是我们在定义组件的时候肯定要考虑的,不过一般都放到最后来处理,有点算处理BUG之类的工作。
3. 整体实现
代码比较简洁:
/** * @param elem: jquery选择器,用来获取要被固定的元素 * @param opts: * - target: jquery选择器,用来获取表示固定范围的元素 * - type: top|bottom,表示要固定的位置 * - height: 要固定的元素的高度,由于高度在做页面时就是确定的并且几乎不会被DOM操作改变,直接从外部传入可以除去获取元素高度的操作 * - wait: 滚动事件回调的节流时间,控制回调至少隔多长时间才执行一次 * - getStickyWidth:获取要固定元素的宽度,window resize或者DOM操作会导致固定元素的宽度发生变化,需要这个回调来刷新stickyWidth */ var Sticky = function (elem, opts) { var $elem = $(elem), $target = $(opts.target || $elem.data('target')); if (!$elem.length || !$target.length) return; var stickyWidth, $win = $(window), stickyHeight = opts.height || $elem[0].offsetHeight, rules = { top: function (rect) { return rect.top < 0 && (rect.bottom - stickyHeight) > 0; }, bottom: function (rect) { var docClientWidth = document.documentElement.clientHeight; return rect.bottom > docClientWidth && (rect.top + stickyHeight) < docClientWidth; } }, type = (opts.type in rules) && opts.type || 'top', className = 'sticky--in-' + type; refreshStickyWidth(); $win.scroll(throttle(sticky, $.isNumeric(opts.wait) && parseInt(opts.wait) || 100)); $win.resize(throttle(function () { refreshStickyWidth(); sticky(); }, 50)); function refreshStickyWidth() { stickyWidth = typeof opts.getStickyWidth === 'function' && opts.getStickyWidth($elem) || $elem[0].offsetWidth; $elem.hasClass(className) && $elem.css('width', stickyWidth + 'px'); } //效果实现 function sticky() { if (rules[type]($target[0].getBoundingClientRect())) { !$elem.hasClass(className) && $elem.addClass(className).css('width', stickyWidth + 'px'); } else { $elem.hasClass(className) && $elem.removeClass(className).css('width', 'auto'); } } //函数节流 function throttle(func, wait) { var timer = null; return function () { var self = this, args = arguments; if (timer) clearTimeout(timer); timer = setTimeout(function () { return typeof func === 'function' && func.apply(self, args); }, wait); } } };
调用方式,固定在顶部的情况(type选项默认为top):
<script> new Sticky('#sticky',{ height: 52, getStickyWidth: function($elem){ return ($elem.parent()[0].offsetWidth - 30); } }); </script>
固定在底部的情况:
<script> new Sticky('#sticky',{ height: 52, type: 'bottom', getStickyWidth: function($elem){ return ($elem.parent()[0].offsetWidth - 30); } }); </script>
还有一个要说明的是,opts的getStickyWidth选项,这个回调用来获取sticky元素的宽度,为什么要把它放出来,通过外部去获取宽度,而不是在组件内部通过offsetWidth获取?是因为当sticky元素的外部容器是自适应的时候,sticky元素固定时的宽度不是由sticky元素自己决定的,而是依赖于外部容器的宽度,所以这个宽度只能在外部去获取,内部获取不准确。比如上面的代码中我减了一个30,如果在组件内部获取的话,我肯定不知道要添加减30这样一个逻辑。
4. 总结
本文提供了一个很常见的sticky组件实现,实现这个组件的关键在于找到控制sticky元素固定与否的关键点,同时在实现的时候函数节流跟window resize的问题需要特别注意。
我一直认为对于一些简单的组件,掌握它的思路,自己去定义比直接从github上去找开源的插件要来的更切实际:
1)代码可控,不用去阅读别人的代码,有问题也能快速修改
2)代码量小,开源的插件会尽可能多做事,而有些工作你的项目并不一定需要它去做;
3)更贴合项目的实际需求,跟第2点差不多的意思,在已有的思路基础上,我们能开发出与项目需求完全契合的功能模块;
4)有助于提高自己的技术水平,增进知识的广度和深度;
所以有能力造轮子的时候,造造也是很有必要的。
本文虽然在最后提供了整体的组件实现,但是并不是建议拿来就用,否则前面大篇幅地去介绍实现思路就没有必要了,我只要放个github地址即可,思路远比实现重要。我最近几篇博客都是在分享思路,而不是分享某个具体的实现,思路这种抽象的东西是通用的,理解前它不是你的,理解后它就存在于脑袋里,任何时候都可以拿来就用,我提供的思路也同样来自于我对其它博客其它插件源码学习之后的思考与总结。
补充于说明:
本文实现有不足,不完美的地方,请在了解本文相关内容后,移步阅读《sticky组件的改进实现》了解更佳的实现。