使用Node.js编写Shell脚本,暨Jscex 0.6.5版本发布
2012-06-19 00:12 by 老赵, 5918 visits昨天不得不花时间做了点保护博客阅读体验的事情,但其实这篇才是我真正想写的。上个星期在香港出差,晚上的活动大都是喝酒,回到酒店便借着些许酒劲改进Jscex。如今虽然Jscex的开发工作并没有详细的时间计划,但我正在使用GitHub的Issues页面记录需要制作的任务点,因此每天都是朝着目标逐步前进的。按照计划,Jscex的0.6.5的主要目标是对Jscex的模块机制进行改进,统一辅助方法,并使用Node.js重新编写发布脚本。这些工作的目的都是为接下来的0.7.0版本作准备,它将会是Jscex在项目功能与质量,以及专业性上有重大突破的版本。
Jscex的模块化划分十分细致,目前正式对外发布的也已经有六个模块(还有一个是命令)。模块化的目的是为了选择性的加载,例如在前端开发过程时需要加载所有的模块,而真正部署时则无需最大的两个模块:JIT编译器及解析器(前者依赖后者),这既照顾到了JavaScript编程体验,也考虑到前端部署时的尺寸。但是模块化便对模块管理提出了要求,这在Node.js平台下问题不大,因为NPM自带包管理功能,但在浏览器上便没有那么好的支持了。例如,模块依赖时还会涉及到版本,版本过高或者过低都是问题,如何在模块没有加载,或是加载了错误的版本时清晰地告诉用户,这便是0.6.5版本中想要解决的问题。此外,Jscex还直接支持不同的包加载环境(如AMD环境,之前我也谈过这方面的单元测试),这也是模块机制的重要责任。有了核心模块机制以后,各模块只需要注册自己的信息以及初始化方法,之前提过的错误提示,包加载环境支持等功能,便可以一并使用了。之后如果需要修复问题,或提供更多的功能,也只需要修改这个核心模块机制——简单地说,其实就是DRY原则。
更重要的一点是,这个各个模块在使用这个模块机制的时候,其实已经提供了自己的元信息,例如模块名称,当前版本,它所依赖的模块及其版本等等。换句话说,在发布这些脚本的时候,我们完全可以直接读取这些信息,而无需额外的配置文件。因此,我也使用Node.js替换了原来的Shell编写发布脚本。使用Node.js来代替Shell脚本在某些情况下的确会更为方便,对于一些常见逻辑表达(条件判断,循环等等)或是操作(文件处理,字符串分析等等)来讲,JavaScript语言的可读性无疑会比Shell高很多(因此也有很多人使用Ruby或Python来代替Shell脚本),对于熟悉JavaScript的同学来说则更不用说了。
大量使用Node.js来编写脚本并不如想象中的麻烦,它受JavaScript异步特性的影响并不大,这是因为Node.js标准库里包含了许多同步的方法,使用这些同步方法便可以完成绝大部分工作了,例如:
"use strict"; var path = require("path"), fs = require("fs"), utils = require("../lib/utils"), Jscex = utils.Jscex, _ = Jscex._; var devDir = path.join(__dirname, "../bin/dev"); var srcDir = path.join(__dirname, "../src"); if (path.existsSync(devDir)) { utils.rmdirSync(devDir); } fs.mkdirSync(devDir); var coreName = "jscex-" + Jscex.coreVersion + ".js" utils.copySync(path.join(srcDir, "jscex.js"), path.join(devDir, coreName)); console.log(coreName + " generated."); var moduleList = ["parser", "jit", "builderbase", "async", "async-powerpack"]; _.each(moduleList, function (i, module) { var fullName = "jscex-" + module; var version = Jscex.modules[module].version; var outputName = fullName + "-" + version + ".js"; utils.copySync(path.join(srcDir, fullName + ".js"), path.join(devDir, outputName)); console.log(outputName + " generated."); });
以上便是发布开发版Jscex的脚本,可见其中每一步IO操作,例如判断目录是否存在,删除/创建目录以及复制文件,都使用了同步的版本。不过,Node.js并不能够完全代替Shell脚本,因为Shell脚本有其重要的武器:管道。试想,你如何使用JavaScript来实现下面这行Shell脚本所做的事情?
cat /data/logs/run.log | grep 'node' | grep -v 'innode' | awk {'print $2'} | xargs sudo kill -9
因此,有时候我们不可避免要在Node.js中调用Shell脚本。同理,我们总会有需要调用外部程序的时候,例如,在发布生产环境的Jscex时,我需要使用Google Closure Compiler来压缩脚本体积,这就需要使用exec来执行外部命令。可惜的是,exec方法没有同步的版本,因此我们必须使用回调函数来处理结果,这又会再次陷入回调函数的泥潭。不过幸好我们有Jscex:
var fromCallback = Jscex.Async.Binding.fromCallback, exec = require('child_process').exec, execAsync = fromCallback(exec, "_ignored_", "stdout", "stderr"); var buildOne = eval(Jscex.compile("async", function (module) { var fullName, version; if (!module) { fullName = "jscex"; version = Jscex.coreVersion; } else { fullName = "jscex-" + module; version = Jscex.modules[module].version; } var inputFile = fullName + ".js"; var outputFile = fullName + "-" + version + ".min.js"; var command = _.format( "java -jar {0} --js {1} --js_output_file {2} --compilation_level SIMPLE_OPTIMIZATIONS", gccPath, path.join(srcDir, inputFile), path.join(prodDir, outputFile)); utils.stdout("Generating {0}...", outputFile); var r = $await(execAsync(command)); if (r.stderr) { utils.stdout("failed.\n"); utils.stderr(r.stderr + "\n"); } else { utils.stdout("done.\n"); } }));
以上代码片段出自生产环境的Jscex发布脚本,可见有了Jscex之后,“阻塞”式地exec外部调用也完全不是问题。可以说,使用Jscex,可以基本解决Node.js代替编写Shell脚本的编程习惯或是风格问题,也欢迎大家多多尝试这种方法,并多多反馈,有问题及时发布在GitHub的Issues页面中。
至于Jscex的0.7.0版本,目前的计划是使用一个合适的JavaScript语法分析器来替换并统一UglifyJS和Narcissus的分析器。UglifyJS的分析器提供的信息太少,而Narcissus则实现地十分不靠谱,例如它连\r\n这种换行符都不支持,此外它还自做主张地将module作为关键字,这对来说Node.js是个很大的麻烦,因此其实目前Jscex的预编译器使用的是经过少许修改的Narcissus语法分析器。在0.7.0版本中,我希望能从语法分析器中得到更多信息,这样便可以引入Source Map,更进一步地支持调试。此外,在改写Jscex的过程中,详细的单元测试自然是必不可少的。
正像我一开始说的那样,Jscex的0.7.0版本将会在项目功能与质量,以及专业性上有重大突破。这么做也不枉已经有600人在GitHub上关注着Jscex:
在Node.js的模块列表上,流程控制分类有80余个项目。其中关注人数最多的是async(2444人),其次是Step(862人),第三便是Jscex。
先睹为快! 不小心从微薄就链过来了!呵呵