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

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

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

listViewBase是列表組件全部文件中最核心的一個,它抽象了全部列表的公共邏輯,未來若是有必要添加其它公共的邏輯,均可以考慮在這個類中處理。它主要作的事情包括:初始化,如排序組件初始化,分頁組件初始化,模板管理引擎初始化,事件綁定和請求發送及處理等。這個文件看起來比較長,有300度行,可是很是好理解。下面我會把它的每一個要點內容一一說明。jquery

源碼地址:https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/base/listViewBase.jsgit

首先看看代碼的總體結構。github

image

注:代碼中的EventBase是我原來寫的一個組件,基於jquery,簡單實現任意對象支持事件管理的功能【jquery技巧之讓任何組件都支持相似DOM的事件管理】;Class也是我原來寫的一個組件,用來支持面向對象思想的類的構造&繼承【詳解Javascript的繼承實現】;Ajax也是一個簡單的組件,對jquery的ajax進行了二次封裝,以便ajax請求的管理更符合本身的思惟習慣【對jquery的ajax進行二次封裝以及ajax緩存代理組件:AjaxCache】。ajax

listViewBase的總體結構跟我之前的寫的組件基本一致,畢竟已經養成這個習慣了。DEFAULTS表示組件的默認options,它繼承了EventBase來實現自身的事件管理。在組件類的靜態成員上,綁定了DEFAULTS,是爲了方便子類進行引用;定義了一個dataAttr的屬性,它有兩個做用:第一是做爲data屬性,在將組件實例綁定到相關DOM元素的jq對象上時用到:json

image

第二是用於生成組件的事件命名空間,組件內全部的事件都會加上這個事件命名空間,以便不會產生事件衝突:緩存

image
image

接着看看DEFAULTS的定義,我會挑主要的進行解釋:數據結構

var DEFAULTS = {
    //接口地址
    url: '',
    //數據模板
    tpl: '',
    //ajax請求方法
    ajaxMethod: 'get',
    //判斷成功的ajax
    isAjaxResSuccess: function (res) {
        return res.code == 200;
    },
    //從ajax返回中解析出數據
    getRowsFromAjax: function (res) {
        return res.data.rows;
    },
    //從ajax返回中解析出總記錄數
    getTotalFromAjax: function (res) {
        return res.data.total;
    },
    //提供給外部解析ajax返回的數據
    parseData: $.noop,
    //提供給模板引擎,以便獲得知足其要求的數據
    renderParse: function(paredRows){
        return {
            rows: paredRows
        }
    },
    //組件初始化完畢後的回調
    afterInit: $.noop,
    //ajax請求以前的事件回調
    beforeAjax: $.noop,
    //ajax請求以後的事件回調
    afterAjax: $.noop,
    //ajax請求成功的事件回調
    success: $.noop,
    //ajax請求失敗的事件回調
    error: $.noop,
    //PageView相關的option,爲空表示不採用分頁
    pageView: {},
    //SortView相關的option,爲空表示不採用排序管理
    sortView: false,
    //在調用query方法的時候,是否自動對SortView進行reset
    resetSortWhenQuery: false,
    //查詢延時
    queryDelay: 0,
};

其中:app

1)isAjaxResSuccess , getRowsFromAjax , getTotalFromAjax做用跟ajax的返回解析有關。一般作了自定義的ajax返回封裝後,ajax的返回多是相似這樣的:

{
  "code": 200,
  "data": {
    "total": 237,
    "rows": [
      {
        "like": 2,
        "title": "博客標題博客標題",
        "avatar": "",
        "summary": "能夠看到這個列表頁實際上是用到了不少語義化的命名的css類的,假如要用面向屬性的命名方法來定義,就會變成下面這個樣子:,能夠看到這個列表頁實際上是用到了不少語義化的命名的css類的,假如要用面向屬性的命名方法來定義,就會變成下面這個樣子:",
        "author": "流雲諸葛",
        "publish_time": "2016-06-05 08:53",
        "comment": 22,
        "read": "666"
      },
      {
        "like": 2,
        "title": "博客標題博客標題",
        "avatar": "",
        "summary": "能夠看到這個列表頁實際上是用到了不少語義化的命名的css類的,假如要用面向屬性的命名方法來定義,就會變成下面這個樣子:,能夠看到這個列表頁實際上是用到了不少語義化的命名的css類的,假如要用面向屬性的命名方法來定義,就會變成下面這個樣子:",
        "author": "流雲諸葛",
        "publish_time": "2016-06-05 08:53",
        "comment": 22,
        "read": "666"
      },
      {
        "like": 2,
        "title": "博客標題博客標題",
        "avatar": "",
        "summary": "能夠看到這個列表頁實際上是用到了不少語義化的命名的css類的,假如要用面向屬性的命名方法來定義,就會變成下面這個樣子:,能夠看到這個列表頁實際上是用到了不少語義化的命名的css類的,假如要用面向屬性的命名方法來定義,就會變成下面這個樣子:",
        "author": "流雲諸葛",
        "publish_time": "2016-06-05 08:53",
        "comment": 22,
        "read": "666"
      }
    ]
  }
}

