這是我寫的關於列表組件的第3篇博客。前面的相關文章有:
1. 列表組件抽象(1)-概述
本文介紹列表組件中我對分頁和排序的抽象思路。
先來說分頁,因為之前寫過一篇簡單封裝分頁功能pageView.js,這次封裝分頁時的思路基本與那篇博客的想法完全一樣,只不過考慮到我要寫的列表組件,還有其它的分頁形式,比如點擊加載更多進行翻頁,基於瀏覽器標准的scroll事件進行翻頁,基於iscroll插件派發的scroll事件進行分頁。於是我在該文的基礎上進一步抽象,將分頁的一些公共邏輯提煉到一個基類中,僅僅將UI與分頁控制的邏輯留給子類實現,這樣能夠最大程度地簡化代碼。這個基類的最終實現是:https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/base/pageViewBase.js,它其實就是簡單封裝分頁功能pageView.js這篇博客中pageView.js分離出來的,所以如果想要去了解這個文件的說明,可以訪問之前的那篇博客。
當我把分頁的一些公共邏輯抽象到pageViewBase之后,在簡單封裝分頁功能pageView.js這篇文章中的pageView.js就會變得特別簡潔,我最終的實現是:https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/simplePageView.js。當你查看pageViewBase.js的源碼和simplePageView.js的源碼,會發現它們合起來就是簡單封裝分頁功能pageView.js里面的pageView.js。
其它的分頁組件實現有:
基於瀏覽器標准的scroll事件進行翻頁,https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/scrollPageView.js
基於iscroll插件派發的scroll事件進行分頁,https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/iscrollPageView.js
這兩個分頁組件其實跟simplePageView沒什么大的不同,只是分頁使用到的事件不同,另外就是由於涉及到滾動翻頁,所以還有一個何時進行自動翻頁的判斷問題,這個判斷問題我會在后面的文章介紹滾動分頁列表組件的時候再來說明。
再來看排序。
相比之下,排序會比分頁麻煩一些。做排序管理的目的在於,列表為用戶提供數據的時候,為了更有針對性的查看數據,用戶一般會希望能夠主動地控制列表的排序規則,所以得考慮排序管理的功能,以便列表能夠實現自定義的排序。在用戶做了排序操作之后,我們需要告訴后台當前排序操作結果對應的排序字段以及每個字段對應的排序值。由於有可能有多列排序的情況,所以傳遞排序參數時,還得按排序操作時的順序,組織好排序字段的順序,以便后台能夠按照用戶的操作結果,來進行排序處理。類似下面這樣的數據結構就可以正確地反映一個排序操作的結果:
[ { "field":"name", "value":"asc" }, { "field":"contact", "value":"desc" } ]
字段在數組中的先后關系即可代表排序時的先后關系。只要能夠得到這樣一個結構,就能把轉成json格式的字符串傳遞給后台進行處理。最終這個數據結構對應到數據庫中的排序規則時,就是這樣的:
order by name asc, contact desc
從排序操作上來說,常見的table插件是這么做的:
1. 如果僅僅是鼠標單擊排序列,那么執行的就是單列排序操作。只要按照 不排序->升序、升序->降序、降序->不排序的切換規則,在鼠標單擊排序列之后,改變該列對應的排序字段的排序方式,然后觸發查詢即可。傳遞到后台時,排序參數最多包含一個字段。
2. 如果在鼠標單擊排序列之前,用戶先摁住了shift鍵,再做點擊操作,此時用戶執行的就是多列排序操作,在shift鍵摁住期間,先點擊的排序列對應的字段在排序結果中的順序靠前,后點擊的靠后。單個排序列還是按照 不排序->升序、升序->降序、降序->不排序的切換規則來更改自身的排序方式,但是在單擊完之后並不會立即觸發列表查詢,而是要等到shift鍵釋放之后,再來查詢。傳遞到后台時,排序參數可能包含多個字段。
按照前面的這個需求,我的實現思路時:先把排序參數的管理和排序操作的控制分開,寫成兩個組件;排序參數的管理組件僅負責排序字段的數據這一層級的控制,不與任何UI打交道;排序操作的管理組件負責與DOM交互,響應用戶的鍵鼠操作,內部實例化一個排序參數管理的組件,利用這個組件實例來完成對排序字段的修改。這么做的好處在於將數據與UI分離,其實也就是表現與行為分離,簡化UI層的邏輯,讓代碼看起來更加清晰。
最后考慮到不同的列表組件,可能有不同的排序UI控制邏輯,所以也決定把排序組件抽象出一個基類,像pageViewBase一樣,把一些排序組件公共的邏輯出現出來,比如事件監聽,啟用禁用以及排序參數管理組件的實例化等。最終我得到了以下2個核心的排序組件相關的文件:
排序參數管理組件:https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/base/sortFields.js
排序控制管理組件的基類:https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/base/sortViewBase.js
下面我把這兩個文件的一些要點一一說明。
先說sortFields。
這個文件比sorViewBase還長,可想而知,如果我把sortFields的邏輯不分離,直接寫在sortViewBase里面,sortViewBase的復雜性肯定會增加不少。為了了解這個組件的作用,我先用幾段簡單的代碼來演示它的用法,雖然在實際使用中,這個組件並不需要直接實例化,但是它還是可以直接實例化的,不然就無法為sortView組件所用了。
通過下面的方式來實例化一個sortFields的組件。
var sf = new SortFields({ config: [ {field: 'name', value: ''}, {field: 'contact', value: 'desc', order: 2}, {field: 'email', value: 'asc', order: 1} ], //排序狀態改變的事件回調 onStateChange: function(e, data){ console.log('field[' + data.field + '] sort state is ' + data.value); }, //排序開始的事件回調 onSortStart: function(e){ console.log('sort start->'); }, //排序結束的事件回調 onSortEnd: function(e){ console.log('<-sort end'); }, //排序值改變的事件回調 onSortChange: function(e){ console.log('sort value change! new value is:'); console.log(JSON.stringify(this.getValue())); }, });
先說config這個option,其它的介紹后面的用法再補充。config用來配置排序管理組件要管理的排序字段。用數組的形式來配置多個排序字段,單個排序字段的排序定義用一個js的字面量對象來配置。用field屬性來定義排序字段的名稱;用value屬性來配置該字段初始化時的排序方式;用type屬性來配置該字段的數據類型,如string,int等,這個有可能在后台會需要;用order屬性來配置該字段在排序規則中的初始化位置。如以上config,在初始化后,實際上對應的排序規則就是:
order by email asc, contact desc
為啥是email在前,contact在后面,這個就是order屬性的作用了。
sortFields組件提供了一個getConfig的實例方法,這個方法返回所有排序字段的當前狀態:
在通過后面要介紹的changeState方法,改變了單個排序字段的排序方式后,我們可以在其它位置通過調用getConfig方法,獲取排序字段最新的狀態,從而更新UI:
比如simpleSortView里面的render方法就是這么做的:
render: function () { var that = this, opts = this.options; //根據sortFields的當前狀態,重新渲染所有排序項 this.sortFields.getConfig().forEach(function (fieldDef) { var $target = that.$sort_items.filter('[data-field="' + fieldDef.field + '"]'); $target.removeClass([ opts.sortAscClass, opts.sortDescClass ].join(' ')); if (fieldDef.value !== 'no') { $target.addClass( fieldDef.value == 'asc' ? opts.sortAscClass : opts.sortDescClass ); } }); }
sortFields提供了getValue方法,可以得到當前的排序結果,這個方法基於getConfig實現,過濾掉不排序的字段,同時按order屬性對getConfig返回的數組進行排序:
getValue: function () { return this.getConfig().filter(function (def) { return def.value !== 'no'; }).sort(sortByOrder); },
sortFields最重要的方法是changeState(fieldName, multiple),這個方法用來改變某個排序字段的排序方式,它接收兩個參數,第一個參數代表字段的名稱,第二個參數表示是否進行多列排序。
這個時候再來補充說明下前面幾個事件option的詳細內容:
當我們在UI層進行排序操作時,只有第一個字段在調用changeState時,會觸發sortStart事件,也就是onSortStart那個回調,表示排序開始;
每次調用changeState方法,都會觸發sortStateChange事件,也就是onStateChange那個回調,表示某個字段的排序方式改變;
如果是單個字段排序,在changeState方法最后會主動調用endSort方法來結束排序,在endSort方法內部,會判斷當前所有的排序狀態以及順序與排序操作前的狀態順序是否有變化,如果有變化則觸發sortChange事件,也就是onSortChange那個回調;
如果是多個字段排序,在changeState方法最后就不會主動調用endSort方法。因為對sortFields組件來說,多列排序的時候,它根本不知道什么時候結束排序,所以必須由UI層主動調用endSort方法,比如說在shift鍵釋放的時候;
在endSort方法最后,會觸發sortEnd事件,也就是onSortEnd那個回調,表示排序結束。
下面先看通過這個方法進行單列排序的演示:
sf.changeState('email');
VM601:14 sort start->
VM601:10 field[email] sort state is desc
VM601:22 sort value change! new value is:
VM601:23 [{"field":"email","value":"desc","order":1,"type":"string"}]
VM601:18 <-sort end
(VM601都是console里面復制的時候帶出來的,不用關注~)從這個演示能看到,所有回調都被觸發,並且getValue方法返回了changeState最新的排序結果。
再來看看多列排序的情況:
sf.changeState('contact',true);
VM601:14 sort start->
VM601:10 field[contact] sort state is asc
sf.changeState('name',true);
VM601:10 field[name] sort state is asc
sf.endSort()
VM601:22 sort value change! new value is:
VM601:23 [{"field":"contact","value":"asc","order":1,"type":"string"},{"field":"name","value":"asc","order":2,"type":"string"}]
VM601:18 <-sort end
在這個演示中,我前后改變了兩個字段的排序方式,先改變的是contact,后改變的是name,最后通過endSort結束了這次多列排序,然后在onSortChange回調中,我們看到了跟我們排序操作一致的排序結果。同時也可以看到sortStart等幾個事件回調,在多列排序操作時的執行情況,跟我前面的說明是完全一致的。
到這里為止,我說明了sortFields的實現思路和使用方法,根據以上內容再去閱讀源碼,應該就比較好理解了。
接下來介紹sortViewBase.
其實這個類就很簡單了。代碼結構跟之前的listViewBase和pageViewBase都一致。
defaults定義如下:
var DEFAULTS = { config: [],//排序字段的配置 sortParamName: 'sort_fields',//排序參數名稱 onChange: $.noop,//排序改變時的回調 onInit: $.noop,//初始化完畢的回調 };
這個config是在內部實例化sortFields組件的時候用到的,sortParamName是在為列表組件提供排序參數時用到的,這個參數名將會用來傳遞到后台,onChange也是提供給列表組件使用的,外部在此回調內觸發列表查詢。
這個基類的實現也有用到模板方法。init方法實現跟前面的博客介紹的組件差不多,它在中間的代碼實例化了前面寫的sortFields組件:
//初始化一個內部的排序管理組件SortFields的實例 var _render = $.proxy(this.render, this); this.sortFields = new SortFields({ config: opts.config, onReset: _render, onStateChange: _render, onSortChange: function (e) { that.trigger('sortViewChange' + that.namespace); } }); this.render();
render是sortView組件的一個實例方法,在sortFields重置,排序狀態改變的時候,都會調用這個render方法來實現UI層的更新。
然后這個基類還提供了enable和disable方法,做啟用和禁用的控制。
最后來介紹sortViewBase的一個實現:simpleSortView。
在demo中,listView_1.html里面,下面這個UI內容,就是simpleSortView組件的實例:
tableView.html里面,整個表頭就是一個simpleSortView的實例:
simpleSortView的defaults如下:
var DEFAULTS = $.extend({}, SortViewBase.DEFAULTS, { //排序項的選擇器 sortItemSelector: '.sort_item', //升序狀態的css類名 sortAscClass: 'sort_asc', //降序狀態的css類名 sortDescClass: 'sort_desc' }),
sortItemSelector用來篩選那些與每個排序字段對應的元素,后面兩個cssClass是作為排序狀態類來使用的。把這些定義成option也是為了增加組件的靈活性。
simpleSortView其實就是做了些事件綁定來控制排序操作,以及UI渲染的邏輯。
rende方法在前面已經說明過了,需要補充一下的就是,simpleSortView為了能夠將DOM元素與排序字段對應起來,必須在DOM元素加些特定的屬性來標識,這里我用的是data-field屬性。只要把某個DOM元素的data-field屬性的值,配置成排序字段的名稱,它們就關聯起來了。
排序操作的控制邏輯其實也非常簡單:
bindEvents: function () { //子類在實現bindEvent時,必須先調用父類的同名方法 this.base(); var that = this, opts = this.options; var rnd = this.namespace_rnd; //在事件后面增加隨機數的目的是防止$document的事件觸發沖突 //結合namespace跟rnd,就相當於給document的事件添加了兩個命名空間 //這樣即使同一個頁面中有多個SimpleSortView的實例,互相之間也不會有事件沖突的影響 $document.on('keydown' + this.namespace + '.' + rnd, function (e) { if(that.disabled) return; if (e.which == 16) { //shift鍵按下的時候,表示要進行多列排序 that.multiple = true; } }).on('keyup' + this.namespace + '.' + rnd, function (e) { if(that.disabled) return; if (e.which == 16 && that.multiple) { that.multiple = false; //shift鍵抬起的時候,調用sortFields的實例的endSort方法,結束多列排序 that.sortFields.endSort(); } }); this.$element.on('click', opts.sortItemSelector, function () { if(that.disabled) return; that.sortFields.changeState($(this).data('field'), that.multiple); }); },
也就是按前面的單列和多列排序操作的需求實現而已。由於用到了$document這種公共的DOM對象來注冊事件,所以為了避免事件沖突,在已有的命名空間的基礎上,又加了一個隨機數作為新的命名空間。如果沒有這個,當頁面內包含多個simpleSortView組件實例時,keydown和keyup事件就會沖突。
到此為止,分頁組件和排序組件的一些要點也都介紹完了,希望這些東西能幫助感興趣的朋友理解我的思路。



