HTA版JSMin(省略修饰语若干)基于javascript语言编写


Posted in Javascript onDecember 24, 2009

以前我使用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
Javascript 相关文章推荐
javascript 定义初始化数组函数
Sep 07 Javascript
js 上下左右键控制焦点(示例代码)
Dec 14 Javascript
jquery五角星评分插件示例分享
Feb 21 Javascript
jQuery+JSON实现AJAX二级联动实例分析
Dec 18 Javascript
Jquery Easyui搜索框组件SearchBox使用详解(19)
Dec 17 Javascript
js实现自定义路由
Feb 04 Javascript
Angular.js实现动态加载组件详解
May 28 Javascript
Angular.JS中指令ng-if的注意事项小结
Jun 21 Javascript
vue-cli如何添加less 以及sass
Jul 06 Javascript
详解Vue+axios+Node+express实现文件上传(用户头像上传)
Aug 10 Javascript
函数式编程入门实践(一)
Apr 20 Javascript
用React Native制作一个简单的游戏引擎
May 27 Javascript
javascript下arguments,caller,callee,call,apply示例及理解
Dec 24 #Javascript
关于Aptana Studio生成自动备份文件的解决办法
Dec 23 #Javascript
window.js 主要包含了页面的一些操作
Dec 23 #Javascript
js 效率组装字符串 StringBuffer
Dec 23 #Javascript
jquery 表单取值常用代码
Dec 22 #Javascript
JavaScript是否可实现多线程  深入理解JavaScript定时机制
Dec 22 #Javascript
JavaScript 图片预览效果 推荐
Dec 22 #Javascript
You might like
bindParam和bindValue的区别以及在Yii2中的使用详解
2018/03/12 PHP
JQuery 表单中textarea字数限制实现代码
2009/12/07 Javascript
对javascript的一点点认识总结《javascript高级程序设计》读书笔记
2011/11/30 Javascript
js判断浏览器类型为ie6时不执行
2014/06/15 Javascript
jQuery中ScrollTo用法示例
2016/09/04 Javascript
javascript构造函数以及原型对象的理解
2017/01/13 Javascript
微信小程序 开发MAP(地图)实例详解
2017/06/27 Javascript
vue.js项目打包上线的图文教程
2017/11/16 Javascript
Bootstrap实现下拉菜单多级联动
2017/11/23 Javascript
JavaScript+H5实现微信摇一摇功能
2018/05/23 Javascript
在iFrame子页面里实现模态框的方法
2018/08/17 Javascript
vue-cli3.0配置及使用注意事项详解
2018/09/05 Javascript
基于Vue+elementUI实现动态表单的校验功能(根据条件动态切换校验格式)
2019/04/04 Javascript
bootstrap-table formatter 使用vue组件的方法
2019/05/09 Javascript
Vue 实现输入框新增搜索历史记录功能
2019/10/15 Javascript
vue项目如何监听localStorage或sessionStorage的变化
2021/01/04 Vue.js
详解node.js创建一个web服务器(Server)的详细步骤
2021/01/15 Javascript
浅谈flask截获所有访问及before/after_request修饰器
2018/01/18 Python
Python递归函数实例讲解
2019/02/27 Python
Python Web框架之Django框架Form组件用法详解
2019/08/16 Python
简单了解python协程的相关知识
2019/08/31 Python
python 实现兔子生兔子示例
2019/11/21 Python
使用Django和Postgres进行全文搜索的实例代码
2020/02/13 Python
idealfit英国:世界领先的女性健身用品和运动衣物品牌
2017/11/25 全球购物
美国汽车性能部件和赛车零件网站:Vivid Racing
2018/03/27 全球购物
运行时异常与一般异常有何异同?
2014/01/05 面试题
留学自荐信的技巧
2013/10/17 职场文书
幼师自荐信
2013/10/26 职场文书
给护士表扬信
2014/01/19 职场文书
三年级学生评语
2014/04/23 职场文书
教师一帮一活动总结
2014/07/08 职场文书
一份关于丢失公司财物的检讨书
2014/09/19 职场文书
学习普通话的体会
2014/11/07 职场文书
倡议书作文
2015/01/19 职场文书
《艾尔登法环》发布最新「战技」宣传片
2022/04/03 其他游戏
Go语言编译原理之变量捕获
2022/08/05 Golang