以上這個ajax返回demo模擬了一個分頁列表時某次ajax請求返回的數據,其中code屬性爲200表示這個ajax是成功的,ajax返回的數據集合存放在data.rows屬性上,數據的總記錄數存放在data.total屬性上。有可能你的項目中,分頁列表返回的數據結構跟這個不同,可是對於列表組件來講,有三個要素是一個請求的返回中必須包含的:

a. 什麼樣的返回纔是成功的;

b. 返回中的哪一部分表示當前請求的數據集;

b. 返回中的哪一部分表示當前數據類型的記錄總數。

isAjaxResSuccess , getRowsFromAjax , getTotalFromAjax解決的就是這三個問題。我提供的這三個option的默認值都是按前面的那個json結構寫的,若是你的項目中列表ajax請求不是這個json結構,只要改變這三個option的定義便可。

2)parseData和renderParse用於解析getRowsFromAjax返回的數據,以及爲模板引擎提供它所須要的model對象。在一個列表ajax請求中,頗有可能某些返回的數據不適合直接顯示在頁面裏面,好比時間戳格式的字段,咱們可能更須要把它轉化爲咱們所習慣的日期格式字符串才行,這個時候只要利用parseData方法便可,這個方法接受getRowsFromAjax返回的數據做爲惟一的參數。renderParse跟模板引擎有關係,拿mustache來講,若是我定義tpl的時候用的是下面相似的結構:

['{{#rows}}<tr>',
            '<td><span class="table_view_order"></span></td>',
            '<td align="middle" class="tc"><input type="checkbox" class="table_check_row"></td>',
            '<td>{{name}}</td>',
            '<td>{{contact}}</td>',
            '<td>{{email}}</td>',
            '<td>{{nickname}}</td>',
            '<td><button class="btn-action" type="button">操做</button></td>',
            '</tr>{{/rows}}'].join(''),

意味着我在使用mustche渲染的時候,須要傳入一個{rows: …}的model才行,這個model裏面的rows是根據tpl裏面的{{#row}}來肯定的。默認狀況下,我在定義tpl的時候,都使用rows做爲遍歷屬性名,若是你不習慣用rows,那麼可經過renderParse這個option來自定義要使用的遍歷屬性名。好比換成records:

renderParse: function(paredRows){
    return {
        records: paredRows
    }
},

3)afterInit等事件的做用在於組件實例可根據自身的需求場景,在這些事件派發的時候,添加額外的一些處理邏輯,而不會影響別的實例。

4)pageView跟sortView用來傳遞分頁組件和排序組件實例化的時候,要傳入的options。若是爲false,則表示這個列表組件沒有對應的分頁組件或排序組件。

5)queryDelay若是大於0,那麼就會延遲發送ajax請求,延遲時間就等於queryDelay設定的時間。

接下來看看一些關鍵的實例方法定義。

1)init方法

源碼:

