最近在嘗試將一個功能強大的jQuery Plugin【jqGrid】整到專案裡,它的功能與彈性讓人印象深刻(不過要上手得花點時間摸索),大家可以直接看線上展示,應該就能感受其威力。

jqGrid有個貼心的設計--將功能模組化。各模組的程式分散在多個js檔,有用到才需要載入,避免Client端載入肥大js只使用其中一丁點功能,白白浪費載入時間及頻寬。(剛好前些時候James Padolsey也提到這點,甚至覺得殺雞不必用牛刀,小功能或許自己寫會更有效率兼便練功)

jqGrid提供了兩種做法: 線上挑選模組後打包成單一js檔,或透過jquery.jqGrid.js動態載入。在開發階段我選擇了動態載入法,卻發現疑似因載入時間差的關係,Reload多次時,偶爾會出現js來不及載入而出錯,於是決定改回靜態include。結果在HTML檔出現很壯觀的景象:

當然,這是極度誇張的特例,但依過去的經驗裡,網頁用的plugin種類一多,先後include十來個js檔也是很平常的。這表示使用者開啟網頁的同時,還一併丟出十來個Request,每個Request都有傳輸與處理的Overhead,平白增加伺服器負擔。同時,用Copy And Paste的方式在每個網頁裡維護又臭又長的include清單,顯然也有違程式簡潔性並且不易修改。

於是,我想要改善這個問題。發噗提出想法後,不少噗友提供了很好的意見。市場上有些現成的解決方案,例如: YUI可以壓縮合併JS/CSS、Microsoft實驗室則有DOLOTO、另外還有些朋友習慣用MasterPage解決這個問題。

考量了一下,我理想中的合併下載工具應該透過單一ASPX網頁就能解決,在運用上會比引用MasterPage來得靈活一點(提供輔助工具會比改變網頁架構容易些,而且不侷限於ASPX,HTM、JS裡也可以引用),也有較大的組合彈性;至於DOLOTO,看起來很強大,但引用過程較繁瑣,且不確定jQuery裡的函數/Plugin被分割肢解+動態載入會不會出問題;至於YUI,好像比較傾向靜態合併,而且ASP.NET + jQuery外再多扯進來另一個Framework有點像在找其他同事麻煩,尤其是我只是想喝杯牛奶罷了。

評估程式邏輯並不複雜,決定立刻捲起袖子動手做,最壞只是損失一些工時。沒一會兒,JsLoader.aspx誕生了:

