现如今无论是 node 还是前端开发都离不开 npm,而脚本功能是 npm 最强大、最常用的功能之一。
关于 npm 脚本(npm scripts)的原理及用途你真的了解吗?
本文就带大家一起深入浅出 npm scripts
。
1. 什么是 npm scripts?
1.1 如何定义 npm scripts
npm 允许在package.json
文件里面,使用scripts
字段定义脚本命令。
{
"scripts": {
"build": "vue-cli-service build", // vue 项目编译命令(前端领域)
"start": "node server.js" // nodejs 服务启动命令 (后端领域)
}
}
上面代码是package.json
文件的一个片段。其中,scripts
字段的值为一个对象。其对象的每一个属性,对应了一段脚本。
1.2 如何使用 npm scripts
在命令行中执行npm run ${script name}
命令,就可以执行对应的脚本命令。例如:
$ npm run build
# 等同于执行
$ vue-cli-service build
$ npm run start
# 等同于执行
$ node server.js
1.3 如何查看 npm scripts
在命令行中,直接使用不带任何参数的npm run
命令,可查看当前项目下的所有 npm 脚本命令。例如:
$ npm run
# 上述命令的返回的结果如下:
Scripts available in test-project@1.0.0 via `npm run-script`:
dev
vue-cli-service serve
lint
eslint --ext .js,.vue src
build
vue-cli-service build
test:unit
jest --clearCache && vue-cli-service test:unit
test:ci
npm run lint && npm run test:unit
1.4 npm scripts 有哪些特点
- 方便指令的集中管理:项目下的脚本均放置在
scripts
对象下进行集中管理。 - 方便指令的集成:通过
&& & ||
等shell
相关的指令可轻松完成指令的集成。例如:npm run lint && npm run test:unit
,可按顺序执行npm run lint
及npm run test:unit
指令(前一个指令执行成功后才会执行后续指令) - 提供了丰富的辅助功能:可以使用 npm 提供的很多辅助功能,例如脚本的
hooks、环境变量
等功能。
2 npm scripts 的底层原理
2.1 npm scripts执行
npm 脚本的原理非常简单。每执行一次npm run
,就会自动新建一个 Shell,并在这个 Shell 里面执行指定的脚本命令。因此,只要是 Shell(通常是 Bash)可以运行的命令,就可以写在 npm 脚本里面。例如:
{
"scripts": {
"linux:ls": "ls -al", // linux ls命令
"script:js": "filePath/jsfile.js" // nodejs 脚本文件(filePath表示文件路径,也可以直接通过node xxx.js执行)
"script:shell": "filePath/shellfile.sh" // bash shell脚本文件(filePath表示文件路径)
}
}
- 直接执行linux命令
$ npm run linux:ls
# 上述命令返回结果如下:
> test-project@1.0.0 linux:ls
> ls -al
total 248
drwxr-xr-x 27 username staff 864 1 10 19:28 .
drwxr-xr-x 10 username staff 320 1 10 15:45 ..
-rw-r--r-- 1 username staff 3101 1 10 15:54 .cz-config.js
-rw-r--r-- 1 username staff 575 1 10 15:54 .editorconfig
-rw-r--r-- 1 username staff 43 1 7 10:39 .eslintignore
- 执行 js 脚本
#!/usr/bin/env node
// jsfile.js文件
const process = require('process') // 引用nodejs内置的process模块
console.log(process.env.PATH) // 获取环境变量PATH值
上述脚本中:
#!/usr/bin/env node
表示采用 node 作为当前脚本的解释执行器。其中#!
称为shebang
,该行的其余部分是解释器的路径,用于指定解释执行器在操作系统中的位置。#!
开头的声明必须放置到脚本的第一行
且与文件后缀无关,因此有了该声明后,js脚本不一定非要以.js作为后缀,可以为.sh或.py等任意后缀(不推荐这样操作)。- 当执行
env node
时,它其实会去env | grep PATH
的路径里去依次查找名为 node 的可执行文件。 - 可以通过
which node
命令来找到你本地的node安装路径,并更改/usr/bin/env node
为实际值,例如我本地的node路径#!/Users/username/.nvm/versions/node/v16.13.1/bin/node
$ npm run script:js
# 上述命令返回结果如下:
> test-project@1.0.0 script:js
> ./jsfile.js
/Users/username/Documents/workspace/project/iotgz-integration-api/node_modules/.bin:/Users/username/Documents/workspace/project/node_modules/.bin:/Users/username/Documents/workspace/node_modules/.bin:/Users/username/Documents/node_modules/.bin:/Users/username/node_modules/.bin:/Users/node_modules/.bin:/node_modules/.bin
- 执行 shell 脚本
#!/usr/bin/env bash
echo $PATH;
其中,#!/usr/bin/env bash
表示采用 bash 作为当前脚本的解释执行器,也可以采用 zsh
等其它执行器。
$ npm run script:shell
> test-project@1.0.0 script:shell
> ./shellfile.sh
/Users/username/Documents/workspace/project/iotgz-integration-api/node_modules/.bin:/Users/username/Documents/workspace/project/node_modules/.bin:/Users/username/Documents/workspace/node_modules/.bin:/Users/username/Documents/node_modules/.bin:/Users/username/node_modules/.bin:/Users/node_modules/.bin:/node_modules/.bin
可以看出,上述两个脚本文件都是输出了环境变量 PATH
的值,由于都是通过npm scripts方式执行的,因此输出的值的内容是一致的。
2.2 环境变量注入
- 环境变量注入:
npm run
新建的这个 Shell,会递归地将当前目录下、上层目录下、上上层目录下的node_modules/.bin
路径加入到PATH
变量,直到到达根目录/
为止(这样做的目的是为了尽可能的找到目标可执行文件)。执行npm run xxx后,你的 PATH 环境变量可能为:/Users/username/test-project/node_modules/.bin:/Users/username/node_modules/.bin:/Users/node_modules/.bin:/node_modules/.bin
- 环境变量恢复:执行结束后,
PATH
变量将恢复为原样。这是因为该环境变量类型为shell环境变量
,非用户环境变量
或系统环境变量
,因此在脚本执行完毕后,将被清除。
这意味着,当前目录或上层目录的node_modules/.bin
子目录里面的所有脚本,都可以直接用脚本名调用,而不必加上路径。比如,当前项目的依赖里面有 Mocha,只要直接写mocha test
就可以了。
"test": "mocha test"
而不用写成下面这样。
"test": "./node_modules/.bin/mocha test"
2.3 .bin目录可执行文件
node_modules/.bin
中存放着可执行脚本,由于 npm 脚本的唯一要求就是可以在 Shell 执行,因此它不一定是 Node 脚本,任何可执行文件都可以写在里面。注意:.bin目录下的脚本实际上为系统软连接
例如:
$ ls -al
total 0
drwxr-xr-x 19 username staff 608 1 10 17:06 .
drwxr-xr-x 2058 username staff 65856 1 10 17:06 ..
lrwxr-xr-x 1 username staff 46 1 10 17:06 commitizen -> ../_commitizen@4.2.4@commitizen/bin/commitizen
lrwxr-xr-x 1 username staff 49 1 10 17:06 commitlint -> ../_@commitlint_cli@15.0.0@@commitlint/cli/cli.js
2.4 脚本退出
npm 脚本的退出码,也遵守 Shell 脚本规则。如果退出码不是0
,npm 就认为这个脚本执行失败。
3 通配符
由于 npm 脚本就是 Shell 脚本,因为可以使用 Shell 通配符。需要注意的是,通配符和JS正则表达式不是同一个东西,他们的区别可以查看:通配符和JS中的正则区别。
"lint": "jshint *.js"
"lint": "jshint **/*.js"
上面代码中,*
表示任意文件名,**
表示任意一层子目录。
如果要将通配符传入原始命令,防止被 Shell 转义,要将星号转义。
"test": "tap test/*.js"
4 传参
向 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"
5 默认值
一般来说,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
文件。
6 钩子
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
等等。所以,可以利用这个变量,在同一个JS脚本文件里面,为不同的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
命令之前运行。
7 简写形式
四个常用的 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
8 环境变量
npm 脚本有一个非常强大的功能,就是可以使用 npm 注入到shell的环境变量。
- 首先,通过
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
取到这两个值。(如果是JS脚本,则可以直接通过require('package.json')获取,因为nodejs模块系统内置支持.js/.json/.node等后缀文件)
- 其次,npm 脚本还可以通过
npm_config_
前缀,拿到 npm 的配置变量,即npm config get xxx
命令返回的值。比如,当前模块的发行标签,可以通过npm_config_tag
取到。
{
"name": "test-project",
"version": "1.0.0",
"scripts": {
"view": "env | grep -E ^npm_config"
}
}
运行 npm run view后得到的结果如下:
$ npm run view
> test-project@1.0.0 view
> env | grep -e ^npm_config
npm_config_metrics_registry=https://registry.npmjs.org/
npm_config_global_prefix=/Users/username/.nvm/versions/node/v16.13.1
npm_config_noproxy=
npm_config_local_prefix=/Users/username/Documents/workspace/project/iotgz-integration-api
npm_config_globalconfig=/Users/username/.nvm/versions/node/v16.13.1/etc/npmrc
npm_config_userconfig=/Users/username/.npmrc
npm_config_init_module=/Users/username/.npm-init.js
npm_config_node_gyp=/Users/username/.nvm/versions/node/v16.13.1/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
npm_config_cache=/Users/username/.npm
npm_config_user_agent=npm/8.3.0 node/v16.13.1 darwin arm64 workspaces/false
npm_config_prefix=/Users/username/.nvm/versions/node/v16.13.1
- 需要注意的是,
package.json
里面的config
对象,可以被npm config set
命令覆盖的。
{
"name" : "test-project",
"version" : "1.0.0",
"config" : { "port" : "8080" },
"scripts" : { "start" : "node server.js" }
}
上面代码中,npm_package_config_port
环境变量返回的值是8080
。这个值可以用下面的方法覆盖:
$ npm config set foo:port 80
9 常用脚本示例
// 删除目录
"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",