init: function (element, options) {
    var $element = this.$element = $(element),
        opts = this.options = this.getOptions(options),
        that = this;

    //初始化,註冊事件管理的功能:EventBase
    this.base($element);

    //模板方法,方便子類繼承實現,在此處添加特有邏輯
    this.initStart();

    //設置數據屬性名稱、命名空間名稱
    this.dataAttr = this.constructor.dataAttr;
    this.namespace = '.' + this.dataAttr;
    //存放查詢條件
    this.filter = {};

    //模板方法,方便子類繼承實現,在此處添加特有邏輯
    this.initMiddle();

    //初始化分頁組件
    //createPageView必須返回繼承了PageViewBase類的實例
    //這裏沒有作強的約束,只能靠編碼規範來約束
    this.pageView = this.createPageView();
    if (this.pageView) {
        //註冊分頁事件
        this.pageView.on('pageViewChange' + this.pageView.namespace, function () {
            that.refresh();
        });
    }

    //初始化模板管理組件,用於列表數據的渲染
    //createTplEngine必須返回繼承了TplBase類的實例
    //這裏沒有作強的約束,只能靠編碼規範來約束
    this.itemTplEngine = this.createTplEngine();

    //初始化排序組件
    //createSortView必須返回繼承了SortViewBase類的實例
    //這裏沒有作強的約束,只能靠編碼規範來約束
    this.sortView = this.createSortView();
    if (this.sortView) {
        //註冊排序事件
        this.sortView.on('sortViewChange' + this.sortView.namespace, function () {
            that.refresh();
        });
    }

    //模板方法,方便子類繼承實現,在此處添加特有邏輯
    this.beforeBindEvents();

    //綁定全部事件回調
    this.bindEvents();

    //模板方法,方便子類繼承實現,在此處添加特有邏輯
    this.initEnd();

    $element.data(this.dataAttr, this);

    this.trigger('afterInit' + this.namespace);
},

這個方法其實很簡單,就是按順序作一些初始化的邏輯而已。稍微值的一提的是,爲了讓子類支持更靈活的擴展,這個方法在一些關鍵代碼的先後都加了空方法,以便子類在父類的這些關鍵代碼執行先後,插入本身的邏輯。createPageView用於子類返回分頁組件的實例,若是返回了分頁組件實例,會自動監聽分頁組件的相關change事件,並調用列表組件的refresh方法,以便根據最新的分頁參數刷新列表。createSortView用於子類返回排序組件的實例,做用徹底相似createPageView。

2. bindEvents方法

就是註冊事件而已。不過子類在提供本身的bindEvents方法的時候,必須在它的bindEvents,經過this.base()調用父類的bindEvents方法。這裏沒有像init方法那樣,增長不少空方法來處理。畢竟沒有那麼多個性化的位置。

3. getParams方法

返回列表的參數:

getParams: function () {
    //參數由:分頁,排序字段以及查詢條件構成
    return $.extend({},
        this.pageView ? this.pageView.getParams() : {},
        this.sortView ? this.sortView.getParams() : {},
        this.filter);
},

在請求發送時,會調用這個方法來獲取要傳遞給後臺的參數。

4. renderData方法

子類不用實現,可是子類會用到,它在內部調用模板引擎管理組件,來返回渲染以後的html字符串,子類在拿到這個字符串以後,可作DOM更新的操做。

5. refresh方法

表明列表刷新。僅在分頁或排序改變的時候調用。

6. query方法

表明列表查詢。這個方法跟refresh方法都在內部調用_query函數進行請求的處理,可是兩個方法使用的場景不同。

refresh方法基本上不影響參數,若是是分頁refresh,那麼參數中只有分頁參數會變化;若是是排序refresh,那麼參數中只有排序參數會變化;若是是其它refresh,全部參數都不變化,列表只是按當前條件從新請求一遍數據而已。

query方法不同:它接收新的查詢條件,用於更新原來的查詢條件。而且它會重置分頁排序組件,若是resetSortWhenQuery爲true,它還會重置排序組件。query方法能夠實現比較強大的列表查詢功能。下面我會盡可能詳細介紹它的用法,因爲沒有查詢條件的表單,因此我直接在控制檯模擬一下了。你能夠直接用http://liuyunzhuge.github.io/blog/form/dist/html/tableView.html這個頁面進行操做,我把這個頁面裏面的的列表組件實例已經存放在window.l屬性上,因此在控制檯能夠經過l這個全局變量拿到列表組件實例。

在此以前,我先假設有一個列表頁面,放了兩個查詢條件,一個是按類型查,一個是按關鍵詞查,當咱們要執行搜索的時候,能夠用下面的方式在給列表增長查詢條件:

l.query({type: '1', keywords: 'ssss'})

查看ajax請求,能夠看到新添加的請求參數:

