npm执行脚本

npm-script

大部分npm用户都知道可以在package.json文件中定义npm start或者npm test这样的脚本任务。其实npm的脚本功能远远不止于启动服务器或者执行测试。

这是一个典型的package.json文件。

// package.json
// 定义start和test脚本任务
{
  "name": "death-clock",
  "version": "1.0.0",
  "scripts": {
    "start": "node server.js",
    "test": "mocha --reporter spec test"
  },
  "devDependencies": {
    "mocha": "^1.17.1"
  }
}
// 这里为讲解需要,我在JSON文件内容中加了注解
// 事实上JSON文件中是不允许有注释的

start其实是默认脚本任务,内容也默认是node server.js,所以上述配置其实是冗余的。为了能用在test任务中调用mocha,还需要把它作为依赖加到devDependencies这一节下(当然,加到dependencies这一节下也可以,但因为在生产环境中不需要用到,所以放到devDependencies下更合适一些)。

mocha --reporter spec test这句命令之所以能运行,是因为npm会在node_modules/.bin目录下检索相应的脚本文件,而mocha包被安装后,一个名为mocha的脚本也会安装到这个目录下。

mocha项目的package.json配置中的这一段描述了被安装到bin目录下的脚本。[1]

// mocha package.json
{
  "name": "mocha",
  ...
  "bin": {
    "mocha": "./bin/mocha",
    "_mocha": "./bin/_mocha"
  },
  ...
}

可以看到,mocha包定义了两个脚本:mocha_mocha

很多npm包都定义了bin这一节的内容。这一节指定的脚本都可以像mocha一样被npm直接运行。执行ls node_modules/.bin命令就可以知道在当前项目下有哪些npm脚本。

# 我一个项目下可用的npm脚本
$ ls node_modules/.bin
_mocha   browserify  envify       jshint
jsx      lessc       lesswatcher  mocha
nodemon  uglifyjs    watchify

执行脚本任务

starttest这样的特殊脚本任务都可以直接执行。

# 执行"start"指定的脚本
$ npm start
$ npm run start

# 执行"test"指定的脚本
$ npm test
$ npm run test

所有其它的脚本任务都必须用npm run来执行。npm runnpm run-script的缩略。

{
  ...
  "scripts": {
    // watch-test starts a mocha watcher that listens for changes
    "watch-test": "mocha --watch --reporter spec test"
  },
}

上述代码指定的脚本任务可以通过npm run watch-test来执行,执行npm watch-test则会报错。

直接执行脚本文件

上述例子中执行的脚本任务都定义在package.json中,但这并不是必要条件。npm run可以执行任意node_modules/.bin路径下的脚本。也就是说,除了npm test,我还可以直接通过npm run mocha来执行mocha脚本。[2]

自动补全

我们很难记住各种模块提供的形形色色的脚本命令。如果有自动补全的话,输入命令就简单多了。事实上用npm执行脚本是可以做到自动补全的。npm提供了非常友好的命令补全功能。执行npm completion可以得到一个npm的命令自动补全脚本,只要应用这个脚本,就能很方便的自动补全npm的普通子命令或者npm run的脚本命令。太方便了!

我习惯于把各种自动补全脚本存成文件放到.bashrc文件可以调用的地方去。

# npm_completion.sh
. <(npm completion)

# 我一个项目的自动补全输出
$ npm run <tab>
nodemon           browserify          build
build-js          build-less          start
jshint            test                deploy
less              uglify-js           express
mocha             watch               watch-js
watch-less        watch-server

很帅吧!

组合脚本任务

上述的npm特性已经可以满足大部分场景了,不过有时候我们需要同时完成多项任务。npm也具备这样的能力。npm run其实最后会把脚本任务输出给sh执行,所以理论上我们可以像在命令行中一样组合各种脚本任务。

管道

假设我们要用browserify打包javascript文件,并且要用uglifyjs进行代码混淆。我只需要用管道(|)把browserify的输出转接给uglifyjs就可以了。非常简单。

