回撥蛋糕 —— cake下順序執行命令

江小湖Laker發表於2014-02-26

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'

相關文章