Tags: javascript ajax
前一陣子根據老闆要求,寫了一個重點商品銷售統計報表的程式,每天統計各門市到昨天為止的重點商品銷售統計,將結果輸出到網頁讓每個門市都可以瀏覽查看。這程式運作了一段時間後,老闆覺得還不錯,就要我改成暢銷商品銷售統計,納入上千項商品為暢銷商品。因為上千項商品的報表很長,老闆就要我加上排序功能。
要排序當然OK啊。只是我很懶,不想為了視覺效果去修改統計程式,更不想為了更新排序結果而要伺服端再回傳一次頁面。於是我打算以動態網頁的方式,直接用 JavaScript 對網頁上的統計表格排序。
首先,我們先看一份示範用的統計表格,如下所示。點擊表格標題即可依此欄位內容排序。
XX銷售報表
項次
條碼
商品名稱
銷售量
銷售額
1
4203803
輕鬆錠
2
4203810
骨錠
10
2800
3
4203811
玻尿酸膠囊
5
2500
4
4203812
膠原葡萄籽錠
4
1200
5
4203817
葉黃素膠囊
9
891
6
4203818
綜合維他命
12
6000
7
4203865
異黃酮
8
1640
8
4203866
甲殼素膠囊
3
450
9
4203868
柚兒茶素
3
210
10
4203876
魚油
11
990
合計
設計概要
一開始,這是一張很單純的統計表格,標題只是文字而不是連結。顯示的統計結果是伺服端統計程式按項次排列之順序。現在我要加上排序功能,而且我不想要發生向伺服器要求更新頁面的動作,亦即我要以動態網頁的方式直接變動頁面上的內容排列順序。
第一步要想的是,排序資料如何得手?所幸網頁上的報表內容係基於「樣式與內容互離」之概念產生 ,因此報表中的內容就是純粹的資料,沒有夾雜其他樣式文字。故僅需取得報表中每一格的內容便可進行排序。排序程式僅需知道欄位名稱(或欄號),就可以直接從網頁上的報表中取得資料加以排序。
第二步則要思考如何依排序結果更新報表的每列順序。所幸網頁上的報表內容係基於良好的 HTML 格式嚴謹地產生 ,如下所示:
< table >
< thead >
< tr >
< td >
< /td >
< /tr >
< /thead >
< tbody >
< tr >
< td >
< /td >
< td >
< /td >
< /tr >
< /tbody >
< /table>
由於其文件結構良好,只需要透過 HTML DOM 取得 tr
元素陣列,再重新安插其 DOM 節點的位置即可。
連續兩個「所幸」並非偶然,因為我早知遵循這兩個原則的好處,一開始就決定如此輸出資料。不了解為何要將「樣式與內容分離」者,往往在報表內容中夾雜樣式文字,例如: <td><span style="color:red">-123</span></td>
。一但這麼寫, JavaScript 就不能簡便地自報表中取得排序資料,要多做許多節點存取動作。不了解為何要按 HTML 的良好格式輸出者,欄位標題列、合計列、分項列等等內容就會混在一起。如此就必須多出判斷更新範圍的動作,而不能簡便地更新局部內容。
實踐概念
在實作過程中,我先以程序導向的方式設計,定義一個變數 sorter ,將所有用得到的變數、函數都塞進裡面。我需要一組設定值告知第幾欄是什麼資料,如此一來僅需傳一個代表資料欄位的字串, JavaScript 就知道該從第幾欄位取得排序資料。我將這組設定值定義於陣列 sortConfig 。
接著定義喚起排序動作的起始函數 sortBy ,只需要傳遞欄位名稱。遞減的排序函數 funcSort_desc 、遞增的排序函數 funcSort_asc 、自 DOM 中取得排序資料與資料列節點的函數 getTbody , getRows 。最後定義實際進行排序及更新頁面內容的函數 doSort 。下列為程式碼:
var sorter = {
" currentType " : " item " ,
" currentOrder " : " asc " ,
" sortConfig " : {
" item " : {
" columnIndex " : 0
},
" ean " : {
" columnIndex " : 1
},
" pname " : {
" columnIndex " : 2
},
" qty " : {
" columnIndex " : 3
},
" amt " : {
" columnIndex " : 4
}
},
" sortBy " : function ( t ) {
if ( this . currentType == t ) {
this . currentOrder = ( this . currentOrder == " asc "
? " desc "
: " asc "
);
}
else {
this . currentType = t ;
}
var keyCellIndex = this . sortConfig [ this . currentType ]. columnIndex ;
var funcSort = this [ " funcSort_ " + this . currentOrder ];
this . doSort ( keyCellIndex , funcSort );
},
" funcSort_desc " : function ( a , b ) {
return a . key - b . key ;
},
" funcSort_asc " : function ( a , b ) {
return b . key - a . key ;
},
" getRows " : function () {
var tbody = this . getTbody ();
var rows = tbody . getElementsByTagName ( ' tr ' );
return rows ;
},
" getTbody " : function () {
return document . getElementsByTagName ( ' table ' )[ 0 ]. getElementsByTagName ( ' tbody ' )[ 0 ];
},
" doSort " : function ( keyCellIndex , sortFunc ) {
var rows = this . getRows ();
var keys = [];
for ( var i = 0 , cell , row = rows [ 0 ]; row ; row = rows [ ++ i ]) {
cell = row . getElementsByTagName ( ' td ' )[ keyCellIndex ]. firstChild . nodeValue ;
keys [ i ] = {
" key " : cell ,
" row " : row
}
}
keys . sort ( sortFunc );
var tbody = this . getTbody ();
for ( var i = 0 , r = keys [ i ]; r ; r = keys [ ++ i ]) {
tbody . appendChild ( r . row );
}
}
}
底下是報表的原始內容 (簡化版) 。報表是由伺服端統計程式產生,所以修改伺服端統計程式的輸出動作,加上一行載入 JavaScript 的動作,並修改標題欄位為呼叫 JavaScript 排序動作的連結。
< script type = " text/javascript " src = " sorter.js " >< /script >
< h3 > XX銷售報表 < /h3 >
< table border = " 1 " cellspacing = " 0 " >
< thead >
< tr >
< td class = " item " >< a href = " javascript:sorter.sortBy('item'); " > 項次 < /a></ td >
< td class = " ean " >< a href = " javascript:sorter.sortBy('ean'); " > 條碼 < /a></ td >
< td class = " pname " >< a href = " javascript:sorter.sortBy('pname'); " > 商品名稱 < /a></ td >
< td class = " qty " >< a href = " javascript:sorter.sortBy('qty'); " > 銷售量 < /a></ td >
< td class = " amt " >< a href = " javascript:sorter.sortBy('amt'); " > 銷售額 < /a></ td >
< /tr >
< /thead >
< tbody id = " reportTbody " >
< tr >
< td > 1 < /td >
< td > 4203803 < /td >
< td > 輕鬆錠 < /td >
< td >& nbsp ; < /td >
< td >& nbsp ; < /td >
< /tr >
< /tbody >
< /table>
Refactoring
透過上述快速的實作過程,我已經驗證了程式碼可行性。最後免不了要重整一下,最好是把上面的程式碼重整成一個可再用的 class 。重整重點有二:一、將原本的變數改寫成可生成新個體的函數型態,即 JavaScript 的類別。二、設定值與排序資料區域可作為引數傳遞。下列為重整後的程式碼,重整過程非常簡單而直覺,各位可以自行比較與思考。
function Sorter ( config ) {
var requiredArgs = [ ' table ' , ' currentType ' , ' currentOrder ' , ' sortConfig ' ];
for ( var i = 0 , k = requiredArgs [ i ]; k ; k = requiredArgs [ ++ i ]) {
this [ k ] = config [ k ];
}
var funcSortMap = {
' desc ' : function ( a , b ) {
return a . key - b . key ;
},
' asc ' : function ( a , b ) {
return b . key - a . key ;
}
}
this . sortBy = function ( t ) {
if ( this . currentType == t ) {
this . currentOrder = ( this . currentOrder == ' asc '
? ' desc '
: ' asc '
);
}
else {
this . currentType = t ;
}
var keyCellIndex = this . sortConfig [ this . currentType ]. columnIndex ;
var funcSort = ( this . sortConfig [ this . currentType ]. funcSortMap
? this . sortConfig [ this . currentType ]. funcSortMap
: funcSortMap
)[ this . currentOrder ];
doSort . apply ( this , [ keyCellIndex , funcSort ]);
}
var tbody ;
function getTbody () {
if ( ! tbody ) {
//pass by id or DOM node?
tbody = ( typeof this . table == ' string ' //pass by id
? document . getElementById ( this . table )
: this . table //pass by DOM node
);
}
return tbody ;
}
function getRows () {
var tbody = getTbody . call ( this );
var rows = tbody . getElementsByTagName ( ' tr ' );
return rows ;
}
function doSort ( keyCellIndex , sortFunc ) {
var rows = getRows . call ( this );
var keys = [];
for ( var i = 0 , cell , row = rows [ 0 ]; row ; row = rows [ ++ i ]) {
cell = row . getElementsByTagName ( ' td ' )[ keyCellIndex ]. firstChild . nodeValue ;
keys [ i ] = {
' key ' : cell ,
' row ' : row
}
}
keys . sort ( sortFunc );
var tbody = getTbody . call ( this );
for ( var i = 0 , r = keys [ i ]; r ; r = keys [ ++ i ]) {
tbody . appendChild ( r . row );
}
}
}
下列是使用案例,配置一個sorter ,引數中指示排序資料區域的 ID 、預設排序型態、順序,以及各資料型態的欄位位置。 HTML 部份則不需改變。
var sorter = new Sorter ({
" table " : " reportTbody " ,
" currentType " : " item " ,
" currentOrder " : " asc " ,
" sortConfig " : {
" item " : {
" columnIndex " : 0
},
" ean " : {
" columnIndex " : 1
},
" pname " : {
" columnIndex " : 2 ,
" funcSortMap " : {
" desc " : function ( a , b ) {
if ( a . key == b . key ) return 0 ;
else if ( a . key > b . key ) return - 1 ;
else return 1 ;
},
" asc " : function ( a , b ) {
if ( a . key == b . key ) return 0 ;
else if ( a . key > b . key ) return 1 ;
else return - 1 ;
}
}
},
" qty " : {
" columnIndex " : 3
},
" amt " : {
" columnIndex " : 4
}
}
});
結語
整個修改動作很快,而且對原有程式的改動幅度意外地小。原有統計程式只改了 View 的部份,且只改了兩行。加上一行 <script type="text/javascript" src="sorter.js"></script>
,再將表格欄位標題的輸出內容改成連結。
現在有很多 JavaScript 的套件提供這些 GUI 視覺元件內容。不過像這種簡單的功能倒也不見得要用那些套件來做。更重要的是,不論用什麼套件,都應該遵循程式、資料、樣式等內容分離的原則,也不要配合套件來決定程式如何寫。本文能夠如此簡便地在最小改動幅度下實踐排序功能,便歸功於分離原則。
附帶一提,當排序資料很多時,瀏覽器會陷入不回應狀態。而底下的程式碼則是另一種版本的 DOM 節點更新程式碼。上述版本直接操作顯示中的節點,而下列版本則是先配置一個 tbody 節點,於背景排放資料列內容後,再替換整個 tbody 節點。理論上較快,但我實際使用時... 測不出效能差異 (IE, Firefox and Opera)。
function doSort ( keyCellIndex , sortFunc ) {
var rows = getRows . call ( this );
var keys = [];
for ( var i = 0 , cell , row = rows [ 0 ]; row ; row = rows [ ++ i ]) {
cell = row . getElementsByTagName ( ' td ' )[ keyCellIndex ]. firstChild . nodeValue ;
keys [ i ] = {
' key ' : cell ,
' row ' : row
}
}
keys . sort ( sortFunc );
var tbody = getTbody . call ( this );
for ( var i = 0 , fr , r = keys [ i ]; r ; r = keys [ ++ i ]) {
tbody . appendChild ( r . row );
}
var newTbody = document . createElement ( ' tbody ' );
for ( var i = 0 , newRow , r = keys [ i ]; r ; r = keys [ ++ i ]) {
newRow = r . row . cloneNode ( true );
newTbody . appendChild ( newRow );
}
var tbody = getTbody . call ( this );
tbody . parentNode . replaceChild ( newTbody , tbody );
}
相關文章
樂多舊網址: http://blog.roodo.com/rocksaying/archives/2854997.html
樂多舊回應