使用Node.js编写Shell脚本,暨Jscex 0.6.5版本发布
发布于 12 年前 作者 jeffz 8747 次浏览 最后一次编辑是 8 年前

原文地址:使用Node.js编写Shell脚本,暨Jscex 0.6.5版本发布

<p>昨天不得不花时间做了点保护博客阅读体验的事情,但其实这篇才是我真正想写的。上个星期在香港出差,晚上的活动大都是喝酒,回到酒店便借着些许酒劲改进<a href=“http://jscex.info/zh-cn/”>Jscex</a>。如今虽然Jscex的开发工作并没有详细的时间计划,但我正在使用<a href=“https://github.com/JeffreyZhao/jscex/issues”>GitHub的Issues页面</a>记录需要制作的任务点,因此每天都是朝着目标逐步前进的。按照计划,<a href=“https://github.com/JeffreyZhao/jscex/issues?milestone=1&state=closed”>Jscex的0.6.5的主要目标</a>是对Jscex的模块机制进行改进,统一辅助方法,并使用Node.js重新编写发布脚本。这些工作的目的都是为接下来的<a href=“https://github.com/JeffreyZhao/jscex/issues?milestone=2&state=open”>0.7.0版本</a>作准备,它将会是Jscex在项目功能与质量,以及专业性上有重大突破的版本。</p>

<p>Jscex的模块化划分十分细致,目前正式对外发布的也已经有<a href=“https://github.com/JeffreyZhao/jscex/tree/master/bin/npm”>六个模块</a>(还有一个是命令)。模块化的目的是为了选择性的加载,例如在前端开发过程时需要加载<a href=“https://github.com/JeffreyZhao/jscex/tree/master/bin/dev”>所有的模块</a>,而真正部署时则<a href=“https://github.com/JeffreyZhao/jscex/tree/master/bin/dev”>无需最大的两个模块</a>:JIT编译器及解析器(前者依赖后者),这既照顾到了JavaScript编程体验,也考虑到前端部署时的尺寸。但是模块化便对模块管理提出了要求,这在Node.js平台下问题不大,因为NPM自带包管理功能,但在浏览器上便没有那么好的支持了。例如,模块依赖时还会涉及到版本,版本过高或者过低都是问题,如何在模块没有加载,或是加载了错误的版本时清晰地告诉用户,这便是0.6.5版本中想要解决的问题。此外,Jscex还直接支持不同的包加载环境(如AMD环境,<a href=“http://blog.zhaojie.me/2012/06/jscex-unit-tests-with-mocha-chai.html”>之前我也谈过这方面的单元测试</a>),这也是模块机制的重要责任。有了核心模块机制以后,各模块只需要注册自己的信息以及初始化方法,之前提过的错误提示,包加载环境支持等功能,便可以一并使用了。之后如果需要修复问题,或提供更多的功能,也只需要修改这个核心模块机制——简单地说,其实就是DRY原则。</p>

<p>更重要的一点是,这个各个模块在使用这个模块机制的时候,其实已经提供了自己的元信息,例如模块名称,当前版本,它所依赖的模块及其版本等等。换句话说,在发布这些脚本的时候,我们完全可以直接读取这些信息,而无需额外的配置文件。因此,我也使用<a href=“https://github.com/JeffreyZhao/jscex/tree/master/scripts”>Node.js替换了原来的Shell编写发布脚本</a>。使用Node.js来代替Shell脚本<a href=“https://github.com/JeffreyZhao/jscex/tree/master/scripts”>在某些情况下的确会更为方便</a>,对于一些常见逻辑表达(条件判断,循环等等)或是操作(文件处理,字符串分析等等)来讲,JavaScript语言的可读性无疑会比Shell高很多(因此也有很多人使用Ruby或Python来代替Shell脚本),对于熟悉JavaScript的同学来说则更不用说了。</p>