// package.json
// browserify的reactify选项用于处理React语法
"scripts": {
  "build-js": "browserify -t reactify app/js/main.js | uglifyjs -mc > static/bundle.js"
},
// 添加必要的依赖项
"devDependencies": {
  "browserify": "^3.14.0",
  "reactify": "^0.5.1",
  "uglify-js": "^2.4.8"
}

串行

另一个场景是我们希望当且仅当上一个命令完成后,再执行下一个命令。可以通过串行符号(&&)来实现这个功能,当然,管道(|)也可以实现类似的效果。[3]

"scripts": {
  // 如果build-js,则继续执行build-less
  "build": "npm run build-js && npm run build-less",
  ...
  "build-js": "browserify -t reactify app/js/main.js | uglifyjs -mc > static/bundle.js",
  "build-less": "lessc app/less/main.less static/main.css"
}

这里,我通过build脚本任务来执行另外两个在package.json中定义的脚本任务。和执行脚本不同之处在于:必须使用npm run来执行其它脚本任务。

并行

有时候并行地执行多个命令的功能也是必要的。使用并行符号(&)可以把子命令作为后台任务并行执行。

"scripts": {
  // 并行地执行watch-js,watch-less和watch-server
  "watch": "npm run watch-js & npm run watch-less & npm run watch-server",
  "watch-js": "watchify app/js/main.js -t reactify -o static/bundle.js -dv",
  "watch-less": "nodemon --watch app/less/*.less --ext less --exec 'npm run build-less'",
  "watch-server": "nodemon --ignore app --ignore static server.js"
},
// 添加必要的依赖项
"devDependencies": {
  "watchify": "^0.6.2",
  "nodemon": "^1.0.15"
}

上述配置有几个挺有意思的点。首先watch脚本任务使用&来并行执行三个watch任务。如果按下Ctrl-C杀掉进程,所有watch任务都会停止,因为它们由同一个父进程执行。

watchifybrowserify命令的watch模式。watch-servernodemon的标准用法:每当有相应的文件变更时,重启服务器。

watch-less则是nodemon的一种不常见的用法。每当有less文件变更的时候,它都会执行npm run build-less命令把less文件编译成CSS文件。--ext less是必需的选项。--exec选项指定的是nodemon执行的外部命令。

复杂脚本

如果是更复杂的脚本任务,我通常会写成bash文件,而在package.json中指定为脚本任务。下面是一个脚本实例,这个脚本做的事情是把编译好的资源加到一个发布分支,并且把这个分之推送到Heroku上。

#!/bin/bash

set -o errexit # Exit on error

git stash save -u 'Before deploy' # Stash all changes, including untracked files, before deploy
git checkout deploy
git merge master --no-edit # Merge in the master branch without prompting
npm run build # Generate the bundled Javascript and CSS
if $(git commit -am Deploy); then # Commit the changes, if any
  echo 'Changes Committed'
fi
git push heroku deploy:master # Deploy to Heroku
git checkout master # Checkout master again
git stash pop # And restore the changes

加上package.json的配置后,就可以通过npm run deploy命令执行这个脚本了。

"scripts": {
  "deploy": "./bin/deploy.sh"
},

小结

对于Node而言,npm的存在意义并不止于包管理器。通过合适的配置,我们可以处理绝大部分的脚本需求。

另一个用npm执行脚本的原因是,只需要配置好starttest,我的项目就可以和HerokuTravisCI这样的SaaS服务提供商整合起来了。

文档信息

项目 内容
原文作者 Anders Janmyr
原文链接 http://anders.janmyr.com/2014/03/running-scripts-with-npm.html
本文链接 http://leungwensen.github.io/blog/2016/from-socialcalc-to-ethercalc.html

如果发现翻译问题,欢迎反馈:leungwensen@gmail.com


  1. 原文笔误:mocha写成了macha ↩︎

  2. 原文错误:npm test写成了mocha test ↩︎

  3. 原文代码注释错误:build-js写成了build-less ↩︎