利用getBoundingClientRect方法實現簡潔的sticky組件

来源:http://www.cnblogs.com/lyzg/archive/2016/03/16/5278516.html
-Advertisement-
Play Games

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


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

代碼下載

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

demo1

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

demo2

1. 實現思路

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

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

image

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

image

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

image

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

image

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

image

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

image

顯然,這就是我們要找的第2個臨界點,不過它的判斷條件是:當target元素的底部離瀏覽器頂部的距離小於sticky元素的高度時,sticky元素就會被取消固定。這裡為什麼是小於sticky元素的高度,而不是小於0,原因是因為基於小於0這個臨界點開發出來的組件,會出現target元素幾乎快從瀏覽器可視區域消失了,但是sticky元素還固定在那的效果:

image

sticky還把footer的內容給蓋住了,本來是為了方便用戶操作,結果影響了用戶操作,所以得把取消固定這個臨界點提前,而用sticky元素的高度最合適。

通過前面對demo變化過程的拆解,我們已經得到了滾動條一直向下滾動時,sticky狀態變化的兩個臨界點:

1)當target元素的頂部離瀏覽器頂部的距離小於0的時候,sticky元素就會被固定;

2)當target元素的底部離瀏覽器頂部的距離小於sticky元素的高度時,sticky元素就會被取消固定。

綜合這兩個臨界點,可以得出滾動條向下滾動時,sticky元素被固定的滾動範圍的判斷條件是:target元素的頂部離瀏覽器頂部的距離小於0 並且 target元素的底部離瀏覽器頂部的距離大於sticky元素的高度。而且這個判斷條件,同樣適用於滾動條向上滾動的情況因為滾動條一直向上滾動時,sticky狀態變化的臨界點是:

1)當target元素的底部離瀏覽器頂部的距離大於sticky元素的高度時,sticky元素就會被固定;

2)當target元素的頂部離瀏覽器頂部的距離大於0的時候,sticky元素就會被取消固定。

(這兩個臨界點,其實跟滾動條向下滾動時提到的兩個臨界點,是一個意思,只不過是正話反著說而已)

所以只要得到【target元素的頂部離瀏覽器頂部的距離】,【target元素的底部離瀏覽器頂部的距離】,【sticky元素的高度】這三個值基本上就能實現這個組件了。這三個值中sticky元素的高度由設計圖決定,它從網頁一開始製作就是已知的,在定義組件的時候我們可以從外部傳進去,雖然也能從js去獲取它的高度,不過顯然沒有必要增加額外的計算;另外兩個值【target元素的頂部離瀏覽器頂部的距離】,【target元素的底部離瀏覽器頂部的距離】,我們正好可以利用DOM提供的一個方法來獲取,這個方法是:getBoundingClientRect,這是一個相容性很好的方法,它的調用方式是:

var target = document.getElementById('main-container');
var rect = target.getBoundingClientRect();
console.log(rect);

返回一個ClientRect對象,這個對象存儲元素框模型的一些信息,比如它的寬高度(width and height),以及元素框上下邊距離瀏覽器頂部邊緣的距離(top and bottom),左右邊距離瀏覽器左邊緣的距離(left and right):

image

無標題

top跟bottom恰恰就是我們要獲取的【target元素的頂部離瀏覽器頂部的距離】,【target元素的底部離瀏覽器頂部的距離】,而且當框的頂部或底部未超出瀏覽器頂部的時候,top跟bottom都是大於0的值,而當框的頂部或底部超出瀏覽器頂部的時候,top跟bottom是小於0的值:

image

當我們找到了【target元素的頂部離瀏覽器頂部的距離】,【target元素的底部離瀏覽器頂部的距離】,【sticky元素的高度】這三個值,就可以用代碼來描述前面的判斷條件:

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地址即可,思路遠比實現重要。我最近幾篇博客都是在分享思路,而不是分享某個具體的實現,思路這種抽象的東西是通用的,理解前它不是你的,理解後它就存在於腦袋裡,任何時候都可以拿來就用,我提供的思路也同樣來自於我對其它博客其它插件源碼學習之後的思考與總結。

最後希望本文能給你帶來一些收穫,謝謝閱讀:)

代碼下載


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 想了很久不知道怎麼入手 沒思路 不會寫    後來直接說不會寫   又給我出了一個 2+(3*2)+2 實現計算器輸入 輸出結果 讓我手寫出程式  蒙了 一塊面試的幾個人都沒做出面試題來 很是受打擊啊 醉了
  • 這是一本框架性的書,它從分散式網站架構設計需要考慮的內容出發,介紹了相關的技術。雖然每一部分講解不是特別深入,但是原理清晰,案例豐富,非常不錯。 現在的網站都越做越大,分散式是必須的選擇,通過這本書我看到了淘寶是怎麼做的,對於今後的工作很有幫助。 作者也是一位非常年輕的工程師,很不錯
  • 併發思想提煉(1)(理解併發,避免死鎖) 一直做伺服器後端和基礎組件平臺開發,常常用到併發,故簡單放些乾貨,一來算是總結,二來希望後人少走彎路, 寫到哪兒算哪兒,不定期更新。 1.    Introduction 先來明白一些概念。Concurrency併發和Multi-thread多線程不同 你在
  • 觀察者(observer)模式定義了一對多的依賴關係,讓多個觀察者對象能夠同時監聽某一主題對象。這個主題對象中的狀態發生改變時,就會通知所有的觀察者對象。 觀察者模式的結構圖: 結構中各個部分的含義: 源代碼: 抽象主題類(Subject):   具體主題類(ConcreteSubject):  
  • 1.ServiceStack服務擁有自身的容器—Funq.Container 當我們使用介面註入的方式調用底層方法時,我們需要在AppHost中重寫Configure(Funq.Container container)方法,在方法中添加container.RegisterAutoWiredAs<T,
  • 可以通過http://htmlpreview.github.io/這個網站,直接線上預覽html頁面。 ↓ ↓ 可以發現:這個網站直接將github上的頁面地址當做參數來傳遞。
  • 上海樂司凱信息科技有限公司 是上海邁伊茲咨詢有限公司的子公司 地址是在臨空經濟區,攜程那路,北翟路外環線 稅前10k 20k,說是有牛人30k也能承受,14薪 有意向留言好了 1、精通HTML/HTML5/XHTML,CSS,精通運用CSS+DIV佈局,可使用HTML5、CSS3、JavaScrip
  • 這是一款基於segment.js製作的非常有創意的分段式SVG文字動畫特效。這個文字動畫特效通過動畫SVG的描邊路徑來製作各種文字的動畫效果,效果非常的贊。 這個SVG文字動畫特效的第一個DEMO中的最後幾個例子使用了mo.js插件,一款由Oleg Solomka編寫的用於製作網頁圖形動畫的Java
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...