最近更新: 2007-03-14

以動態網頁方式排序網頁的表格內容

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

樂多舊回應
未留名 (#comment-4180321)
Thu, 15 Mar 2007 11:26:22 +0800
javascript:sort method 似乎對中文排序會有問題
未留名 (#comment-4182437)
Thu, 15 Mar 2007 16:02:58 +0800
忘了提,文字的排序方式和數字不一樣。JavaScript 預設的排序方式是文字遞增(只有遞增,遞減要自己寫)。但我的主要需求是數字排序,所以我程式中預設的排序方式是數字排序。

文字排序方式為:
desc = function(a,b) {
if (a == b)
return 0;
else if (a > b)
return -1;
else
return 1;
}

asc = function(a,b) {
if (a == b)
return 0;
else if (a > b)
return 1;
else
return -1;
}

可以在生成 sorter 的引數中,加上指定欄位排序方式的設定值。

送佛送到西,這個動作加到本文中了。可以比較重整前的第33行,與重整後的第28-31行。
未留名 (#comment-9823705)
Thu, 26 Apr 2007 16:16:38 +0800
請問您的code可以使用嗎?(非商業用途)

另外再請教石頭成,請問您的javascript是看哪些資料學的呢, 在一般書上都沒看過您使用的語法
未留名 (#comment-9877065)
Fri, 27 Apr 2007 13:41:43 +0800
程式碼皆採自由軟體授權你或任何人使用。請參考 授權內容說明。用於程式時,必須選擇 LGPL 或 GPL。
簡單說,你必須在我的程式碼部份加上作者姓名及LGPL 授權聲明: Copyright (C) Shih Yuncheng. Program is issued on under GNU LGPL.

關於 JavaScript ,請看 重新認識 JavaScript。一般書上沒有的原因,我只能說那些作者不夠認真看待 JavaScript ,沒有學好它。
mpe.mpe@gmail.com(mpe) (#comment-14371377)
Sun, 02 Sep 2007 02:17:53 +0800
我想請問一下a.key - b.key怎麼讓 keys.sort(sortFunc);
帶進去的,有點不暸,a.key - b.key又代表什麼呢?
未留名 (#comment-21458179)
Wed, 15 Dec 2010 16:14:18 +0800
> 為什麼我將引用你所寫的排序引用當我的程式確一直執行不了 畫面還是一直停在原本資料面並無排序

你大概沒有指示正確的 id 與 class 給它,所以它不知道你要排序的東西在哪。

我的程式是認 tbody 的 id 得知要更新的資料範圍在哪。認 thead - td 的 class 得知要排序的欄位是哪個。

此外,jQuery, Dojo 這些 JavaScript framework 都已經實作了更好的排序UI。你不妨試試。但是它們的使用概念和我這裡示範的一樣,都要指示正確的名稱。