<p>大量使用Node.js来编写脚本并不如想象中的麻烦,它受JavaScript异步特性的影响并不大,这是因为Node.js标准库里包含了许多同步的方法,使用这些同步方法便可以完成绝大部分工作了,例如:</p>

<pre class=“code”><span style=“color: maroon”>“use strict”</span>;

<span style=“color: blue”>var </span>path = require(<span style=“color: maroon”>“path”</span>), fs = require(<span style=“color: maroon”>“fs”</span>), utils = require(<span style=“color: maroon”>"…/lib/utils"</span>), Jscex = utils.Jscex, _ = Jscex._;

<span style=“color: blue”>var </span>devDir = path.join(__dirname, <span style=“color: maroon”>"…/bin/dev"</span>); <span style=“color: blue”>var </span>srcDir = path.join(__dirname, <span style=“color: maroon”>"…/src"</span>);

<span style=“color: blue”>if </span>(path.existsSync(devDir)) { utils.rmdirSync(devDir); }

fs.mkdirSync(devDir);

<span style=“color: blue”>var </span>coreName = <span style=“color: maroon”>“jscex-” </span>+ Jscex.coreVersion + <span style=“color: maroon”>".js" </span>utils.copySync(path.join(srcDir, <span style=“color: maroon”>“jscex.js”</span>), path.join(devDir, coreName)); console.log(coreName + <span style=“color: maroon”>" generated."</span>);

<span style=“color: blue”>var </span>moduleList = [<span style=“color: maroon”>“parser”</span>, <span style=“color: maroon”>“jit”</span>, <span style=“color: maroon”>“builderbase”</span>, <span style=“color: maroon”>“async”</span>, <span style=“color: maroon”>“async-powerpack”</span>];

_.each(moduleList, <span style=“color: blue”>function </span>(i, module) { <span style=“color: blue”>var </span>fullName = <span style=“color: maroon”>“jscex-” </span>+ module; <span style=“color: blue”>var </span>version = Jscex.modules[module].version; <span style=“color: blue”>var </span>outputName = fullName + <span style=“color: maroon”>"-" </span>+ version + <span style=“color: maroon”>".js"</span>; utils.copySync(path.join(srcDir, fullName + <span style=“color: maroon”>".js"</span>), path.join(devDir, outputName)); console.log(outputName + <span style=“color: maroon”>" generated."</span>); });</pre>

<p>以上便是<a href=“https://github.com/JeffreyZhao/jscex/blob/master/scripts/build-dev.js”>发布开发版Jscex的脚本</a>,可见其中每一步IO操作,例如判断目录是否存在,删除/创建目录以及复制文件,都使用了同步的版本。不过,Node.js并不能够完全代替Shell脚本,因为Shell脚本有其重要的武器:管道。试想,你如何使用JavaScript来实现下面这行Shell脚本所做的事情?</p>

<pre class=“code”>cat /data/logs/run.log | grep ‘node’ | grep -v ‘innode’ | awk {‘print $2’} | xargs sudo kill -9</pre>

<p>因此,有时候我们不可避免要在Node.js中调用Shell脚本。同理,我们总会有需要调用外部程序的时候,例如,在发布生产环境的Jscex时,我需要使用<a href=“https://developers.google.com/closure/compiler/”>Google Closure Compiler</a>来压缩脚本体积,这就需要使用<a href=“http://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback”>exec</a>来执行外部命令。可惜的是,exec方法没有同步的版本,因此我们必须使用回调函数来处理结果,这又会再次陷入回调函数的泥潭。不过幸好我们有Jscex:</p>

<pre class=“code”><span style=“color: blue”>var </span>fromCallback = Jscex.Async.Binding.fromCallback, exec = require(<span style=“color: maroon”>‘child_process’</span>).exec, execAsync = fromCallback(exec, <span style=“color: maroon”>“ignored”</span>, <span style=“color: maroon”>“stdout”</span>, <span style=“color: maroon”>“stderr”</span>);

