輕量級的js檔打包解決方案
| | 8 | | ![]() |
最近在嘗試將一個功能強大的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 />的方式實現。