精通 npm 脚本

Paula Santamaría 原作,授权 New Frontend 翻译。

你可能在 package.json 文件里看到过 scripts 属性,甚至自己写过一些脚本。但你知道 npm 脚本都能干什么吗?

我用 npm 脚本有好些年了,但几周前想要给脚本传个参数的时候突然意识到我不知道这要怎么做。所以我就决定好好学习一番 npm 脚本,写下这篇文章。

我会在这篇文章中分享我学到的内容,如何充分利用 npm 脚本。

引言

npm 脚本是在 package.json 文件中定义的一组内置的或自定义的脚本,目的是提供一种执行重复任务的简单方式,比如:

  • 运行代码检查(linter)工具
  • 执行测试
  • 本地启动项目
  • 构建项目
  • 精简(minify/uglify)JS 和 CSS

在 CI/CD 流程中也可以使用 npm 脚本简化构建、生成测试报告之类的任务。

定义 npm 脚本只需选定脚本名称并在 script 文件中写下运行命令:

{
    "scripts": {
        "hello-world": "echo \"Hello World\""
    }
}

需要注意的是,在 npm 脚本中,所有依赖提供的可执行命令都是可用的。你可以直接使用它们,就像使用其他 PATH 中包含的命令一样。来个例子:

{
    "scripts": {
        "lint": "./node_modules/.bin/eslint .",
    }
}

可以直接写成:

{
    "scripts": {
        "lint": "eslint ."
    }
}

npm run

现在只需打开终端,在项目的根目录运行 npm run hello-world 就能执行刚才定义的脚本。

> npm run hello-world

"Hello World"

运行 npm run 则会返回所有可用的脚本

> npm run

Scripts available in sample-project via `npm run-script`:
    hello-world
        echo "Hello World"

如你所见,npm run 不仅会输出 package.json 中列出的脚本名称,还会输出具体的脚本命令。

ℹ️ npm runnpm run-script别名,也就是说,你也可以运行 npm run-script hello-world。这篇文章会使用 npm run <script>,因为它比较短。

内置脚本和别名

在上面的例子中,我们创建了一个叫做 hello-world自定义脚本,不过 npm 同样支持 teststart 之类的内置脚本

比较有意思的是,不同于自定义的脚本,这些内置脚本可以使用别名运行,这样整个命令就更简短易记。例如,下面的命令都会运行 test 脚本。

npm run-script test
npm run test
npm test
npm t

类似地,下面这些命令都会运行 start 脚本:

npm run-script start
npm run start
npm start

要使内置脚本生效,需要先在 package.json 中定义它们,否则无法成功运行。定义它们的方法和定义其他脚本一样。下面是个例子:

{
    "scripts": {
        "start": "node app.js",
        "test": "jest ./test",
        "hello-world": "echo \"Hello World\""
    }
}

执行多个脚本

如果想要组合多个脚本一并运行,可以使用 &&&

  • 使用 && 串行执行多个脚本,例如:npm run lint && npm test
  • 使用 & 并行执行多个脚本,例如:npm run lint & npm test

    • 这只在 Unix 环境下有效。在 Windows 下会串行执行。

比如,我们可以创建组合其他两个脚本的脚本:

{
    "scripts": {
        "lint": "eslint .",
        "test": "jest ./test",
        "ci": "npm run lint && npm test"
    }
}

理解错误

脚本完成时的退出状态码不为零意味着出现了错误

这也意味着我们可以通过指定非零退出码有意让脚本失败。

{
    "scripts": {
        "error": "echo \"This script will fail\" && exit 1"
    }
}

脚本报错时我们会得到一些细节,比如错误码 errnocode,这些有助于我们上网搜索这个报错。

如果需要更多信息,可以查看完整的日志文件。错误信息的末尾有相应的路径。脚本失败时,这个文件会包含完整的日志。

静默执行或输出详细信息

npm run <script> --silent 可以减少日志阻止脚本报错

--silent--loglevel silent 的简写。如果你想要运行可能失败的脚本却不想收到报错,可以使用这个选项。比如你可能希望在 test 命令失败的时候仍然继续运行整个 CI 流程。

也可以进一步简写为 -snpm run <script> -s

ℹ️ 如果要让脚本不存在时也不要报错,可以使用 --if-present 选项:npm run <script> --if-present

关于日志级别

我们已经知道 --silent 可以减少日志。不过如果我们需要更详细的日志呢?可以调节吗?

日志级别包括:silenterrorwarnnoticehttptiminginfoverbosesilly。默认级别是 notice。日志级别决定输出哪些日志。比当前定义的级别高(更严重)的日志也会输出。

使用 --loglevel <level> 可以明确指定日志级别。如前所述,--silent 等效于 --loglevel silent

所以,如果希望更详细的日志,我们可以定义指定 --loglevel info 之类比默认级别(notice)严重程度低的级别。

许多级别都有简写:

  • -s--silent--loglevel silent
  • -q--quiet--loglevel warn
  • -d--loglevel info
  • -dd--verbose--loglevel verbose
  • -ddd--loglevel silly

所以如果希望输出尽可能多的日志,我们可以使用 npm run <script> -dddnpm run <script> --loglevel silly

引用脚本文件

npm 脚本支持执行脚本文件,适用于复杂脚本。不过如果脚本非常简短就没有写到单独文件中的必要了。

下面是一个例子:

{
    "scripts": {
        "hello:js": "node scripts/helloworld.js",
        "hello:bash": "bash scripts/helloworld.sh",
        "hello:cmd": "cd scripts && helloworld.cmd"
    }
}

在这个例子中,我们使用 node <script-path.js> 执行 JS 文件,bash <script-path.sh> 执行 bash 文件。

