本文总结了npm script的原理和基础用法,包括执行多个命令、生命周期钩子、npm变量等等......
[What] 什么是npm script
在package.json
里面定义的scripts字段就是npm script
json
{
// ...
"scripts": {
"build": "node build.js"
}
}
scripts字段是一个对象,它的每一个属性,对应一段脚本
原理
当我们在项目里面运行npm run xxx
的时候,实际步骤如下
-
从
package.json
里面读取scripts对象 -
以传给npm run的第一个参数作为键,在scripts对象里面获取对应的值作为接下来要执行的命令,如果没找到就直接报错
-
新建一个shell,将当前目录的
node_modules/.bin
子目录加入PATH变量(这就意味着,当前目录的node_modules/.bin
子目录里面的所有脚本,都可以直接用脚本名调用,而不需要加上路径) -
在这个shell中执行上述命令
npm script的退出码也遵守shell脚本的规则,如果退出码不是0,则视为脚本执行失败
[Why] 为什么需要npm script
使用npm script主要有以下几个好处:
-
编写单一职责的命令,提高代码复用性
-
不同项目的脚本,只要功能相同,就可以有相同的对外接口,提升可读性、降低项目的上手门槛
-
通过连接多个命令,可以打造自动化的工作流
-
减少重复地手动执行命令的频率
-
避免人为因素(例如:单词拼写错误/参数输入错误/遗漏某个命令等)导致命令执行失败,提高命令执行的成功率
-
[How] 如何使用npm script
执行多个命令
在当前目录下执行多个不同命令
- 串行执行
使用&&
符号连接多个命令,当前序命令失败时,后续命令都不会执行
json
"script": {
"lint:js": "eslint *.js",
"lint:css": "eslint *.less",
"lint:all": "npm run lint:js && npm run lint:css"
}
- 并行执行
使用单个&
符号来连接多个命令
json
"script": {
"lint:js": "eslint *.js",
"lint:css": "eslint *.less",
"lint:all": "npm run lint:js & npm run lint:css"
}
在不同目录下执行多个相同命令
对于monorepo项目,一个项目的目录下可能会有多个workspace(工作区),可以在项目根目录的package.json
里面进行如下配置
json
"workspaces": [
"packages/a",
"packages/b",
"packages/c"
]
运行npm script时可以通过-w
来指定工作区,在(单个、多个或者所有 )工作区的上下文中串行运行指定的命令
几个简单的例子
- 在
packages/a
目录下执行npm build
- 在
packages/a
和packages/b
目录下执行npm build
- 在所有workspaces下执行
npm build
bash
# 在packages/a目录下执行npm build,以下2条命令完全等价
npm run build -w=packages/a
cd packages/a && npm run build
# 在packages/a和packages/b目录下执行npm build
npm run build -w=packages/a -w=packages/b
# 在所有workspace下执行npm build
npm run build -ws
传递参数
向npm脚本传递参数需要用 --
标明
json
"script": {
"lint:js": "eslint *.js",
"lint:js:fix": "npm run lint:js -- --fix",
}
当执行npm run lint:js:fix
的时候,实际运行的命令是eslint *.js --fix
添加注释
可以直接在命令前面以#
开头加上注释
json
"script": {
"test": "# 这是一条注释 \n exit1 "
}
调整日志输出
执行npm script的过程中会产生一些日志输出,我们可以通过--loglevel
来设置日志输出级别
从简洁到详细的排序如下:默认的输出级别为notice
输出级别 | 完整命令 | 简写 | 备注 |
---|---|---|---|
silent | --loglevel silent | -s 或 --slient | "没有消息就是最好的消息" |
error | --loglevel error | ||
warn | --loglevel warn | -q 或 --quiet | |
notice | --loglevel notice | ||
http | --loglevel http | ||
timing | --loglevel timing | ||
info | --loglevel info | -d | |
verbose | --loglevel verbose | --d 或 --verbose | 详细打印出每个步骤的信息,有利于排查问题 |
silly | --loglevel silly | -ddd |
我们也可以通过npm config set loglevel
来修改默认的输出级别
除常规的notice级别以外,推荐根据实际情况使用silent和verbose这两种模式
例如:执行命令npm view @arco-desing/web-react
查看包的信息
输出级别 | 输出截图 |
---|---|
verbose | |
notice | |
silent | 没有任何输出 |
生命周期钩子
npm script是具有生命周期机制的,具体来说就是pre 和post
- pre:用于在某些动作之前执行其他的动作
- post:用于在某些动作之后执行其他的动作
npm默认提供以下钩子
- prepublish,postpublish
- preinstall,postinstall
- preuninstall,postuninstall
- preversion,postversion
- pretest,posttest
- prestop,poststop
- prestart,poststart
- prerestart,postrestart
执行npm run build
时,会分3个阶段串行执行
- 检查是否存在prebuild命令,如果有,就执行该命令,否则进入第2阶段
- 检查是否存在build命令,如果有,就执行运行build命令,若执行成功则进入第3阶段,否则就会报错
- 检查是否存在postbuild命令,如果有,就执行该命令
使用npm变量
预定义变量
npm内置了很多变量,可以通过执行npm run env
来查看完整的预定义变量列表,例如:
- npm_package_name:当前项目名称
- npm_package_version:当前项目版本号
在bash中读取变量,只需要加一个前缀$即可(变量不区分大小写)
在windows平台下需要使用%前缀
json
"script": {
"echo:package:name": "echo $npm_package_name",
}
在js文件中读取变量,需要通过process.env
对象来读取(必须是全小写)
json
"script": {
"echo:package:name": "node test.js",
}
// test.js
console.log(process.env.npm_package_name)
自定义变量
我们可以通过在package.json里面配置自定义属性来添加自定义变量
例如,在package.json里面配置默认的端口号
json
{
"name": "app",
"version": "1.0.0",
"config": {
"port": "9000"
},
"scripts": {
"echo:port": "echo $npm_package_config_port"
}
}
这样我们就可以通过npm_package_config_port
来读取端口号
把npm script拆分到单独文件中
当npm script不断累积的时候,全部放在package.json里面会导致可读性大大降低
因此,我们可以把npm script剥离到单独的文件中
shelljs
shelljs提供了各种常见命令的跨平台支持,例如cd、mkdir、exec等命令
通过npm install shelljs -D
安装以后,我们就可以在 . js文件中编写shell命令
javascript
const { env } = require('shelljs')
const npm_package_version = env['npm_package_version']
console.log('package verson is:', npm_package_version)
zx
zx是Google推出的、可用于以javascript编写脚本的工具
通过npm install zx -D
安装以后,我们就可以在 .mjs文件中使用javascript的语法来编写shell命令
- 使用``await $`xxx```的方式来执行shell命令
- 提供常用shell命令对应的内置函数(例如:cd、chalk、fs)
- 所有同步的逻辑(语句)都可以使用原生的javascript语法
例如:将"本地分支关联到远程分支"这个功能封装成一个script命令
git push --set-upstream remoteGitNam currentBranchName
要实现这个功能,我们就需要获取当前分支的名称和远程仓库的名称
获取当前分支的名称直接使用git symbolic-ref --short HEAD
即可
而远程仓库的名默认是origin,但用户可以通过git remote rename
命令进行修改
因此我们可以通过git remote -v
列举出已进行关联的所有远程仓库,再通过正则表达式去匹配我们想要关联的那个远程仓库即可
代码示例如下:
javascript
// setUpStreamBranch.mjs
#!/usr/bin/env zx
// 当前分支的名称
const currentBranchName = await $`git symbolic-ref --short HEAD`
// 远程仓库的名称,用默认值兜底
let remoteName = 'origin'
const result = await $`git remote -v`
const lines = result.stdout.split('\n')
const regExp = new RegExp('\(push\)$')
// 远程仓库中,当前项目的具体名称
const gitName = 'xxx'
for (const line of lines) {
if (line.includes(gitName) && regExp.test(line)) {
remoteName = line.split('\t')[0]
}
}
// 本地分支关联远程分支
await $`git push --set-upstream ${remoteName} ${currentBranchName}`
在package.json
里面添加命令
json
"scripts": {
"set:upstream:branch": "zx setUpstreamBranch.mjs"
}