rnd:0.3144281458900091
_ajax:1
page:1
page_size:3
sort_fields:[{"field":"time","value":"asc","order":1,"type":"datetime"},{"field":"sales","value":"desc","order":2,"type":"int"}]
type:1
keywords:ssss

若是此時改變其中一個查詢條件的值:

l.query({type: '2'})

列表就會用新的查詢條件請求數據:

rnd:0.15610846260036104
_ajax:1
page:1
page_size:3
sort_fields:[{"field":"time","value":"asc","order":1,"type":"datetime"},{"field":"sales","value":"desc","order":2,"type":"int"}]
type:2
keywords:ssss

若是在改變查詢條件的同時,給query方法傳遞第二個參數,值爲false:

l.query({type: '3'}, false)

會發現此次的列表請求中,已經沒有了以前的那個keywords的參數:

rnd:0.09752677645742791
_ajax:1
page:1
page_size:3
sort_fields:[{"field":"time","value":"asc","order":1,"type":"datetime"},{"field":"sales","value":"desc","order":2,"type":"int"}]
type:3

由於query方法的第二個參數若是是false的話,列表組件在更新查詢條件的時候,將採用替換而不是覆蓋的方式處理。

前面說的query方法會重置分頁組件或排序組件,是指在請求前會調用分頁組件或排序組件實例的reset方法,以便還原排序和分頁參數值爲默認值。

最後再看核心一個函數定義:_query函數。

//更新查詢條件
//若是append爲false,那麼用newFilter替換當前的查詢條件
//不然,僅僅將newFilter包含的參數複製到當前的查詢條件裏面去
function updateFilter(newFilter, append) {
    var filter;

    if (newFilter) {
        if (append === false) {
            filter = newFilter;
        } else {
            filter = $.extend({}, this.filter, newFilter);
        }
        this.filter = filter;
    }
}

//_query函數中關鍵的模板方法與事件的調用順序:
//method: beforeQuery
//[method: queryCancel]
//event: beforeAjax
//1-成功:
//  method: querySuccess
//  event: success
//  method: afterQuery
//  event: afterAjax
//2-失敗:
//  method: queryError
//  event: error
//  method: afterQuery
//  event: afterAjax
function _query(clear, newFilter, append) {
    var that = this,
        opts = this.options;

    if (!opts.url) return false;

    //調用子類可能實現了的beforeQuery方法,以便爲該子類添加統一的一些query前的邏輯
    if (this.beforeQuery(clear) === false) {
        this.queryCancel(clear);
        return false;
    }

    if (clear) {
        //更新查詢條件
        updateFilter.call(this, newFilter, append);

        //重置分頁組件
        this.pageView && this.pageView.reset();
    }

    //禁用分頁組件,防止重複操做
    this.pageView && this.pageView.disable();

    //還原排序組件
    this.sortView && opts.resetSortWhenQuery && this.sortView.reset();

    //觸發beforeAjax事件,以便外部根據特有的場景添加特殊的邏輯
    this.trigger('beforeAjax' + this.namespace);

    if (opts.queryDelay) {
        var dtd = $.Deferred();
        var timer = setTimeout(function () {
            clearTimeout(timer);
            _request().done(function () {
                dtd.resolve.apply(dtd, arguments);
            }).fail(function () {
                dtd.reject.apply(dtd, arguments);
            });
        }, opts.queryDelay);

        return $.when(dtd);
    } else {
        return _request();
    }

    function _request() {
        return Ajax[opts.ajaxMethod](opts.url, that.getParams())
            .done(function (res) {
                //判斷ajax是否請求成功
                var isSuccess = opts.isAjaxResSuccess(res),
                    rows = [],
                    total = 0;

                if (isSuccess) {
                    //獲得全部行
                    rows = opts.getRowsFromAjax(res);

                    that.originalRows = rows;

                    //獲得總記錄數
                    total = opts.getTotalFromAjax(res);

                    //刷新分頁組件
                    that.pageView && that.pageView.refresh(total);

                    var parsedRows = opts.parseData(rows);
                    if (!parsedRows) {
                        parsedRows = rows;
                    }

                    that.parsedRows = parsedRows;

                    //調用子類實現的querySuccess方法,一般在這個方法內作列表DOM的渲染
                    that.querySuccess(that.renderData(opts.renderParse(parsedRows)), {
                        clear: clear,
                        total: total
                    });

                    //觸發success事件,以便外部根據特有的場景添加特殊的邏輯
                    that.trigger('success' + that.namespace);

                    _always();

                    //觸發afterAjax事件,以便外部根據特有的場景添加特殊的邏輯
                    that.trigger('afterAjax' + that.namespace);
                } else {
                    _fail();
                }
            })
            .fail(_fail);
    }

    function _fail() {
        //調用子類實現的queryError方法,以便子類實現特定的加載失敗的展現邏輯
        that.queryError({
            clear: clear
        });

        //觸發error事件,以便外部根據特有的場景添加特殊的邏輯
        that.trigger('error' + that.namespace);

        _always();

        //觸發afterAjax事件,以便外部根據特有的場景添加特殊的邏輯
        that.trigger('afterAjax' + that.namespace);
    }

    function _always() {
        //從新恢復分頁組件的操做
        that.pageView && that.pageView.enable();

        //調用子類實現的afterQuery方法,以便子類實現特定的請求以後的邏輯
        that.afterQuery({
            clear: clear
        });
    }
}