注意 CMD 和 BAT 文件无法直接通过 scripts/helloworld.cmd 运行,需要首先通过 cd 进入文件所在目录。否则 npm 会报错。

把脚本写在单独的文件还有一项优势,如果脚本比较复杂,这样比在 package.json 里写很长的一行要容易维护。

前置和后置

你可以为任何脚本创建前置(pre)和后置(post)脚本,npm 会自动依序执行。唯一的要求是在脚本名称中带上 prepost 前缀,例如:

{
    "scripts": {
        "prehello": "echo \"--Preparing greeting\"",
        "hello": "echo \"Hello World\"",
        "posthello": "echo \"--Greeting delivered\""
    }
}

如果运行 npm run hello,npm 会按照如下顺序执行脚本:prehellohelloposthello。输出如下:

> script-test@1.0.0 prehello
> echo "--Preparing greeting"

"--Preparing greeting"

> script-test@1.0.0 hello
> echo "Hello World"

"Hello World"

> script-test@1.0.0 posthello
> echo "--Greeting delivered"

"--Greeting delivered"

ℹ️ 如果单独运行 prehelloposthello,npm 不会自动运行其他脚本。只有运行「主」脚本(hello)时才会自动运行。

访问环境变量

执行 npm 脚本时,npm 会提供一些可供使用的环境变量。这些环境变量基于 npm 配置、package.json等资源生成。

npm_config_ 前缀的环境变量是一些配置项。以下是一些例子:

{
    "scripts": {
        "config:loglevel": "echo \"Loglevel: $npm_config_loglevel\"",
        "config:editor": "echo \"Editor: $npm_config_editor\"",
        "config:useragent": "echo \"User Agent: $npm_config_user_agent\""
    }
}

执行上面这些命令的结果如下:

> npm run config:loglevel
# Output: "Loglevel: notice"

> npm run config:editor
# Output: "Editor: notepad.exe"

> npm run config:useragent
# Output: "User Agent: npm/6.13.4 node/v12.14.1 win32 x64"

ℹ️ 运行 npm config ls -l列出所有配置项

类似地,versionmain 之类的 package.json 字段,对应的环境变量以 npm_package_ 为前缀。看些例子:

{
    "scripts": {
        "package:main": "echo \"Main: $npm_package_main\"",
        "package:name": "echo \"Name: $npm_package_name\"",
        "package:version": "echo \"Version: $npm_package_version\""
    }
}

执行上面这些命令,会得到类似下面这样的结果:

> npm run package:main
# Output: "Main: app.js"

> npm run package:name
# Output: "Name: npm-scripts-demo"

> npm run package:version
# Output: "Version: 1.0.0"

最后,在 package.jsonconfig 字段中可以加上自定义环境变量。这些环境变量会以 npm_package_config 为前缀。

{
    "config": {
        "my-var": "Some value",
        "port": 1234
    },
    "script": {
        "packageconfig:port": "echo \"Port: $npm_package_config_port\"",
        "packageconfig:myvar": "echo \"My var: $npm_package_config_my_var\""
    }
}

运行上述命令的结果是:

> npm run packageconfig:port
# Output: "Port: 1234"

> npm run packageconfig:myvar
# Output: "My var: Some value"

ℹ️ 在 Windows 的命令提示符(cmd)环境下,需要使用 %npm_package_config_port% 而不是 $npm_package_config_port 来访问环境变量。

传递参数

在有些情况下,你可能想要给脚本传些参数。可以在命令后加上 --,比如 npm run <script> -- --argument="value"

让我们看个例子:

{
    "scripts": {
        "lint": "eslint .",
        "test": "jest ./test",
    }
}

如果只想跑改动过的测试:

> npm run test -- --onlyChanged

如果想把代码检查工具的结果保存到某个文件:

> npm run lint -- --output-file lint-result.txt

通过环境变量传递参数

另一种传递参数的方法是通过环境变量。传给脚本的键值对会被转换为以 npm_config 为前缀的环境变量。也就是说,下面的脚本:

{
    "scripts": {
        "hello": "echo \"Hello $npm_config_firstname!\""
    }
}

可以这样调用:

> npm run hello --firstname=Paula
# Output: "Hello Paula"

命名管理

命名脚本没什么特别的规则。不过,给脚本起名字的时候如果注意一些原则,和其他开发者协作就会更方便。

  • 保持简短。如果你去看 Svelte 的 npm 脚本,会注意到大多数脚本名称都是一个单词。保持简短,便于记忆。
  • 保持一致。命名脚本时可能需要使用多个单词。这种情况下需要选定一种命名风格并处处使用。可以是驼峰(camelCase),也可以是短横(kebab-case),或者任何其他你偏爱的风格。但要避免混用不同风格。

前缀

你可能注意到很多项目的惯例是使用前缀和冒号来给脚本分组,比如 build:prod。这不过是一种通过前缀区分脚本组别的命名惯例,不会影响脚本的行为。

例子:

{
    "scripts": {
        "lint:check": "eslint .",
        "lint:fix": "eslint . --fix",
        "build:dev": "...",
        "build:prod": "..."
    }
}

文档

考虑给脚本加上文档,便于其他人理解如何以及何时使用这些脚本。我一般会在 README 中简单说明下每个脚本的作用。

每个脚本的文档应该包括以下内容:

  • 脚本名称
  • 描述
  • 接受的参数(可选)
  • 指向其他文档的链接(可选):例如,如果脚本会执行 tsc --build,可以考虑加个指向 TypeScript 文档的链接。

结语

这些便是我钻研 npm 脚本学到的东西。我希望你觉得它们有用!我在这个过程中学到了不少东西。学习这些东西花掉的时间比我预计的多,但很值。

如果我漏掉了什么东西,请留言告诉我,让这篇指南更加完整。💬

评论

Loading comments ...