列表組件抽象(3)-分頁和排序管理說明

這是我寫的關於列表組件的第3篇博客。前面的相關文章有:css

1. 列表組件抽象(1)-概述html

2. 列表組件抽象(2)-listViewBase說明git

本文介紹列表組件中我對分頁和排序的抽象思路。github

先來講分頁,由於以前寫過一篇簡單封裝分頁功能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。json

其它的分頁組件實現有:數組

基於瀏覽器標準的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沒什麼大的不一樣,只是分頁使用到的事件不一樣,另外就是因爲涉及到滾動翻頁,因此還有一個什麼時候進行自動翻頁的判斷問題,這個判斷問題我會在後面的文章介紹滾動分頁列表組件的時候再來講明。oop

再來看排序

相比之下,排序會比分頁麻煩一些。作排序管理的目的在於,列表爲用戶提供數據的時候,爲了更有針對性的查看數據,用戶通常會但願可以主動地控制列表的排序規則,因此得考慮排序管理的功能,以便列表可以實現自定義的排序。在用戶作了排序操做以後,咱們須要告訴後臺當前排序操做結果對應的排序字段以及每一個字段對應的排序值。因爲有可能有多列排序的狀況,因此傳遞排序參數時,還得按排序操做時的順序,組織好排序字段的順序,以便後臺可以按照用戶的操做結果,來進行排序處理。相似下面這樣的數據結構就能夠正確地反映一個排序操做的結果:

[
  {
    "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的實例方法,這個方法返回全部排序字段的當前狀態:

image

在經過後面要介紹的changeState方法,改變了單個排序字段的排序方式後,咱們能夠在其它位置經過調用getConfig方法,獲取排序字段最新的狀態,從而更新UI:

image

好比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組件的實例:

image

tableView.html裏面,整個表頭就是一個simpleSortView的實例:

image

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事件就會衝突。

到此爲止,分頁組件和排序組件的一些要點也都介紹完了,但願這些東西能幫助感興趣的朋友理解個人思路。

相關文章
相關標籤/搜索