<span style=“color: blue”>var </span>buildOne = eval(Jscex.compile(<span style=“color: maroon”>“async”</span>, <span style=“color: blue”>function </span>(module) { <span style=“color: blue”>var </span>fullName, version; <span style=“color: blue”>if </span>(!module) { fullName = <span style=“color: maroon”>“jscex”</span>; version = Jscex.coreVersion; } <span style=“color: blue”>else </span>{ fullName = <span style=“color: maroon”>“jscex-” </span>+ module; version = Jscex.modules[module].version; }

<span style="color: blue">var </span>inputFile = fullName + <span style="color: maroon">&quot;.js&quot;</span>;
<span style="color: blue">var </span>outputFile = fullName + <span style="color: maroon">&quot;-&quot; </span>+ version + <span style="color: maroon">&quot;.min.js&quot;</span>;

<span style="color: blue">var </span>command = _.format(
    <span style="color: maroon">&quot;java -jar {0} --js {1} --js_output_file {2} --compilation_level SIMPLE_OPTIMIZATIONS&quot;</span>,
    gccPath,
    path.join(srcDir, inputFile),
    path.join(prodDir, outputFile));

utils.stdout(<span style="color: maroon">&quot;Generating {0}...&quot;</span>, outputFile);

<span style="color: blue">var </span>r = <span style="color: red">$await(execAsync(command))</span>;
<span style="color: blue">if </span>(r.stderr) {
    utils.stdout(<span style="color: maroon">&quot;failed.\n&quot;</span>);
    utils.stderr(r.stderr + <span style="color: maroon">&quot;\n&quot;</span>);
} <span style="color: blue">else </span>{
    utils.stdout(<span style="color: maroon">&quot;done.\n&quot;</span>);
}

}));</pre>

<p>以上代码片段出自<a href=“https://github.com/JeffreyZhao/jscex/blob/master/scripts/build-prod.js”>生产环境的Jscex发布脚本</a>,可见有了Jscex之后,“阻塞”式地exec外部调用也完全不是问题。可以说,使用Jscex,可以基本解决Node.js代替编写Shell脚本的问题,也欢迎大家多多尝试这种方法,并多多反馈,有问题及时发布在GitHub的Issues页面中。</p>

<p>至于Jscex的0.7.0版本,目前的计划是使用一个合适的JavaScript语法分析器来替换并统一UglifyJS和Narcissus的分析器。UglifyJS的分析器提供的信息太少,而Narcissus则实现地十分不靠谱,例如它连\r\n这种换行符都不支持,此外它还自做主张地将module作为关键字,这对来说Node.js是个很大的麻烦,因此其实目前Jscex的预编译器使用的是经过少许修改的Narcissus语法分析器。在0.7.0版本中,我希望能从语法分析器中得到更多信息,这样便可以引入Source Map,更进一步地支持调试。此外,在改写Jscex的过程中,详细的单元测试自然是必不可少的。</p>

<p>正像我一开始说的那样,Jscex的0.7.0版本将会在项目功能与质量,以及专业性上有重大突破。这么做也不枉已经有600人在GitHub上关注着Jscex:</p>

<p><img src=“http://img.zhaojie.me/blog/jscex-github-watchers-20120619.png” /></p>

<p>在Node.js的模块列表上,<a href=“https://github.com/joyent/node/wiki/Modules#wiki-async-flow”>流程控制分类</a>有80余个项目。其中关注人数最多的是<a href=“https://github.com/caolan/async”>async</a>(2444人),其次是<a href=“https://github.com/creationix/step”>Step</a>(862人),第三便是<a href=“https://github.com/JeffreyZhao/jscex”>Jscex</a>。</p>

7 回复

昨晚看到老赵哥发微博了~不过还没细看!

老赵,最近相当勤奋,给力

顶一下 node shell script ,哈哈哈

顶一下老赵!

回到顶部