Node 开发离不开 npm,而脚本功能是 npm 最强大、最常用的功能之一。
本文介绍如何使用 npm 脚本(npm scripts)。
一、什么是 npm 脚本?
npm 允许在package.json
文件里面,使用scripts
字段定义脚本命令。
{ // ... "scripts": { "build": "node build.js" } }
上面代码是package.json
文件的一个片段,里面的scripts
字段是一个对象。它的每一个属性,对应一段脚本。比如,build
命令对应的脚本是node build.js
。
命令行下使用npm run
命令,就可以执行这段脚本。
$ npm run build # 等同于执行 $ node build.js
这些定义在package.json
里面的脚本,就称为 npm 脚本。它的优点很多。
- 项目的相关脚本,可以集中在一个地方。
- 不同项目的脚本命令,只要功能相同,就可以有同样的对外接口。用户不需要知道怎么测试你的项目,只要运行
npm run test
即可。- 可以利用 npm 提供的很多辅助功能。
查看当前项目的所有 npm 脚本命令,可以使用不带任何参数的npm run
命令。
$ npm run
二、原理
npm 脚本的原理非常简单。每当执行npm run
,就会自动新建一个 Shell,在这个 Shell 里面执行指定的脚本命令。因此,只要是 Shell(一般是 Bash)可以运行的命令,就可以写在 npm 脚本里面。
比较特别的是,npm run
新建的这个 Shell,会将当前目录的node_modules/.bin
子目录加入PATH
变量,执行结束后,再将PATH
变量恢复原样。
这意味着,当前目录的node_modules/.bin
子目录里面的所有脚本,都可以直接用脚本名调用,而不必加上路径。比如,当前项目的依赖里面有 Mocha,只要直接写mocha test
就可以了。
"test": "mocha test"
而不用写成下面这样。
"test": "./node_modules/.bin/mocha test"
由于 npm 脚本的唯一要求就是可以在 Shell 执行,因此它不一定是 Node 脚本,任何可执行文件都可以写在里面。
npm 脚本的退出码,也遵守 Shell 脚本规则。如果退出码不是0
,npm 就认为这个脚本执行失败。
三、通配符
由于 npm 脚本就是 Shell 脚本,因为可以使用 Shell 通配符。
"lint": "jshint *.js" "lint": "jshint **/*.js"
上面代码中,*
表示任意文件名,**
表示任意一层子目录。
如果要将通配符传入原始命令,防止被 Shell 转义,要将星号转义。
"test": "tap test/\*.js"
四、传参
向 npm 脚本传入参数,要使用--
标明。
"lint": "jshint **.js"
向上面的npm run lint
命令传入参数,必须写成下面这样。
$ npm run lint -- --reporter checkstyle > checkstyle.xml
也可以在package.json
里面再封装一个命令。
"lint": "jshint **.js", "lint:checkstyle": "npm run lint -- --reporter checkstyle > checkstyle.xml"
五、执行顺序
如果 npm 脚本里面需要执行多个任务,那么需要明确它们的执行顺序。
如果是并行执行(即同时的平行执行),可以使用&
符号。
$ npm run script1.js & npm run script2.js
如果是继发执行(即只有前一个任务成功,才执行下一个任务),可以使用&&
符号。
$ npm run script1.js && npm run script2.js
这两个符号是 Bash 的功能。此外,还可以使用 node 的任务管理模块:script-runner、npm-run-all、redrun。
六、默认值
一般来说,npm 脚本由用户提供。但是,npm 对两个脚本提供了默认值。也就是说,这两个脚本不用定义,就可以直接使用。
"start": "node server.js", "install": "node-gyp rebuild"
上面代码中,npm run start
的默认值是node server.js
,前提是项目根目录下有server.js
这个脚本;npm run install
的默认值是node-gyp rebuild
,前提是项目根目录下有binding.gyp
文件。
七、钩子
npm 脚本有pre
和post
两个钩子。举例来说,build
脚本命令的钩子就是prebuild
和postbuild
。
"prebuild": "echo I run before the build script", "build": "cross-env NODE_ENV=production webpack", "postbuild": "echo I run after the build script"
用户执行npm run build
的时候,会自动按照下面的顺序执行。
npm run prebuild && npm run build && npm run postbuild
因此,可以在这两个钩子里面,完成一些准备工作和清理工作。下面是一个例子。
"clean": "rimraf ./dist && mkdir dist", "prebuild": "npm run clean", "build": "cross-env NODE_ENV=production webpack"
npm 默认提供下面这些钩子。
- prepublish,postpublish
- preinstall,postinstall
- preuninstall,postuninstall
- preversion,postversion
- pretest,posttest
- prestop,poststop
- prestart,poststart
- prerestart,postrestart
自定义的脚本命令也可以加上pre
和post
钩子。比如,myscript
这个脚本命令,也有premyscript
和postmyscript
钩子。不过,双重的pre
和post
无效,比如prepretest
和postposttest
是无效的。
npm 提供一个npm_lifecycle_event
变量,返回当前正在运行的脚本名称,比如pretest
、test
、posttest
等等。所以,可以利用这个变量,在同一个脚本文件里面,为不同的npm scripts
命令编写代码。请看下面的例子。
const TARGET = process.env.npm_lifecycle_event; if (TARGET === 'test') { console.log(`Running the test task!`); } if (TARGET === 'pretest') { console.log(`Running the pretest task!`); } if (TARGET === 'posttest') { console.log(`Running the posttest task!`); }
注意,prepublish
这个钩子不仅会在npm publish
命令之前运行,还会在npm install
(不带任何参数)命令之前运行。这种行为很容易让用户感到困惑,所以 npm 4 引入了一个新的钩子prepare
,行为等同于prepublish
,而从 npm 5 开始,prepublish
将只在npm publish
命令之前运行。
八、简写形式
四个常用的 npm 脚本有简写形式。
npm start
是npm run start
npm stop
是npm run stop
的简写npm test
是npm run test
的简写npm restart
是npm run stop && npm run restart && npm run start
的简写
npm start
、npm stop
和npm restart
都比较好理解,而npm restart
是一个复合命令,实际上会执行三个脚本命令:stop
、restart
、start
。具体的执行顺序如下。
- prerestart
- prestop
- stop
- poststop
- restart
- prestart
- start
- poststart
- postrestart
九、变量
npm 脚本有一个非常强大的功能,就是可以使用 npm 的内部变量。
首先,通过npm_package_
前缀,npm 脚本可以拿到package.json
里面的字段。比如,下面是一个package.json
。
{ "name": "foo", "version": "1.2.5", "scripts": { "view": "node view.js" } }
那么,变量npm_package_name
返回foo
,变量npm_package_version
返回1.2.5
。
// view.js console.log(process.env.npm_package_name); // foo console.log(process.env.npm_package_version); // 1.2.5
上面代码中,我们通过环境变量process.env
对象,拿到package.json
的字段值。如果是 Bash 脚本,可以用$npm_package_name
和$npm_package_version
取到这两个值。
npm_package_
前缀也支持嵌套的package.json
字段。
"repository": { "type": "git", "url": "xxx" }, scripts: { "view": "echo $npm_package_repository_type" }
上面代码中,repository
字段的type
属性,可以通过npm_package_repository_type
取到。
下面是另外一个例子。
"scripts": { "install": "foo.js" }
上面代码中,npm_package_scripts_install
变量的值等于foo.js
。
然后,npm 脚本还可以通过npm_config_
前缀,拿到 npm 的配置变量,即npm config get xxx
命令返回的值。比如,当前模块的发行标签,可以通过npm_config_tag
取到。
"view": "echo $npm_config_tag",
注意,package.json
里面的config
对象,可以被环境变量覆盖。
{ "name" : "foo", "config" : { "port" : "8080" }, "scripts" : { "start" : "node server.js" } }
上面代码中,npm_package_config_port
变量返回的是8080
。这个值可以用下面的方法覆盖。
$ npm config set foo:port 80
最后,env
命令可以列出所有环境变量。
"env": "env"
十、常用脚本示例
// 删除目录 "clean": "rimraf dist/*", // 本地搭建一个 HTTP 服务 "serve": "http-server -p 9090 dist/", // 打开浏览器 "open:dev": "opener http://localhost:9090", // 实时刷新 "livereload": "live-reload --port 9091 dist/", // 构建 HTML 文件 "build:html": "jade index.jade > dist/index.html", // 只要 CSS 文件有变动,就重新执行构建 "watch:css": "watch 'npm run build:css' assets/styles/", // 只要 HTML 文件有变动,就重新执行构建 "watch:html": "watch 'npm run build:html' assets/html", // 部署到 Amazon S3 "deploy:prod": "s3-cli sync ./dist/ s3://example-com/prod-site/", // 构建 favicon "build:favicon": "node scripts/favicon.js",
十一、参考链接
- How to Use npm as a Build Tool, by Keith Cirkel
- Awesome npm scripts, by Ryan Zimmerman
(完)
Abner 说:
及时雨,正学习用npm script替代grunt和gulp~
2016年10月11日 19:11 | # | 引用
eeve 说:
总结得真好,一直在用但是没深入了解这一块的东西,感谢分享!
2016年10月11日 20:18 | # | 引用
羽风 说:
Facebook 发布了新的 Node 模块管理器 Yarn
2016年10月12日 09:49 | # | 引用
吃翔宝宝 说:
老师昨天更新的~
2016年10月12日 13:56 | # | 引用
tripleW 说:
非常感谢!
2016年10月12日 14:41 | # | 引用
疯癫de小牛仔 说:
阮老师,你有一点没说关于“node_modules/.bin子目录加入PATH”中,其实在node_modules/.bin/里面的脚本,其实都做了软连接到对应的module里。比如
# ls -al
lrwxr-xr-x 1 xxxx staff 19 Jun 3 17:02 babel -> ../babel-cli/bin/babel.js
这样执行时才会执行对应模块里的脚本。
2016年10月12日 17:11 | # | 引用
颜海镜 说:
学习了,感谢,这个东西真是实用
2016年10月12日 19:15 | # | 引用
jon 说:
变量那一节,我在git bash下测试,都没报错也没有返回结果,环境win 7
2016年10月13日 13:49 | # | 引用
Lei 说:
大赞,学到了好几个有用的知识点。如果有打赏按钮就好了。
2016年10月14日 04:42 | # | 引用
Watonyweng 说:
阮老师,第八章简写形式的中npm脚本的第四项retart书写有误吧?
2016年10月17日 10:42 | # | 引用
晚晴幽草轩 说:
巨细而深,十分感谢。
2016年10月21日 15:36 | # | 引用
Eyas 说:
有个问题请教一下,在windows系统使用变量是 %npm_package_name% ,在类unix系统使用变量是 $npm_package_name,有没有办法在一条script命令中用到变量时能兼容 windows 和类 unix 呢
2016年10月28日 10:12 | # | 引用
寻找安多芬 说:
深受启发!
2016年11月 9日 16:24 | # | 引用
小李子 说:
ES6的模塊引入語法:import
這種同步的方式,以後在瀏覽器中如何實現?
難道讓瀏覽器卡住嗎?
2016年11月23日 14:53 | # | 引用
涛锅 说:
这里你错字啦,不是因为,应该是因此。
2016年12月 1日 11:26 | # | 引用
潜水员 说:
这篇文章不就是直接抄链接吗?某宝的技术书和所谓布道者出了不少,水分太多,搞了一堆山寨框架和产品,除了你们自己不知道还有谁在用。
2017年3月16日 22:24 | # | 引用
喷子的爸爸 说:
抄谁的连接了?没事不要乱喷
2017年9月18日 14:40 | # | 引用
高杨 说:
我想在package.json一条属性
{
"sha": "xxxxxxxxx"
}
如何操作,我当前的做法是读取package.json文件,然后修改,再保存,
阮大神,有没有其他正规一点的方式,
或者package.json如何自动添加sha信息
2017年11月 1日 09:57 | # | 引用
周振 说:
一切情况都可以省略 run 而不是上述的几种类似 npm run start
2017年12月 6日 23:30 | # | 引用
jason 说:
并行脚本应该是用 "|" 符号连接
2018年10月10日 15:34 | # | 引用
qgp 说:
回复: 并行脚本应该是用 "|" 符号连接
在window10中测试, 的确是 :
"|" 并行
"&" 继发执行
2019年4月23日 17:00 | # | 引用
Thinking80s 说:
很有帮助,正好了解npm实现。
2019年7月31日 10:16 | # | 引用
Ruby 说:
终于了解了为什么scripts里面执行命令可以不带路径:临时将/node_modules/.bin 下的命令加入了环境变量;
另外还了解了钩子,package.json变量,执行顺序。
2019年8月 2日 11:27 | # | 引用
HEro 说:
读阮老师的文章总是那么打收益
2019年12月18日 14:40 | # | 引用
Lidc 说:
你好,scripts有时会包含||连接符,请问作用是什么?
2020年2月 3日 17:52 | # | 引用
arkin 说:
通用的跨平台脚本怎么写?
如:发布脚本.
windows不支持bash.
2020年6月16日 11:44 | # | 引用
elon 说:
也许cross-env 和 run-script-os能帮到你
2020年7月13日 18:07 | # | 引用
lois 说:
写的通俗易懂,受益匪浅
2020年10月25日 13:10 | # | 引用
damon 说:
四、传参
好像不用 -- 说明,直接追加就可以
2021年8月 6日 10:56 | # | 引用
Roger 说:
1、"view": "echo $npm_package_name" 执行npm run view,输出的是“$npm_package_name”,并未将npm_package_name识别为变量
2、在package.json中配置config:{"param":123},在执行npm run get param,输出undifined,执行npm config list,未看到param变量,config配置没有生效
2021年8月30日 20:31 | # | 引用
Roger 说:
回复上条:
1、经测试,在linux环境bash下可以识别npm_package_name为变量,在window控制台中不行
2、npm config相关指令与package.json中的config无关,config相关指令操作的是npm_config_xxx,而后者是npm_package_config_xxx,两者的关系是如果npm config set 包名:test 123,npm_config_test会变成123,npm_package_config_test也会同步被修改为123(无论之前是否存在)
2021年8月31日 15:32 | # | 引用
Jake Wu 说:
“
注意,prepublish这个钩子不仅会在npm publish命令之前运行,还会在npm install(不带任何参数)命令之前运行。这种行为很容易让用户感到困惑,所以 npm 4 引入了一个新的钩子prepare,行为等同于prepublish,而从 npm 5 开始,prepublish将只在npm publish命令之前运行。
”
经实测,npm版本为6.12.0,prepare钩子是在postinstall后面执行的,而不是install之前执行的
2021年11月 2日 16:53 | # | 引用
Xianren 说:
深深受益,十分感谢
2021年12月22日 15:40 | # | 引用
努力中的小k 说:
计算机也太难学了,要学这么久,我快要熬死了
2022年5月 4日 02:42 | # | 引用
joy 说:
npm 脚本有一个非常强大的功能,就是可以使用 npm 的内部变量。
这个是怎么获取到的
2022年5月 7日 11:30 | # | 引用
aehyok 说:
不错,那么多年前的文章仍热都不过时
2022年5月10日 14:28 | # | 引用
samwangdd 说:
并不是,自定义的 npm script 都需要 run
2023年1月 7日 11:54 | # | 引用
白荣誉 说:
一峰老师的技术博客真的是清晰易懂
2024年12月 2日 21:50 | # | 引用