這是我寫的關於列表組件的第2篇博客。前面的相關文章有:css
1. 列表組件抽象(1)-概述html
listViewBase是列表組件全部文件中最核心的一個,它抽象了全部列表的公共邏輯,未來若是有必要添加其它公共的邏輯,均可以考慮在這個類中處理。它主要作的事情包括:初始化,如排序組件初始化,分頁組件初始化,模板管理引擎初始化,事件綁定和請求發送及處理等。這個文件看起來比較長,有300度行,可是很是好理解。下面我會把它的每一個要點內容一一說明。jquery
源碼地址:https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/base/listViewBase.jsgit
首先看看代碼的總體結構。github
注:代碼中的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
第二是用於生成組件的事件命名空間,組件內全部的事件都會加上這個事件命名空間,以便不會產生事件衝突:緩存
接着看看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行。
下一篇博客補充對排序跟分頁組件的說明。