以前我使用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 + "\\" + 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。現在在64位Windows 7上也可以正確安裝了;安裝以後也不用手動“解除鎖定”了。
秘密在這裡: 複製代碼 代碼如下:var appsPath = wshShell.ExpandEnvironmentStrings(wshShell.RegRead(regUSF + "Personal")) + "\\Scriptlet";
try{
fso.OpenTextFile(instPath + "\\" + appExec + ":Zone.Identifier", 1).Close();
fso.OpenTextFile(instPath + "\\" + appExec + ":Zone.Identifier", 2).Close();
}catch(ex){ }
檔案打包下載 檔案附一個改名的jse.方便經常開發js的朋友,以免混淆。
因為好多朋友是用的win2003開發,.js檔案使用普通文本開啟的,不可能以後用js都讓運行吧,直接改成install.jse即可運行了,呵呵。
感謝作者發布這麼好的東西。作者的blog地址
http://www.cnblogs.com/NanaLich