深入npm script

系列 -
警告
本文最后更新于 2022-12-02,文中内容可能已过时。

npm script 是一个前端人必须得掌握的技能之一。本文基于 npm v7 版本


下文是我认为前端人至少需要掌握的知识点。

npm - package.json

创建项目的第一步,一般都是使用 npm init 来初始化或修改一个 package.json 文件,后续的工程都将基于 package.json 这个文件来完成。

sh

# -y 可以跳过询问直接生成 pkg 文件(description默认会使用README.md或README文件的第一行)
npm init [-y | --scope=<scope>] # 作用域包是需要付费的
# 初始化预使用 npm 包
npm init [initializer]

initializer 会被解析成 create-<initializer> 的 npm 包,并通过 npmx exec 安装(临时)并执行安装包的二进制执行文件。

initializer 匹配规则:[<@scope/>]<name>,比如:

  • npm init react-app demo —> npm exec create-react-app demo
  • npm init @usr/foo —> npm exec @usr/create-foo

这两个命令都是从 npm 包(本地安装或远程获取)中运行任意命令。

sh

# pkg 为 npm 包名,可以带上版本号
# [args...] 这个在文档中被称为 "位置参数"...奶奶的看了我好久才理解
# --package=xxx 等价于 -p xxx, 可以多次指定包
# --call=xxx 等价于 -c xxx, 指定自定义指令
npm exec -- <pkg> [args...]
npm exec --package=<pkg> -- <cmd> [args...]
npm exec -c '<cmd> [args...]'
npm exec --package=foo -c '<cmd> [args...]'

# 旧版 npx
npx -- <pkg> [args...]
npx --package=<pkg> -- <cmd> [args...]
npx -c '<cmd> [args...]'
npx --package=foo -c '<cmd> [args...]'

拓展:

  1. -p 可以指定多个需要安装的包,如果本地没有指定的包会去远程下载并临时安装。

  2. -c 自定义指令运行的是已经安装过的包,也就是说要么已经本地安装过 shell 中可以直接执行,要么-p指定包。另外,可以带入 npm 的环境变量

    sh

     # 查询npm环境变量
     npm run env | grep npm_
     # 把某个环境变量带入shell命令
     npm exec -c 'echo "$npm_package_name"'

辨析:

  • npx:

    sh

    # 这里是把 foo 当指令, 后面的全部是参数
    npx foo bar --package=@npm/foo # ==> foo bar --package=@npm/foo
  • npm exec:

    sh

    # 这里会优先去解析 -p 指定的包
    npm exec foo bar --package=@npm/foo # ==> foo bar
    # 想要让 exec 与 npx 实现一样的效果使用 -- 符号, 抑制 npm 对 -p 的解析
    npm exec -- foo bar --package=@npm/foo # ==> foo bar --package=@npm/foo

    ps 一句:官网(英文真的很重要)和一些中文文档读的是真 tm 累~或许这就是菜狗吧…

npm 环境变量中有一个是:npm_command=run-script,它的别名就是 run

npm run [key],实际上调用的是 npm run-script [key],根据 keypackage.jsonscripts 对象找到对应的要交给 shell 程序执行的命令。(mac 默认是 bash,个人设为 zsh)

teststartrestartstop这四个是内置可以直接执行的命令。

再次遇见 --,作用一样也是抑制 npm 对形如 --flag="options" 的解析,最终把 --flag="options" 整体传给命令脚本。eg:

sh

npm run test -- --grep="pattern"
#> npm_test@1.0.0 test
#> echo "Error: no test specified" && exit 1 "--grep=pattern"

正如 shell 脚本执行需要指定 shell 程序一样,run-scriptpackage.json的 script 对象中解析出的 shell 命令在执行之前会有一步 “装箱” 的操作:node_modules/.bin 加在环境变量 $PATH 的中,这意味着,我们就不需要每次都输入可执行文件的完整路径了。

node_modules/.bin 目录下存着所有安装包的脚本文件,文件开头都有 #!/usr/bin/env node,这个东西叫 Shebang

sh

# "scripts": {
#   "eslint": "eslint ./src/**/*.js"
# }
npm run eslint # ==>node ./node_modules/.bin/eslint *.js

node_modules/.bin 中的文件,实际上是在 npm i 安装时根据安装库的源代码中package.jsonbin 指向的路径创建软链。

JSON

"node_modules/eslint": {
  "version": "8.30.0",
  "bin": {
    "eslint": "bin/eslint.js"
  },
  "engines": {
    "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
  }
}

如果 node_modules/.bin 中没有对应的执行脚本,那么会去全局目录下查找,如果还没有再从环境变量 $PATH 下查找是否有同名的可执行程序,否则就报错啦。

PS: npm run 后不带参数直接执行,可以查看 package.json 中所有可执行的命令,就没必要再去点开文件看了。

直接举个常用的打印日志例子:

sh

# 日志输出 精简日志和较全日志
npm run [key] -s # 全称是 --loglevel silent 也可简写为 --silent
npm run [key] -d # 全称是 --loglevel verbose 也可简写为 --verbose

两个常用的内置变量 npm_config_xxxnpm_package_xxx,eg:

JSON

"view": "echo $npm_config_host && echo $npm_package_name"

当执行命令 npm run view --host=123,就会输出 123 和 package.json 的 name 属性值。

如果上方 [key] 指向的是另一个 npm run 命令,想传参给真正指向的命令该怎么做呢?又得依靠 --的能力了。下面两条命令对比,就是可以把 --fix 传递到 eslint ./src/**/*.js 之后。

diff