<%@ Page Language="C#" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.Collections.Generic" %>
<script runat="server">
    private List<string> jsQueue = new List<string>();
    private Dictionary<string, string> jsPool = new Dictionary<string, string>();
    private string JS_SET_PREFIX = "JSS_";
    void Page_Load(object sender, EventArgs e)
    {
        string[] f = (Request["f"] ?? "")
            .Split(new char[] {',',';'}, StringSplitOptions.RemoveEmptyEntries);
        foreach (string jsFile in f)
            queueJs(jsFile);            
        Response.ContentType = "text/javascript";
        foreach (string js in jsQueue)
            Response.Write(jsPool[js]);
        Response.End();
    }
    private void queueJs(string jsFile) 
    {
        string[] p = jsFile.Split(new char[] {',',';'}, 
            StringSplitOptions.RemoveEmptyEntries);
        //if multi-part
        if (p.Length > 1)
        {
            foreach (string f in p)
                queueJs(f);
            return;
        }
        //lower case
        jsFile = jsFile.ToLower();
        //js set name without .js
        if (jsFile.EndsWith(".js"))
        {
            //if already queued
            if (jsPool.ContainsKey(jsFile)) return;
            //else put the js into queue
            jsQueue.Add(jsFile);
            jsPool.Add(jsFile, getJsContent(jsFile));
        }
        else //if set, try to find it from web.config
        {
            string jsFiles =
                System.Configuration.ConfigurationManager.AppSettings[
                    JS_SET_PREFIX + jsFile];
            if (string.IsNullOrEmpty(jsFiles))
            {
                jsQueue.Add(jsFile);
                jsPool.Add(jsFile,
                    string.Format("alert('JsLoader Error: [{0}] set not configured!');",
                    EscapeStringForJS(jsFile)));
            }
            else //process the predefined file list 
                queueJs(jsFiles); 
        }
    }
    //Get js file content
    private string getJsContent(string jsFile)
    {
        try
        {
            string file = Server.MapPath("./" + jsFile);
            if (!File.Exists(file))
                return string.Format("alert('JsLoader Error: [{0}] not found!');",
                    EscapeStringForJS(jsFile));
            else //Add cache/packing module here if you want
                return File.ReadAllText(file);
        }
        catch
        {
            throw new ApplicationException("Failed to process " + jsFile);
        }
    }
    /// <summary>
    /// Replace characters for Javscript string literals
    /// </summary>
    /// <param name="text">raw string</param>
    /// <returns>escaped string</returns>
    public static string EscapeStringForJS(string s)
    {
        return s.Replace(@"\", @"\\")
                .Replace("\b", @"\b")
                .Replace("\f", @"\f")
                .Replace("\n", @"\n")
                .Replace("\0", @"\0")
                .Replace("\r", @"\r")
                .Replace("\t", @"\t")
                .Replace("\v", @"\v")
                .Replace("'", @"\'")
                .Replace(@"""", @"\""");
    }    
</script>

JsLoader.aspx的工作原理很簡單,程式放在js同層目錄下,網頁裡寫成<script type="text/javascxript" src="/img/loading.svg" data-src="/js/JsLoader.aspx?f=js1.js,js2.js"></script>就可以動態將js1.js與js2.js兩個檔案合併成一個傳回。而在getJsContent()裡,還可以加入Cache及壓縮js的機制(例如: 加參數就改成載入壓縮過的.min.js版本),讓效率更好一些。不過,我加入最重要的簡化是除了直接列出js檔名外,可在/js/web.config裡定義一些預設的"套件",這樣子就可以用JsLoader.aspx?f=jqgrid取代原本一長串js檔案清單,而多組"套件"也可以再組成一個"套餐"(如JSS_gridpage),如某幾個網頁有共同的js載入需求,可以只宣告一次,用在多個網頁上。檔案清單、套件、套餐三種方式可以視需要自由組合運用。

<?xml version="1.0"?>
<configuration>
  <appSettings>
    <!-- Use JSS_[JS Set Name], JS Set Name should be lower case -->
    <add key="JSS_jqgrid" 
value="jquery-ui-1.7.1.custom.min.js,i18n/grid.locale-tw.js,...略...,jqModal.js,jqDnR.js" />
    <add key="JSS_nummask" value="jquery.afaNumMask.js" />
    <add key="JSS_dyndatetime" value="jquery.dynDateTime.js,../css/calendar/calendar-tw.js"/>
    <add key="JSS_json2" value="json2.js"/>
    <add key="JSS_gridpage" value="jqgrid,json2,nummask,dyndatetime"/>
  </appSettings>
</configuration>

最後還是要講一下JsLoader.aspx的黑暗面,將多個js檔包在一起的做法,固然可減少Request次數,但js檔過於肥大,會讓載入時間變久,而Debug時js檔太大,Debugger處理起來效率不佳。過與不及都不是好事,尺度怎麼拿捏,大家見機行事吧!

PS: 順道推一下小喵介紹的dynDateTime Plugin,我從ASP時代就一直仰賴它的前身解決日期選擇問題,如今有了jQuery Plugin版本,自然要力挺到底了!!


Comments

# by Ark

幾個經驗 .net 不就有現成的 ScriptManager1.CompositeScript.Scripts 可以玩? 子頁換ScriptManagerProxy 缺點是操作的script 不能放在head 要擺在 runat server 之後 MySql 有 limit 語法這點~MSSql 一直沒有較有效能的語法對應不論是TOP xxoo 或是 rownum over 3小叮噹~我指的是好幾十萬筆的資料 jqgrid 預設的XML 肥 json的格式不直接套Array回client再另外loop處理~封包也是肥了點 再來是 jqgrid的事件綁定 Demo 的資料筆數少感覺不出來 但當資料是2-300筆*10多攔時就會有頓頓的情況了~估計換成live的綁法會改善 恩~喝牛奶清宿便嗎?

# by chicken

我也有類似的 solution... 現個寶, 解法跟你一樣,不過用的時後是包裝成控制項... 下列控制項就放在 head 裡就可以了: <ch:script runat="server"> <js src="01.js" /> <js src="02.js" /> <js src="03.js" /> </ch:script> 另外 CSS 也有對應的版本: <cs:style runat="server"> <css src="01.css" /> <css src="02.css" /> <css src="03.css" /> </cs:style> 其它用程式動態追加檔案就不提了。不過這類方法不宜用太多,用過頭我就直接去用 script manager 了

# by Jeffrey

to Ark, 謝謝賜教。恰巧也有噗友提到CompositeScript,其實這個做法的構想多少也來自它。不過我把機制包裝得可以如同一般js可以直接include或$.getScript,主要是讓應用時更有彈性,同時加上套件與套餐的概念也有些許簡化的效果。 至於jqGrid,雖然還沒試過數百列資料,但我對大量Cell數的Table效能不抱什麼期望(過去有過類似的嘗試,最後還是投靠了Silverlight),主要用來提供同一頁面上數筆資料編輯(例如: 交通費申報時的明細項目),結果再一次傳回後端。 to chicken,有理,JsLoader.aspx應該也可以再多加上CSS支援。

# by 海角147號

這個 jqGrid 的 Demo 真的滿屌的! 不過我是自己用JavaScript開發了一組AJAX-Framework Browser端的DataTable物件 以及 DataGridExp 物件 用DataTable去取得資料 ImportXML 也是使用 Pure JavaScript 開發的 用DataGridExp去顯示DataTable的資料..!! 可以自己設計GridTemplate 以及PageControlTemplate ..^^..實際開發的範例在這裡 http://www.lativ.com.tw/Home/?PID=C65EE0 大家有空可以上去參觀參觀!

# by Bryant

Jeffrey大,跪求一個簡單的asp.net 使用jqGrid的範例,我用一個gridview bind好資料後(資料來源是sql server)想套用jqGrid,但總是不成功,可否請你提點一下,感激不盡。

# by Jeffrey

to Bryant, 我自己應用jqGrid都是以AJAX方式供應資料來源(JSON),jqGrid再將資料轉化成HTML Table;GridView Bind完資料後就已是現成的HTML Table了,似乎與jqGrid原始的應用方向不同。或許你可以考慮用jqGrid的ASP.NET控件版取代GridView(http://www.trirand.net/demoaspnet.aspx)

# by Nick

請問有辦法在jqGrid第一行tile折行嗎? EX: 身分證字號 => 身分證 字號

# by Jeffrey

to Nick, 依我的看法,得透過jQuery事後選取標題所在HTML元素,再更動其.html()插入<br />的方式實現。

Post a comment