npm script原理与实践

1,016 阅读6分钟

本文总结了npm script的原理和基础用法,包括执行多个命令、生命周期钩子、npm变量等等......

1. [What] 什么是npm script

package.json里面定义的scripts字段就是npm script,它的每个属性都对应一段脚本

{
  // ...
  "scripts": {
    "build": "node build.js"
  }
}

npm script的执行原理

当我们在项目里面运行npm run xxx的时候,实际步骤是:

  1. package.json里面读取scripts对象

  2. 以传给npm run的第一个参数作为键,在scripts对象里面获取对应的值作为接下来要执行的命令,如果没找到就直接报错

  3. 新建一个shell,将当前目录的node_modules/.bin子目录加入PATH变量(这就意味着,当前目录的node_modules/.bin子目录里面的所有脚本,都可以直接用脚本名调用,而不需要加上路径)

  4. 在这个shell中执行上述命令

npm script的退出码也遵守shell脚本的规则,如果退出码不是0,则视为脚本执行失败

2. [Why] 为什么需要npm script

使用npm script主要有以下几个好处:

  • 编写单一职责的命令,提高代码复用性

  • 不同项目的脚本,只要功能相同,就可以有相同的对外接口,提升可读性、降低项目的上手门槛

  • 通过连接多个命令,可以打造自动化的工作流

    • 减少重复地手动执行命令的频率

    • 避免人为因素(例如:单词拼写错误/参数输入错误/遗漏某个命令等)导致命令执行失败,提高命令执行的成功率

3. [How] 如何使用npm script

3.1 执行多个命令

3.1.1 在当前目录下执行多个不同命令

  1. 串行执行

使用&&符号连接多个命令,当前序命令失败时,后续命令都不会执行

"script": {
  "lint:js": "eslint *.js",
  "lint:css": "eslint *.less",
  "lint:all": "npm run lint:js && npm run lint:css"
}
  1. 并行执行

使用单个&符号来连接多个命令

"script": {
  "lint:js": "eslint *.js",
  "lint:css": "eslint *.less",
  "lint:all": "npm run lint:js & npm run lint:css"
}

3.1.2 在不同目录下执行多个相同命令

对于monorepo项目,一个项目的目录下可能会有多个workspace(工作区),可以在项目根目录的package.json里面进行如下配置

"workspaces": [
   "packages/a",
   "packages/b",
   "packages/c"
]

运行npm script时可以通过-w来指定工作区,在(单个、多个或者所有)工作区的上下文中串行运行指定的命令

几个简单的例子

  • packages/a目录下执行npm build
  • packages/apackages/b目录下执行npm build
  • 在所有workspaces下执行npm build
# 在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

3.2 传递参数

向npm脚本传递参数需要用 -- 标明

"script": {
  "lint:js": "eslint *.js",
  "lint:js:fix": "npm run lint:js -- --fix",
}

当执行npm run lint:js:fix的时候,实际运行的命令是eslint *.js --fix

3.3 添加注释

可以直接在命令前面以#开头加上注释

"script": {
  "test": "# 这是一条注释 \n exit1 "
}

3.4 调整日志输出

执行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没有任何输出

3.5 生命周期钩子

npm script是具有生命周期机制的,具体来说就是prepost

  • pre:用于在某些动作之前执行其他的动作
  • post:用于在某些动作之后执行其他的动作

npm默认提供以下钩子

  • prepublish,postpublish
  • preinstall,postinstall
  • preuninstall,postuninstall
  • preversion,postversion
  • pretest,posttest
  • prestop,poststop
  • prestart,poststart
  • prerestart,postrestart

执行npm run build时,会分3个阶段串行执行

  1. 检查是否存在prebuild命令,如果有,就执行该命令,否则进入第2阶段
  2. 检查是否存在build命令,如果有,就执行运行build命令,若执行成功则进入第3阶段,否则就会报错
  3. 检查是否存在postbuild命令,如果有,就执行该命令

3.6 使用npm变量

3.6.1 预定义变量

npm内置了很多变量,可以通过执行npm run env来查看完整的预定义变量列表,例如:

  • npm_package_name:当前项目名称
  • npm_package_version:当前项目版本号

在bash中读取变量,只需要加一个前缀$即可(变量不区分大小写)

在windows平台下需要使用%前缀

"script": {
  "echo:package:name": "echo $npm_package_name",
}

在js文件中读取变量,需要通过process.env对象来读取(必须是全小写)

"script": {
  "echo:package:name": "node test.js",
}

// test.js
console.log(process.env.npm_package_name)

3.6.2 自定义变量

我们可以通过在package.json里面配置自定义属性来添加自定义变量

例如,在package.json里面配置默认的端口号

{
  "name": "app",
  "version": "1.0.0",
  "config": { 
    "port": "9000" 
  }, 
  "scripts": {
    "echo:port": "echo $npm_package_config_port"
  }
}      

这样我们就可以通过npm_package_config_port来读取端口号

3.7 把npm script拆分到单独文件中

当npm script不断累积的时候,全部放在package.json里面会导致可读性大大降低

因此,我们可以把npm script剥离到单独的文件中

3.7.1 shelljs

shelljs提供了各种常见命令的跨平台支持,例如cd、mkdir、exec等命令

通过npm install shelljs -D安装以后,我们就可以在 . js文件中编写shell命令

const { env } = require('shelljs')

const npm_package_version = env['npm_package_version']

console.log('package verson is:', npm_package_version)

3.7.2 zx

zx是Google推出的、可用于以javascript编写脚本的工具

通过npm install zx -D安装以后,我们就可以在 .mjs文件中使用javascript的语法来编写shell命令

  • 使用await $`xxx`的方式来执行shell命令
  • 提供常用shell命令对应的内置函数(例如:cd、chalk、fs)
  • 所有同步的逻辑(语句)都可以使用原生的javascript语法

3.8 场景实践

在团队协作中,我们经常会遇到“给他人添加npm包的负责人权限”这样的需求,手动重复地输入命令npm owner add <user> <package>显然过于繁琐

因此我们可以将这一段逻辑抽象成npm script,思路如下:

  • 通过question获取用户输入的npm账户名
  • 通过fs.readdir遍历文件目录
  • 通过fs.readJsonSync获取npm包名,执行命令npm owner add <user> <package>

代码示例如下:

// addOwner2.mjs
#!/usr/bin/env zx
import chalk from 'chalk'
import { fs, question } from 'zx'

const username = await question(`Enter user's email prefix below: \n`)

console.log(chalk.green('Start to add owner... \n'))

await fs.readdir('./packages', (_, files) => {
  files.forEach(async (file) => {
    const path = `./packages/${file}/package.json`
    if (fs.existsSync(path)) {
      const { name: packageName } = fs.readJSONSync(path)
      if (packageName) {
        if ((await $`npm owner add ${username} ${packageName} -s`.exitCode) === 0) {
          console.log(chalk.green(`Successfully added ${username} to ${packageName} \n`))
        } else {
          console.log(chalk.red(`Failed to add ${username} to ${packageName} \n`))
        }
      } else {
        console.log(chalk.red(`Package name not found in ${path} \n`))
      }
    }
  })
})

package.json里面添加命令

"scripts": {
  "add:owner": "zx addOwner.mjs"
}

参考资料

scripts | npm Docs

npm scripts 使用指南 - 阮一峰的网络日志

用 npm script 打造超溜的前端工作流 - 掘金小册