回调蛋糕 —— cake下顺序执行命令

731 查看

cake是CoffeeScript自带的make工具,简单易用。不过有一个问题,因为Node.js默认是异步的,所以你很难保证执行顺序。

如何解决呢?其实用回调函数就可以。

例如,假定我们平时使用config.json(方便开发和测试),但是在发布NPM包的时候,希望发布config.json.example。这就要求我们在npm publish前后修改文件名——也就是说,要保证执行的顺序,不能出现文件名还没改好就发布的情况。如果用sh表达的话,就是:

mv config.json config.json.example
npm publish
mv config.json.example config.json

硬来的话,是这么写:

{spawn} = require 'child_process'

spawn 'mv', ['config.json', 'config.json.example']
spawn 'npm', ['publish']
spawn 'mv', ['config.json.example', 'congfig.json']

硬蛋糕可不好吃!这样是不行的,因为child_process.spawn是异步的。

所以,我们需要把上面的代码改成回调的形式,确保上一步退出时才调用下一步:

mv = spawn 'mv', ['config.json', 'config.json.example']
mv.on 'exit', ->
  npm_publish = spawn 'npm', ['publish']
  npm_publish.on 'exit', ->'
    spawn 'mv', ['config.json.example', 'congfig.json']

这样就可以了。可是每次这么写还蛮烦的。我们把它抽象成一个函数。

嗯,该怎么写呢?首先,我们确定这个函数的输入,函数应当接受一个命令,后面是一串回调函数。因此:

shell_commands = (args...) ->

args...是splats,用来表示参数数目不定。

然后我们需要做的就是执行这个命令,在命令执行完毕之后,运行那一串回调函数。

因此,我们首先得到需要执行的命令,这很简单,pop掉最后的一串回调函数,再交给spawn执行即可。

args.pop()
[command, args...] = args
spawn command, args

注意上面的args...同样是splats,这次用于模式匹配。

然后加上运行回调函数的代码,我们的函数基本就成形了:

shell_commands = (args...) ->
  callback = args.pop()
  [command, args..] = args
  shell_command = spawn command, args
  shell_command.on 'exit' ->
    callback()

有个问题,回调到最后,会是单纯的命令,没有回调函数了。因此我们加一个判断,如果args的最后一项不是函数的话,那就不pop了,直接执行命令就行了:

shell_commands = (args...) ->
  if typeof(args[a.length - 1]) is 'function'
    callback = args.pop()
  else
    callback = ->
  [command, args..] = args
  shell_command = spawn command, args
  shell_command.on 'exit' ->
    callback()

抽象出了这个函数以后,我们原先的代码就可以这么写了:

shell_commands 'mv', 'config.json', 'config.json.example', ->
  shell_commands 'npm', 'publish', ->
    shell_commands 'mv', 'config.json.example', 'config.json'

是不是清爽多了?

没有处理的问题是,万一在重命名的过程中出问题了,那么我们不应该发布NPM包。所以,我们加一下判断,如果进程的exit code不为0(这就意味着有错误),那么继续执行。

shell_commands = (args...) ->
  if typeof(args[a.length - 1]) is 'function'
    callback = args.pop()
  else
    callback = ->
  [command, args..] = args
  shell_command = spawn command, args
  shell_command.on 'exit', (code) ->
    if code isnt 0
      callback(code)
    else
      console.log("Exited with status: " + code)

相应地,调用的时候也需要传入code

shell_commands 'mv', 'config.json', 'config.json.example', (code) ->
  shell_commands 'npm', 'publish', (code) ->
    shell_commands 'mv', 'config.json.example', 'config.json'

通常大家喜欢verbose的输出,那么我们就在终端显示一下执行的命令:

console.log(command, " ", args.join(" "))

最终的CakeFile类似这样:

{spawn} = require 'child_process'

shell_commands = (args...) ->
  if typeof(args[a.length - 1]) is 'function'
    callback = args.pop()
  else
    callback = ->
  [command, args..] = args
  console.log(command, " ", args.join(" "))
  shell_command = spawn command, args
  shell_command.on 'exit', (code) ->
    if code isnt 0
      callback(code)
    else
      console.log("Exited with status: " + code)

task "publish", "publish NPM", ->
  shell_commands 'mv', 'config.json', 'config.json.example', (code) ->
    shell_commands 'npm', 'publish', (code) ->
      shell_commands 'mv', 'config.json.example', 'config.json'

task "test", "run unit tests", ->
  shell_commands './node_module/.bin/mocha'