"eslint": "eslint ./src/**/*.js",
- "eslint:fix": "npm run eslint --fix",
+ "eslint:fix": "npm run eslint -- --fix"

在脚本文件中,也可以获取命令的传参:

JSON

"go": "node test.js --key=val --host=123"

js

// test.js
const args = process.argv
console.log('📌📌📌 ~ args', args)

const env = process.env.NODE_ENV
console.log('📌📌📌 ~ env', env)

此外,process.env 可以获取到本机的环境变量配置,常用的如:

  • NODE_ENV
  • npm_lifecycle_event,正在运行的脚本名称
  • npm_package_[xxx]
  • npm_config_[xxx]
  • …等等

其中 process.env.NODE_ENV 也可以通过命令来设置,在 *NIX 系统下可以这么使用:

sh

"go": "export NODE_ENV=123 && node test.js --key=val --host=123"

为了抹除平台的差异,常常使用的是 cross-env 这个库。

npm 提供 prepost两种钩子机制,分别在对应的脚本前后执行。

官网提供了集成方法:

sh

npm completion >> ~/.zshrc # 本地 shell 设置的是哪个就是哪个

npm completion 的输出注入 .zshrc 之后就可以通过 tab 来自动补全命令了。

sh

npm config set <key> <value>
npm config get <key>
npm config delete <key>

# 查看配置
npm config list
# 查看全局安装包和全局软链
npm ls -g

npm 3 之前:

txt

+-------------------------------------------+
|                   app/                    |
+----------+------------------------+-------+
           |                        |
           |                        |
+----------v------+       +---------v-------+
|                 |       |                 |
|  webpack@1.15.0 |       |  nconf@0.8.5    |
|                 |       |                 |
+--------+--------+       +--------+--------+
         |                         |
   +-----v-----+             +-----v-----+
   |async@1.5.2|             |async@1.5.2|
   +-----------+             +-----------+

npm 3 之后:

txt

         +-------------------------------------------+
         |                   app/                    |
         +-+---------------------------------------+-+
           |                                       |
           |                                       |
+----------v------+    +-------------+   +---------v-------+
|                 |    |             |   |                 |
|  webpack@1.15.0 |    | async@1.5.2 |   |  nconf@0.8.5    |
|                 |    |             |   |                 |
+-----------------+    +-------------+   +-----------------+

优势很明显,相同的包不会再被重复安装,同时也防止树过深,导致触发 windows 文件系统中的文件路径长度限制错误。

能这么做的原因:得益于 node 的模块加载机制,node 之 require 加载顺序及规则

当我们开发一个 npm 模块或者调试某个开源库时,npm link 就发挥本事了,主要分为两步:

  1. 作为包的目标文件下执行 npm link。它会在创建一个全局软链 {prefix}/lib/node_modules/<package> 指向该命令执行时所处的文件夹。
    这里的 prefix 可以通过 npm prefix -g 来查看
  2. npm link <pkgName> 然后把刚刚创建的全局链接目标链接到项目的 node_modules 文件夹中。
    注意 这里的 package.jsonname 属性而不是文件夹名

举个例子吧,对 react v17.0.2 源码打包,然后在自己项目中链接打包的代码进行调试:

sh

# 安装完依赖后对核心打包
yarn build react/index,react/jsx,react-dom/index,scheduler --type=NODE

# 分别进入react react-dom scheduler 创建软链
cd ./build/react
npm link
cd ./build/react-dom
npm link
cd ./build/scheduler
npm link

对三个包 link 后,本地全局就会多了这三个包,如图:

然后在项目中使用这三个包:

sh

npm link raect react-dom scheduler # 此优先级是高于本地安装的依赖的

解除包是注意:如果是开发 cli 这样的全局包时,需要使用 npm unlink <pkgName> -g 才能生效.

首先得有 npm 账号,直接去官网注册就好,其次有一个可以发布的包,然后:

sh

# ------ terminal ------
# 1. 登录 npm 账号
npm adduser 或者 npm login
# npm whoami 可以查看登录的账号
# 2. 发布
npm publish
# 3. 带有 @scope 的发布需要跟上如下参数
npm publish --access=public
# 4. 更新版本 直接手动指定版本,也可以 npm version [major | minor | patch],自动升对应版本
npm version [semver]

sermver 版本规范

这里与 shell 的符号是一样的:

  • &:如果命令后加上了 &,表示命令在后台执行,往往可用于并行执行
    • 想要看执行过程可以最后添加 wait 命令,等待所有子进程结束,不然类似 watch 监听的命令在后台执行的时候没法 ctrl + c 退出了
  • &&: 前一条命令执行成功后才执行后面的命令
  • |:前一条命令的输出作为后一条命令的输入
  • ||:前一条命令执行失败后才执行后面的命令
  • ; 多个命令按照顺序执行,但不管前面的命令是否执行成功 d

npm-run-all 这个库也实现了以上的执行逻辑,不过我是不建议使用,写命令就老老实实写不好嘛,越写越熟练哈哈~

  • devDependencies,首先无论 dependencies 还是 devDependencies,npm i 都会被安装上的,区别是被安装包的 devDependencies 不会被安装,也就是说一个包作为第三方包被安装时,devDependencies 里的依赖不会被安装,目的就是为了减少一些不必要的依赖。npm install --productionNODE_ENV 被设置为 production 即生产环境,也不会下载 devDependencies 的依赖
  • peerDependencies,就是对等依赖,在 monorepo 和 npm 包中很常见。
    提示宿主环境去安装满足 peerDependencies 所指定依赖的包,然后在 import 或者 require 所依赖的包的时候,永远都是引用宿主环境统一安装的 npm 包,最终解决插件与所依赖包不一致的问题。