listViewBase是列表元件所有檔案中最核心的一個,它抽象了所有列表的公共邏輯,將來如果有必要新增其它公共的邏輯,都可以考慮在這個類中處理。它主要做的事情包括:初始化,如排序元件初始化,分頁元件初始化,模板管理引擎初始化,事件繫結和請求傳送及處理等。這個檔案看起來比較長,有300度行,但是非常好理解。下面我會把它的每個要點內容一一說明。
原始碼地址:https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/base/listViewBase.js
首先看看程式碼的整體結構。
注:程式碼中的EventBase是我原來寫的一個元件,基於jquery,簡單實現任意物件支援事件管理的功能【jquery技巧之讓任何元件都支援類似DOM的事件管理】;Class也是我原來寫的一個元件,用來支援物件導向思想的類的構造&繼承【詳解Javascript的繼承實現】;Ajax也是一個簡單的元件,對jquery的ajax進行了二次封裝,以便ajax請求的管理更符合自己的思維習慣【對jquery的ajax進行二次封裝以及ajax快取代理元件:AjaxCache】。
listViewBase的整體結構跟我以前的寫的元件基本一致,畢竟已經養成這個習慣了。DEFAULTS表示元件的預設options,它繼承了EventBase來實現自身的事件管理。在元件類的靜態成員上,繫結了DEFAULTS,是為了方便子類進行引用;定義了一個dataAttr的屬性,它有兩個作用:第一是作為data屬性,在將元件例項繫結到相關DOM元素的jq物件上時用到:
第二是用於生成元件的事件名稱空間,元件內所有的事件都會加上這個事件名稱空間,以便不會產生事件衝突:
接著看看DEFAULTS的定義,我會挑主要的進行解釋:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
|
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, }; |
其中:
1)isAjaxResSuccess , getRowsFromAjax , getTotalFromAjax作用跟ajax的返回解析有關。通常做了自定義的ajax返回封裝後,ajax的返回可能是類似這樣的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
|
{ "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方法
原始碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
|
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 |
如果此時改變其中一個查詢條件的值:
列表就會用新的查詢條件請求資料:
|
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函式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
|
//更新查詢條件 //如果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為例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
|
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行。
下一篇部落格補充對排序跟分頁元件的說明。