這是我寫的關於列表組件的第4篇博客。前面的相關文章有: 1. 列表組件抽象(1)-概述 2. 列表組件抽象(2)-listViewBase說明 3. 列表組件抽象(3)-分頁和排序管理說明 本文介紹列表組件中我對滾動列表及滾動分頁的實現思路。 在pc端,通過滾動進行翻頁的需求非常常見;移動端也是,只 ...
這是我寫的關於列表組件的第4篇博客。前面的相關文章有:
1. 列表組件抽象(1)-概述
本文介紹列表組件中我對滾動列表及滾動分頁的實現思路。
在pc端,通過滾動進行翻頁的需求非常常見;移動端也是,只不過移動端由於scroll事件觸發有延遲,必須等到屏幕停止滑動後才會觸發,而不是在用戶的手指離開屏幕就立即觸發,所以移動端最好是不用scroll事件直接做滾動翻頁,而是用iscroll這類插件提供更實時的scroll事件更好。
不管是pc還是移動端,滾動翻頁列表的特點都是差不多的:
1)基本上由以下幾個部分組成:數據列表,頂部的載入中提示,底部的載入中提示,沒有更多了,沒有找到記錄。正是按照這個思路,所以我把滾動列表的html結構設計成:
2)跟其它列表組件不同的是,滾動列表在請求新的數據後,有2種方式來渲染新的數據。一種是跟其它列表組件一樣,直接把原來的列表內容替換;另一種是將新數據追加在原有的列表內容之後。第1種通常用於直接更改列表的查詢條件時使用;第2種用於翻頁查詢或者刷新操作。
3)在前面的幾個部分中,有兩個載入中的提示,都是用來提升用戶體驗的東西。頂部載入提示用於條件查詢,底部載入提示用於翻頁查詢。從它們在html中的位置也能看出來。
4)載入更多的按鈕,一是防止滾動事件失效而準備的,二是有些場景可能會禁用掉滾動翻頁,所以就要提供直接點擊按鈕的手工翻頁。
5)沒有更多了這個部分,在翻頁查詢後,根據數據結果判斷沒有更多的數據時顯示。
6)沒有找到記錄的這個部分用於在列表首次查詢時,如果數據為空時顯示。
7)當通過滾動或者滑動操作,使得滾動列表隱藏於可視區域之下的部分不斷往上滾動,併在達到某一個臨界點的時候,觸發翻頁查詢,將下一頁的數據追加到數據列表後面進行顯示。
針對以上的這些需求邏輯,考慮pc端和移動端的場景,我寫了兩個組件分別用於實現滾動列表。同時與這兩個列表組件一起使用的還有另外兩個分頁組件,它們兩兩之間是配套使用的。
首先是用於實現pc端,可相對window或者某個DOM元素進行滾動分頁的列表組件scrollListView以及它配套的分頁組件scrollPageView組件,源碼分別是:
https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/scrollListView.js
https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/scrollPageView.js
然後是用於移動端,結合iscroll一起使用的iscrollListView和iscrollPageView組件,源碼分別是:
https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/iscrollListView.js
https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/iscrollPageView.js
針對以上組件有以下demo可以查看相關功能演示:
pc端相對window滾動分頁demo:http://liuyunzhuge.github.io/blog/form/dist/html/listView_2.html
pc端相對某個DOM元素滾動分頁demo:http://liuyunzhuge.github.io/blog/form/dist/html/listView_3.html
移動端滾動分頁demo:http://liuyunzhuge.github.io/blog/form/dist/html/listView_4.html
後面的部分說明以上組件的要點。不過由於iscrollListView直接繼承了scrollListView,實現非常簡單;iscrollPageView的實現思路也跟scrollPageView差不多。所以後面只介紹scrollListView和scrollPageView的相關內容。
先來看scrollListView.js。
首先,代碼結構還是跟前幾篇博客介紹的組件都差不多,所以這裡不再覆述。defaults是這樣定義的:
var DEFAULTS = $.extend({}, ListViewBase.DEFAULTS, { //列表容器的選擇器 dataListSelector: '.data_list', //頂部載入中的html loadingTopHtml: '<div class="loading_top">載入中...</div>', //沒有結果的html noContentHtml: '<div class="no_content">沒有找到相關記錄:(</div>', //底部載入中的html loadingBottomHtml: '<div class="loading_bottom">載入中...</div>', //沒有更多的html noMoreHtml: '<div class="no_more">沒有更多了</div>', //載入更多的html loadMoreHtml: '<a href="javascript:;" class="btn_load_more">載入更多</a>' });
主要是用來定義滾動列表的那幾個組成部分。如果不想用預設值,那麼在實例化組件的時候,傳入想要設置的option就行了。
scrollListView繼承了listViewBase,為了增加自己的初始化邏輯,所以用到了initMiddle這個模板方法,併在其中做了一些jq對象緩存,以及內部狀態管理初始化的邏輯:
initMiddle: function () { var opts = this.options, $element = this.$element, $data_list = this.$data_list = $element.find(opts.dataListSelector), $load_more = this.$load_more = $(opts.loadMoreHtml).appendTo($element), $no_content = $(opts.noContentHtml).appendTo($element), $loading_top = $(opts.loadingTopHtml).prependTo($element), $loading_bottom = $(opts.loadingBottomHtml).appendTo($element), $no_more = $(opts.noMoreHtml).appendTo($element); $load_more.css('display', 'block'); //狀態管理:初始化完畢,頂部載入中,底部載入中,沒有結果,沒有更多,載入完畢 var states = this.states = { init: function () { $data_list.show(); $load_more.hide(); $no_content.hide(); $loading_top.hide(); $loading_bottom.hide(); $no_more.hide(); }, loading_top: function () { $data_list.show(); $load_more.hide(); $no_content.hide(); $loading_top.show(); $loading_bottom.hide(); $no_more.hide(); }, loading_bottom: function () { $data_list.show(); $load_more.hide(); $no_content.hide(); $loading_top.hide(); $loading_bottom.show(); $no_more.hide(); }, no_content: function () { $data_list.hide(); $load_more.hide(); $no_content.show(); $loading_top.hide(); $loading_bottom.hide(); $no_more.hide(); }, loaded: function () { $data_list.show(); $load_more.show(); $no_content.hide(); $loading_top.hide(); $loading_bottom.hide(); $no_more.hide(); }, no_more: function () { $data_list.show(); $load_more.hide(); $no_content.hide(); $loading_top.hide(); $loading_bottom.hide(); $no_more.show(); }, 'set': function (action) { this.curState = action; this[action](); }, isNomore: function () { return this.curState == 'no_more'; }, isNoContent: function () { return this.curState == 'no_content'; } }; states.set('init'); },
以上代碼中的那個states對象用來實現內部的狀態管理,可以看作一個簡單的狀態機。採用這個做法的原因,一是為了滿足最前面介紹滾動列表組件特點時描述的那些需求邏輯,二是為了讓這些UI控制邏輯看起來更清晰。有了它,我只要在何時的時機,改變下組件的狀態,就能列表組件顯示不同的內容了。比較簡單好理解。
然後通過createPageView來實現分頁組件的初始化邏輯。這裡就得使用scrollPageView來實例化了:
createPageView: function () { var pageView, opts = this.options; if (opts.pageView) { //初始化分頁組件 delete opts.pageView.onChange; pageView = new ScrollPageView($.extend(opts.pageView, { $loadMore: this.$load_more, $element: this.$element })); } return pageView; },
然後把scrollPageView需要的幾個dom對象以option的形式傳給了它。
考慮到滾動列表組件的特殊性,我還用到了listViewBase的其它幾個模板方法來實現滾動列表的需求。
首先是beforeQuery:
beforeQuery: function (clear) { //如果clear為true,則顯示頂部的載入狀態,表示正在進行新條件的列表查詢 //否則顯示底部的載入狀態,表示正在進行翻頁查詢 this.states.set(clear ? 'loading_top' : 'loading_bottom'); },
這個方法會接受一個參數clear,為true則表示進行條件查詢,否則表示進行翻頁查詢。這個方法的作用在於查詢前顯示載入提示。
然後是querySuccess:
querySuccess: function (html, args) { var pageView = this.pageView, rows = this.originalRows, method = args.clear ? 'html' : 'append';//根據查詢類型,來決定要如何處理渲染新的數據 //沒有查到結果 if (rows.length == 0 && pageView.pageIndex == 1) { this.states.set('no_content'); return; } //沒有更多 if (rows.length < pageView.pageSize) { this.states.set('no_more'); html.length && this.$data_list[method](html); return; } //載入完畢 this.states.set('loaded'); this.$data_list[method](html); },
它用來實現請求成功的後的邏輯,最重要的當然是渲染數據。但是考慮到列表組件的需求,還得根據多方面的參數,判斷該把列表設置為什麼樣的狀態。請求成功後的結果無非三種,沒有查到數據,沒有更多,載入成功。這三個狀態,根據分頁信息和記錄數就能判斷清楚,見源碼裡面if邏輯的寫法。
然後是queryError:
queryError: function () { this.states.set('loaded'); },
這個主要是在請求失敗的時候,還原列表的狀態而已。
最後是afterQuery:
afterQuery: function () { if (this.states.isNoContent() || this.states.isNomore()) { this.pageView.disable(); } }
它在請求完成之後,判斷如果是沒有數據或者沒有更多的狀態的話,就禁用掉分頁組件,免得用戶操作不慎導致還會發出一些查不到數據的請求。
以上就是scrollListView實現的核心了,只有100多行。
再來看scrollPageView。
它的defaults定義如下:
var DEFAULTS = $.extend({}, PageViewBase.DEFAULTS, { //載入更多的按鈕 $loadMore: null, //滾動元素 $element: null, //滾動區域的目標元素,如果沒有傳,預設就是window對象,用來註冊scroll事件 $target: null, //是否啟用滾動翻頁 scrollPage: true, //滾動元素底邊跟滾動可視區域底邊的距離,它是滾動翻頁的臨界點 offset: -100, //滾動事件的綁定時的延遲時間 scrollBindDelay: 0, //滾動事件的節流間隔 throttle: 100, });
應該好理解。offset的作用後面會繼續說明,scrollBindDelay是用來延遲滾動事件綁定的。為啥會搞這個,是因為chrome瀏覽器有個特性,如果在瀏覽網頁的時候,刷新之後,滾動條會恢復到刷新前瀏覽的位置,並且它這個自動恢復也會觸發滾動事件。那麼當列表組件初始化完畢之後,很有可能會發出兩次查詢請求,一次是由初始化調用發出的,一次是由自動恢復的滾動事件發出的。所以加上這個option,有利於控制列表初始化後的首次請求。$loadMore用於註冊點擊事件,添加手動翻頁的邏輯。$element表示滾動列表相關的dom對象。$target表示滾動相對的目標對象,如果不傳,就指向window對象。
scrollPageView內部提供了簡單的節流函數來做滾動事件回調的節流控制:
//簡單函數節流 function throttle(func, interval) { var last = Date.now(); return function () { var now = Date.now(); if ((now - last) > interval) { func.apply(this, arguments); last = now; } } }
也提供了獲取css屬性在瀏覽器重繪之後的值的函數:
//用來獲取css某個屬性經過瀏覽器重繪之後的值 function getComputedValue(element, prop) { var computedStyle = window.getComputedStyle(element, null); if (!computedStyle) return null; if (computedStyle.getPropertyValue) { return computedStyle.getPropertyValue(prop); } else if (computedStyle.getAttribute) { return computedStyle.getAttribute(prop); } else if (computedStyle[prop]) { return computedStyle[prop]; } }
其它代碼倒是沒啥好補充的,重點看下滾動事件相關的翻頁控制邏輯,我就只貼了相關的匿名函數代碼了:
function () { if (that.disabled) return; var targetHeight, bottom; //目標元素的clientHeight作為滾動區域的高度 //bottom:滾動元素的底邊到滾動區域頂邊的距離 if (!opts.$target) { targetHeight = document.documentElement.clientHeight; bottom = opts.$element[0].getBoundingClientRect().bottom; } else { targetHeight = opts.$target[0].clientHeight; var targetRect = opts.$target[0].getBoundingClientRect(), targetBorderTop = parseInt(getComputedValue(opts.$target[0], 'border-top-width')), elemRect = opts.$element[0].getBoundingClientRect(); //如果target是其它的html元素,由於都是採用boundingClientRect來計算的,所以要減去目標元素頂部邊框的寬度,這樣才不會有誤差 bottom = elemRect.bottom - targetRect.top - (isNaN(targetBorderTop) ? 0 : targetBorderTop); } //bottom+ opts.offset等於一條臨界線 //如果opts.offset小於0,那麼這條臨界線就位於滾動元素底邊再往上opts.offsets的距離的位置 //如果Opts.offset大於0,那麼這條臨界線就位於滾動元素底邊再往下|opts.offsets|的絕對距離的位置 //翻頁觸發的條件是:這條臨界線剛好出現在滾動區域裡面的時候 if ((bottom + opts.offset) < targetHeight) { pageIndexChange(that.pageIndex + 1, that); } }
以上的思路也比較簡單,只要判斷列表元素的底部跟目標對象的可視區域的底部達到臨界距離即可,這個臨界距離就是defaults中定義的offset值。以相對window滾動為例說明如何來做這個判斷:
根據上圖,可以得知翻頁臨界的判斷條件就是上圖中臨界線到目標對象可視區域頂邊的距離小於目標對象可視區域的高度。這個圖雖然是以相對window的滾動情況來說明問題的,但是相對DOM對象進行滾動的判斷方式跟這個是一模一樣的,只要我們能夠得到DOM對象的可視區域高度以及臨界線到DOM對象可視區域頂邊的距離即可。我的代碼中是利用getBoundingClientRect來求的,相當於還是以瀏覽器的可視區域的頂邊作為參考線,不過考慮到普通的DOM對象可能也有頂部邊框的問題,在計算最後的臨界線到DOM對象可視區域的頂邊距離時,減去了DOM對象頂部變寬的寬度。只有這樣得出的臨界線距離才是相對於DOM對象可視區域頂邊而言的。
以上就是本文的全部內容,介紹了我想要補充說明的關於滾動列表組件的部分。這一塊內容,我覺得沒有特別廣的適用性,畢竟各個產品對滾動翻頁這種需求的邏輯可能也不盡相同,我這邊提供的是我現有項目中的實現思路,可能只能對您有一定的參考價值。所以要是有不妥的,歡迎隨時幫我指正出來。謝謝閱讀:)