這個函數源碼較長,可是理解起來應該不會麻煩,由於它也跟init方法同樣,純粹是按順序編寫的一些邏輯。在這個函數裏面調用了另外幾個模板方法,派發了大量的事件。雖然看起來這些模板方法,跟事件的做用有些重合,其實它們的做用是徹底不一樣的。模板方法是直接添加在類層面的,它能夠爲子類提供類級的擴展;而事件是由具體的實例派發的,因此它只能在給特定的實例添加擴展。

這些模板方法以及事件的觸發順序也比較關鍵,都是按照先調用模板方法,再派發事件的順序來的,拿querySuccess方法與success事件來講,必定是先調用querySuccess方法,再派發success事件,這個起因也跟前面的類級擴展和實例級擴展的層次有關係。全部模板方法以及事件的調用關係,按照請求成功或失敗分了2條線,我在註釋中已經描述地很清楚了。

以上就是listViewBase這個基類的所有內容了。

接下來看看它的子類該如何實現,以simpleListView爲例:

define(function (require) {
    var $ = require('jquery'),
        MustacheTpl = require('mod/listView/mustacheTpl'),
        SimplePageView = require('mod/listView/simplePageView'),
        SimpleSortView = require('mod/listView/simpleSortView'),
        ListViewBase = require('mod/listView/base/listViewBase'),
        Class = require('mod/class');

    var DEFAULTS = $.extend({}, ListViewBase.DEFAULTS, {
        //列表容器的選擇器
        dataListSelector: '.data_list',
        //分頁組件選擇器
        pageViewSelector: '.page_view',
        //排序組件選擇器
        sortViewSelector: '.sort_view'
    });

    var SimpleListView = Class({
        instanceMembers: {
            initMiddle: function () {
                var opts = this.options,
                    $element = this.$element;

                //緩存核心的jq對象
                this.$data_list = $element.find(opts.dataListSelector);
            },
            createPageView: function () {
                var pageView,
                    opts = this.options;

                if (opts.pageView) {
                    //初始化分頁組件
                    delete opts.pageView.onChange;
                    this.$element.append(SimplePageView.create());
                    pageView = new SimplePageView(this.$element.find(opts.pageViewSelector), opts.pageView);
                }
                return pageView;
            },
            createSortView: function () {
                var sortView,
                    opts = this.options;

                if (opts.sortView) {
                    //初始化分頁組件
                    delete opts.sortView.onChange;
                    sortView = new SimpleSortView(this.$element.find(opts.sortViewSelector), opts.sortView);
                }
                return sortView;
            },
            createTplEngine: function () {
                return new MustacheTpl(this.options.tpl);
            },
            querySuccess: function (html, args) {
                this.$data_list.html(html);
            }
        },
        extend: ListViewBase,
        staticMembers: {
            DEFAULTS: DEFAULTS,
            dataAttr: 'simpleList'
        }
    });

    return SimpleListView;
});

忽略掉SimplePageView以及SimpleSortView的實現,這個我下一篇博客會補充說明,你會發現實現一個簡單的列表組件已經很是簡潔了,代碼不到70行。

下一篇博客補充對排序跟分頁組件的說明。

相關文章
相關標籤/搜索