ホームページ > ウェブフロントエンド > jsチュートリアル > getBoundingClientRect メソッド_javascript スキルを使用して単純なスティッキー コンポーネントを実装する方法

getBoundingClientRect メソッド_javascript スキルを使用して単純なスティッキー コンポーネントを実装する方法

WBOY
リリース: 2016-05-16 15:10:03
オリジナル
1809 人が閲覧しました

sticky組件,通常應用於導航條或工具列,當網頁在某一區域滾動的時候,將導航條或工具列這類元素固定在頁面頂部或底部,方便用戶快速進行這類元素提供的操作。本文介紹這種組件的實現思路,並提供一個同時支持將sticky元素固定在頂部或底部的具體實現,由於這種組件在網站中非常常見,所以有必要掌握它的實現方式,以便在有需要的時候基於它的思路寫出功能較多的組件出來。

固定在頂部的demo效果(對應sticky-top.html):

固定在底部的demo效果(對應sticky-bottom.html):

1. 實現思路

實現這個元件的關鍵在於找到元素何時被固定以及何時被取消固定的臨界點,要找到這個臨界點,首先要詳細看看前面demo的變化過程。在前面的demo中,有一個導航條元素,也就是我們要控制固定與否的元素,我稱它為sticky元素;還有一個元素,它用來顯示網頁的一塊列表內容,這個列表元素跟sticky元素在功能上是相關的,因為sticky元素要導航的正是這個列表元素提供的內容,本文在開始介紹sticky組件的功能時,就說過sticky組件固定是發生在網頁滾動至某一區域的時候,離開這一區域就會取消固定,這個滾動區域或者說滾動範圍,就是由列表元素來決定的,所以這個列表元素是找到臨界點的關鍵,它表示sticky組件可被固定的網頁滾動範圍,為了後面引用方便,我把這個元素稱為target元素。下面就來詳細了解下前面demo的變化過程,由於固定在底部的情況與固定在頂部的情況實現思路是相通的,如果弄明白了固定在頂部的實現原理,相信你也一定能弄清楚固定在底部的實作原理,所以這裡也是為了減少篇幅,提高效率,只介紹固定在頂部的情況:

一開始sticky元素和target元素的狀態是這樣的:

當捲軸慢慢向下,使得網頁向上滾動的時候,sticky元素和target元素在一段滾動距離內狀態並沒有發生變化,一直到這個狀態(滾動條滾動距離為573px):

在這個狀態只要滾動條再往下滾動1px,sticky元素就會被固定在頂部(滾動條滾動距離為574px):

也就是說當target元素的頂部離瀏覽器頂部的距離小於0的時候(target元素的頂部未超出瀏覽器頂部的時候,距離看作大於0),sticky元素就會被固定,所以這就是我們要找的第一個臨界點。然後捲軸繼續向下捲動,只要target元素還在瀏覽器視覺區域內,sticky元素就會一直被固定:

直到這個狀態(滾動條滾動距離為1861px):

在這個狀態只要捲軸再往下滾動1px,sticky元素就會取消固定在頂部(滾動條滾動距離為1862px):


Obviously, this is the second critical point we are looking for, but its judgment condition is: when the distance between the bottom of the target element and the top of the browser is less than the height of the sticky element, the sticky element will be unfixed. The reason why the height here is less than the height of the sticky element instead of less than 0 is because for components developed based on the critical point of less than 0, the target element will almost disappear from the browser's visible area, but the sticky element is still fixed there. Effect:


sticky also covers the content of the footer. It was originally intended to facilitate user operations, but it ended up affecting user operations. Therefore, the critical point of unfixing must be advanced, and the height of the sticky element is most appropriate.

Through the previous dismantling of the demo change process, we have obtained two critical points of sticky state changes when the scroll bar scrolls all the way down:

1) When the distance between the top of the target element and the top of the browser is less than 0, the sticky element will be fixed;

2) When the distance between the bottom of the target element and the top of the browser is less than the height of the sticky element, the sticky element will be unfixed.

Based on these two critical points, it can be concluded that when the scroll bar scrolls down, the judgment condition for the fixed scroll range of the sticky element is: the distance between the top of the target element and the top of the browser is less than 0 and the bottom of the target element is The distance from the top of the browser is greater than the height of the sticky element. And this judgment condition also applies to the situation when the scroll bar scrolls upward, because when the scroll bar keeps scrolling upward, the critical point of sticky state change is:

1) When the distance between the bottom of the target element and the top of the browser is greater than the height of the sticky element, the sticky element will be fixed;

2) When the distance between the top of the target element and the top of the browser is greater than 0, the sticky element will be unfixed.

(These two critical points actually have the same meaning as the two critical points mentioned when the scroll bar scrolls down, but they are just the opposite of what is true)

So as long as you get the three values ​​[distance between the top of the target element and the top of the browser], [distance between the bottom of the target element and the top of the browser], and [height of the sticky element], you can basically implement this component. . The height of the sticky element among these three values ​​​​is determined by the design drawing. It is known from the beginning of the web page. When defining the component, we can pass it in from the outside. Although its height can also be obtained from js, obviously There is no need to add additional calculations; the other two values ​​[the distance between the top of the target element and the top of the browser] and [the distance between the bottom of the target element and the top of the browser] can be obtained by using a method provided by the DOM. This The method is: getBoundingClientRect, which is a method with good compatibility. Its calling method is:

var target = document.getElementById('main-container');
var rect = target.getBoundingClientRect();
console.log(rect);
ログイン後にコピー

Returns a ClientRect object. This object stores some information about the element box model, such as its width and height, as well as the distance between the upper and lower edges of the element box and the top edge of the browser (top and bottom), and the distance between the left and right sides. The distance between the left edge of the browser (left and right):


top and bottom are exactly what we want to get [the distance between the top of the target element and the top of the browser], [the distance between the bottom of the target element and the top of the browser], and when the top or bottom of the box does not exceed the top of the browser When , top and bottom are both values ​​greater than 0, and when the top or bottom of the box exceeds the top of the browser, top and bottom are values ​​less than 0:

When we find the three values ​​[distance between the top of the target element and the top of the browser], [distance between the bottom of the target element and the top of the browser], and [height of the sticky element], we can use code to describe it. Previous judgment conditions:

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组件的改进实现》了解更佳的实现。

関連ラベル:
ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート