以前我使用JSMin的時候,都是從http://fmarcia.info/jsmin/這里打開執行頁面,然后把自己的代碼粘貼過去,再把減肥后的代碼復制回文本編輯工具、保存。
久而久之,我發現這樣實在是太麻煩了!既然我們是程序員,為何不自己動手把事情變得簡單一點呢?
因此我開始了對JSMin進行“友好化”的工作。
而在進行“友好化”工作的過程中,“不出意料”地遇到了一些意想不到的問題,馬上我就講遇到的是哪些問題、最后怎樣解決。
不過由于是在一切問題都解決之后才想起來寫文的,所以很抱歉,這次是“無圖無真相”。
在開始進行講解之前,我先說明一下這次問題所涉及到的一些技術知識――如果你具有Windows腳本宿主的編程經驗,那么這部分你可以跳過;如果你具有ASP的經驗,那么這部分可能有一些內容你已經知道了。
Windows Script Host(WSH)即Windows腳本宿主,表現為一些可執行文件(wscript.exe和cscript.exe),它們的功能是在不受Internet安全策略限制的情況下執行用JScript或VBScript編寫的任務文件,從而進行一些系統管理工作。值得注意的是,默認情況下Windows中以.js為擴展名的文件都被認為是WSH文件,如果你嘗試雙擊一個.js文件,你可能會發現一些錯誤提示,或者被殺毒軟件攔截。
HTML Application簡寫為HTA,顧名思義,是“HTML應用程序”――它和Windows腳本宿主一樣在相當寬松的安全條件下執行腳本,和Windows腳本宿主不同的地方是HTML應用程序往往具有較好的圖形界面。本質上說一個HTA文件其實就是使用了特殊擴展名的HTM文件,如果你不想在制作界面上花費太多的精力,而又正好同時具有HTML和WSH經驗,那么使用HTML Application來解決問題是一個相當不錯的選擇。需要注意的是HTML Application也常被認為是WPF技術的前身,如果你的目標使用環境具有WPF支持(即.NET框架3.0+),那么使用WPF可能更加適合。HTA文件因為易于創作而一度被用于木馬下載器,時至今日可能仍有一些殺毒軟件粗暴地將HTA文件判斷為木馬下載器。就像.exe這樣的二進制可執行文件一樣,一個HTA文件是否有害是由自身設計決定的,而不是被文件類型先天決定。
FileSystemObject,簡稱FSO,是Windows腳本技術中為了方便腳本進行文件系統操作而提供的組件對象,在ASP編程中也很常見。
WshShell,是WSH運行環境為了方便腳本操作Shell相關物件而提供的組件對象,很多WSH程序都使用了這一對象。
ADODB.Stream,有的時候也被稱作“ADO Stream”,是ADO中為了方便操作二進制數據流而提供的組件對象,但也經常被用在數據庫以外的地方,例如文件操作。
“只要在文件圖標上點點鼠標就可以”這樣的功能特性被稱作“外殼關聯”之類的東西,很多時候大家對外殼關聯的認知是“關聯到鼠標雙擊”什么的。但稍微留心一點的人都會發現在點擊鼠標右鍵彈出的快捷選單中,除了默認的指令以外,還有一些其它的像是“編輯”、“打印”之類的指令。
而我們這一次要做的,就是為.js文件增加一個“Minimize”指令,當我們點擊這個指令的時候就會啟動JSMin給我們的ECMAScript代碼減肥。
就在這里,我遇到了第一個問題:
我編寫了一個.js文件(實際上是一個WSH任務文件),用它來實現“自動化”地“安裝”。
在Windows中為.js文件增添文件關聯,需要在HKCR/.js這個注冊表項的“默認”值所對應的“JSFile”――HKCR/JSFile項之下的Shell項添加子項,以及從屬的名為Command的子項。
在HKCR/JSFile/Shell下面添加了Minimize項并為Command子項設置了“我的hta文件路徑 "%1"”值以后,我發現使用這個Minimize指令后會產生一個“不是合法的可執行文件”這樣的錯誤,而如果在前面添加了start指令,又出現了“打開方式”對話框……
看起來在Shell項下面這么投機取巧好像是不行,所以我只好先讀取htafile的文件類型設置,然后將它設置到剛才新增加的Minimize指令中,.js文件中是這樣寫的:
view sourceprint?1 var asocCommand = wshShell.RegRead("HKEY_CLASSES_ROOT//htafile//Shell//Open//Command//").replace("%1", instPath + "http://" + appExec).replace("%*", '"%1"');
外殼關聯的問題剛解決,緊接著就是命令行參數的解析問題:
最初我打算用WSH任務文件作為這個小工具的載體,但馬上我發現默認情況下WSH文件缺乏UI支持――VBScript中的InputBox和HTML中的prompt在用JScript編寫的WSH任務文件中是不存在的,如果堅持使用WSH就只能通過Windows控制臺來獲得用戶輸入。而如果通過控制臺來獲得用戶輸入的話,這個工具的標準使用流程就變成了“鼠標點擊文件圖標――快捷選單――Minimize――鍵盤輸入一個字符”,在一系列鼠標操作(當然,用鍵盤操作也是可能的)之后突然改用鍵盤,這好像不太對勁。
因此,我選擇了使用HTA這種可以提供豐富界面的文件類型來作為實現這個工具的途徑。
而選擇了HTA,也就意味著同樣要失去WSH“不外傳”的一些特性,例如缺少了用于解析命令行參數的“Arguments”對象。
而在我的構思中,應該是可以通過-level參數指定JSMin的代碼縮減等級、通過-silent參數來關閉提示信息的。如果不能從命令行中解讀出這些參數,這些功能就沒有辦法實現。
因此我編寫了兩個函數:
//========//========
// parse command-line info
// 在HTA環境中從命令行中解讀出被執行的HTA的實際路徑和附加的參數
// 此部分代碼由NanaLich原創,您可以不經書面許可在任何情況下直接使用。您不應擅自聲稱您或您的所屬機構創作了這些代碼,也不應該擅自以NanaLich的名義發布修改版本。
//========//========
function namedOrNot(args) {
var named = {}, not = [], c;
for(var i = 0; i < args.length; i++) {
c = args[i];
switch(c.charAt(0)) {
case "-":
case "/":
c = c.substring(1);
if(c.indexOf("=") > 0) {
c = c.split("=");
named[c.shift()] = c.join("=");
} else if(c.indexOf(":") > 0) {
c = c.split(":");
named[c.shift()] = c.join(":");
} else {
// 不能確定一個命名參數是不是也接受附加參數,這是個未解難題
//i++;
named[c] = args[i + 1];
}
break;
default:
not.push(c);
break;
}
}
args.named = named;
args.unnamed = not;
}
function parseArgs(str) {
var a = [], q = false, c = "", $ = "";
function mit() {
if(c)
a.push(c);
}
for(var i = 0; i < str.length; i++) {
$ = str.charAt(i);
if($ == '"') {
q = !q;
} else {
if($ == " " && !q) {
mit();
c = "";
} else {
c += $;
}
}
}
mit();
namedOrNot(a);
return a;
}
//========//========
這樣,只要將HTA:Application對象的commandLine屬性傳入這個函數,就可以獲得解析之后的命名和不命名參數了。
解決了命令行參數的問題之后,接下來就是文件的編碼問題。
在我們實際的Web開發過程中,我們可能因為各種各樣的原因使用“非Unicode區域編碼”和“Unicode(UTF-16)編碼”以外的其它編碼格式,例如“Unicode(UTF-16) Big Endian”和“UTF-8”這兩種編碼方式。
如果我們通過既有的文本編輯工具來復制、粘貼代碼,我們只要在保存文件的時候選擇編碼類型就可以了;但現在我們正在設計一種免去“打開――復制――粘貼――復制――粘貼――保存”這樣繁瑣的操作步驟的工具,我們就需要在這個工具中設計自動適應編碼類型的功能。
很遺憾,在我的測試中FSO和ADODB.Stream都不具備自動識別文本編碼的能力,必須另想其它辦法――幸好,在HTA中VBScript也是默認支持的,VBScript雖然沒有直接對字節組進行操作的功能,但在VBScript中把字節組當作字符串來進行操作仍然可以在一定程度上滿足我們的要求。
因此,我編寫了下面這個函數:
Function vbDetectFileEncoding(fn)
Dim Stream, B3
Set Stream = CreateObject("ADODB.Stream")
Stream.Type = 1
Call Stream.Open()
Call Stream.LoadFromFile(fn)
B3 = CStr(Stream.Read(3))
Call Stream.Close()
Set Stream = Nothing
Dim L1
L1 = Left(B3, 1)
If (L1 = ChrW(&hFEFF)) Then
vbDetectFileEncoding = "unicode"
Exit Function
Elseif (L1 = ChrW(&hFFFE)) Then
vbDetectFileEncoding = "unicodeFEFF"
Exit Function
Elseif B3 = (ChrB(&hEF) & ChrB(&hBB) & ChrB(&hBF)) Then
vbDetectFileEncoding = "utf-8"
Exit Function
End If
vbDetectFileEncoding = defEncoding
End Function
這個函數根據一個文本文件中可能存在的BOM來推斷文件所采用的編碼方式。
這里需要注意的一點是:
ADODB.Stream的相關文檔中寫道Allowed values are typical strings passed over the interface as Internet character set names (for example, "iso-8859-1", "Windows-1252", and so on). For a list of the character set names that are known by a system, see the subkeys of HKEY_CLASSES_ROOT/MIME/Database/Charset in the Windows Registry.,而注冊表中和“Unicode Big Endian”(Encoding 1201)相對應的項目是“unicodeFFFE”,從這些信息上推斷在ADO Stream中使用“Unicode Big Endian”編碼時應該指定Charset屬性為“unicodeFFFE”;
這里有一個容易混淆的實施是:BOM字符的Unicode編號是U+FEFF,而“一般的”“Unicode編碼”實為“Unicode Little Endian”――BOM字符會被寫成“FF FE”這樣兩個字節,而在“Unicode Big Endian”中才會被寫成“FE FF”。
那么到這里,問題就出現了――如果和“unicodeFFFE”正相反的是“unicodeFEFF”的話,我們可以理解這里面的“FEFF”是BOM字符的Unicode編號;但與此同時代表“Unicode Big Endian”的“unicodeFFFE”中的“FFFE”具有什么含義呢?顯然這不可能是“一個Unicode編號為U+FFFE的字符”的意思。
而在實踐中,我發現無論為Charset屬性設置“unicode”還是“unicodeFFFE”,輸出的文件都是采用“Unicode (Little Endian)”編碼的,這顯然和文檔所表達的意思不符。
當我再進一步作出嘗試的時候,卻發現如果希望輸出“Unicode Big Endian”編碼的文件,Charset屬性應該設置為“unicodeFEFF”――我們很容易發現,“FEFF”正好符合BOM字符在文件中的字節順序;我們同樣可以發現為Charset屬性設置“unicodeFXFX”時其實就相當于“用FXFX這樣的字節順序來進行編碼”的意思,是享受特殊對待的,并不完全符合注冊表中所寫的樣子。
到現在為止,小工具已經可以讀寫“Unicode”、“Unicode Big Endian”、“UTF-8”和“非Unicode區域編碼”這些編碼方式的文件了,只是我仍然沒想明白為什么文檔和注冊表中會寫著有誤或者完全無用的信息……
上面這些問題被一一解決以后,好像就再也沒有什么值得研究的問題了。
但是在使用JSMin的過程中,卻發現了另一個問題:
有些人(比方說我)主要使用Windows工作,Windows中文本文件的默認行分隔符號是CR LF對。
而JSMin會把所有的CR都替換成LF,這樣的話CR LF對就變成了兩個LF的“LF LF”對了。
按照JSMin本來的設計,控制字符LF通常都是要被縮減掉的,因此兩個連續的LF也不是什么問題;但jsmin.js有一個新增的功能是保留具有一定特征(/*! ... */)的“重要注釋”,而如果在這種“重要注釋”中存在CR LF對的話,最終會變成無法去除的兩個LF控制字符,這可不好。
為了解決這個問題,我對jsmin.js稍微作了一些修改……不過因為JSMin有點超出我的理解能力,我也只能把CR變成空格,不過好在這樣做在Windows中也有一些好處(在大部分的Windows版本中,單獨的LF控制字符在“記事本”程序中幾乎無法觀察到,用它分隔的兩行文本看起來也像是粘在了一起),就這么湊合用吧……這部分修改我就不單獨列出來了。
本文中所提到的一切代碼,都可以在這個壓縮包中找到。
壓縮包內包含三個文件:install.js是注冊文件關聯的“安裝”腳本、jsmin.hta是使用“Minimize”指令時實際運行的“應用程序”、jsmin.js則是我修改以后的jsmin.js。
將三個文件解壓至同一個文件夾之后,雙擊install.js即可安裝本工具。如果您重新安裝了操作系統,您可能會發現工具仍然遺留在您的個人文件夾中;只要您雙擊個人文件夾中遺留下來的install.js,您就又可以使用本工具了。
注意:自Windows XP起,較新版本的Windows會為從網絡上下載而來的文件設置一個標志,這個標志可能會讓HTA文件不能正常執行,如果你在使用的時候碰到了這樣的問題,請點擊文件屬性對話框中的“解除鎖定”按鈕以去除這個標志。
更新:修改了install.js?,F在在64位Windows 7上也可以正確安裝了;安裝以后也不用手動“解除鎖定”了。
秘密在這里:
var appsPath = wshShell.ExpandEnvironmentStrings(wshShell.RegRead(regUSF + "Personal")) + "http://Scriptlet";
try{
fso.OpenTextFile(instPath + "http://" + appExec + ":Zone.Identifier", 1).Close();
fso.OpenTextFile(instPath + "http://" + appExec + ":Zone.Identifier", 2).Close();
}catch(ex){ }
文件打包下載 文件附一個改名的jse.方便經常開發js的朋友,以免混淆。
因為好多朋友是用的win2003開發,.js文件使用普通文本打開的,不可能以后用js都讓運行吧,直接改成install.jse即可運行了,呵呵。
感謝作者發布這么好的東西。作者的blog地址
http://www.cnblogs.com/NanaLich