npm - npx 深度理解

1,259 阅读5分钟

npmnpx 的区别?

npxnpm 是两个 Node.js 包管理工具中的命令行工具,它们之间有一些区别:

  1. npmNode.js 的包管理器,用于安装和管理 JavaScript 模块,以及在项目中运行脚本。

    npxnpm 5.2.0 及更高版本中包含的一个命令行工具,用于执行本地安装的或在线安装的 Node.js 包中的命令。

  2. npm 安装的包需要在本地全局或项目依赖中进行安装,才能在命令行中直接使用。而 npx 可以在不需要全局安装的情况下直接运行某个包中的命令。

  3. npx 会首先检查本地是否存在指定的包,如果存在则直接运行,如果不存在则先下载该包,再执行其中的命令。这使得 npx 更加灵活,不需要事先安装一个包就能立即运行它的命令。

总之,npm 主要用于管理和安装依赖包,而 npx 主要用于运行本地或在线安装的包中的命令。


package.json bin字段:

全局包中的 bin 字段的作用:

package.json bin 中设置 命令,全局安装时,此命令会在安装后注册到全局 ./node_modules/.bin/ 二进制文件;

  1. 当只有一个命令时, 可以放在 bin字段 对象中,设置 key : value , 命令为 key

  2. 当只有一个命令时, 还可以省略 bin 字段对象,只设置 bin 指向的 执行文件地址, 命令为 package.json name 字段值;

  3. 当有多个命令时,可以放在 bin字段 对象中,设置多组 key : value , 命令分别为 key

  4. [注]:value 只能是文件的路径

示例:

// test-package package.json 
{ 
    "bin": { 
        "test1": "cli.js", 
        "test2": "cli2.js" 
    } 
} 

// 1. 如果只有一个命令的话 
// 1-1: 命令 test1 
{ 
    "bin": { 
        "test1": "cli.js" 
     } 
 } 
 
 // 1-2: 命令 test-package 
 { 
     "name": "test-package" 
     "bin": "cli.js" 
 } 
 
 // 2. 如果有多个命令 
 { 
     "bin": { 
         "test1": "cli.js", 
         "test2": "cli2.js" 
     } 
  }

用法:

// test-package 
npm install test-package -g 

> test1 
// -> test1 node 脚本 

> test2 
// -> test2 node 脚本

什么是 main bin

// 1. 当 <package> 的 package.json 中 bin字段对象中有多条 <key:value> 时,key值 和 name字段的 value值 相同时, <命令 key> 为 main bin; 
{ 
    "name": "test-pkg", 
    "bin": { 
        "test-pkg": "./cli.js", 
        "test1": "./cli1.js" 
        } 
    }
}

// 2. 当 <package> 的 package.json 中 bin字段对象中只有一条 <key:value> 时, 此时这一条命令 <命令 key> 为 main bin; 
{ 
    "name": "test-pkg", 
    "bin": { 
        "test1": "./cli1.js" 
    } 
} 

// 3. 当 <package> 的 package.json 中 bin字段 的 value值为一个指向执行文件的路径字符串时,name字段的 value值,即为 main bin; 
{ 
    "name": "test-pkg", 
    "bin": "./cli.js"
} 

// 4. 当 bin 字段对象中有多个 <key:value>, 并且没有 key值 等于 package.json 中的 name 字段值时,会按照 位置排名选出 main bin 命令; 
// package.json 如下: 
{ 
    "name": "test-pkg", 
    "bin": { 
        "b": "./b.js",
        "a": "./a.js", 
    } 
} 
// 此时,<command a> 即为 main bin; 
// [注]:此判定方式在 npm v5~v6 版本 验证如此;npm v7~x 版本会抛出错误: `npm ERR! could not determine executable to run`

npx 命令:

功能:

优先本地包中执行命令;

介绍:

  1. npxnpm v5.2.0 引入的一条命令(npx),一个 npm 包执行器,指在提高从 npm 注册表使用软件包时的体验。

  2. npx 命令执行

    1. 会在 ~/.npm/_npx 最终生成包的临时文件;
    2. 然后执行 的 main bin 指定的可执行脚本文件;
  3. npx 的原理很简单,就是运行的时候,会到 node_modules/.bin 路径和环境变量 $PATH 里面,检测命令是否存在,存在则执行,不存在则临时安装 <package> 来执行。

版本差异:

V1:

npm v5.2.0 -- v6.14.18 :

node v14 - (npm v6.14.17) 及 node v14 之前版本:

npx <package> 
    // 1. 如果local 和 global 都找不到此 package 时, 会在 ~/.npm/_npx 下载生成包的临时文件夹,然后执行 主bin 所指定的可执行脚本文件; 
    // 2. 执行完毕之后,删除临时包文件夹; 

main bin 命令: 
// 1. 当 bin 字段对象中有多个 <key:value>, 并且没有 key值 等于 package.json 中的 name 字段值时,会按照 位置排名选出 main bin 命令; 
// 如: 
// package.json 
{ 
    "name": "test-pkg", 
    "bin": { 
        "b": "./b.js",
        "a": "./a.js", 
    } 
} 
// 此时,<command a> 即为 main bin; 

// 其它 main bin 的判定方式,参考 "什么是 main bin ?"

V2:

npm v7.0.0 -- v8.19.4:

node v15 - (npm v7.7.6):

npx <package> 
    // 1.如果 local 和 global 都找不到此 <package> 时,会先做一个 Need to install the following packages: 的提醒,由用户手动输入 y or yes 来决定是否继续安装; 
    // 2.安装之后,同样会在 ~/.npm/_npx 下载生成一个包的临时文件夹,然后执行 主bin 所指定的可执行文件; 
    // 3.执行完毕之后,并不会立即删除临时包文件夹; 
    // 》🤔这里留了个疑问,这个临时文件什么时候删除!?还是就是不删除了!? 
    // 》 猜测,npx 运行时,会去检测远程包的更新情况,来决定是否重新拉取远程包;如果有变化,会执行重新安装流程,否则,直接使用 ~/.npm/_npx 中已存在的临时包;

V3:

npm v9.0.0 ~ x:

node v16, v18,v20 - (npm v9.6.7) :

npx <package> 
    // ~/.npm/_npx 中已有<package>缓存,则使用缓存中的版本(不一定是 @latest 版本),然后执行 主bin 命令指定的可执行文件 
    // ~/.npm/_npx 没有<package>的缓存,则拉取最新版本的包,然后执行 主bin 命令指定的可执行文件 npx <package>[@version] 
    // 会拉去对应版本的包,然后执行 主bin 命令指定的可执行文件 npx <package>[@latest] 
    // 会拉取最新版本的包,然后执行 主bin 命令指定的可执行文件 npx clear-npx-cache 
    // 清除 ~/.npm/_npx 下的缓存文件

参考: issue

语法:

// 快捷使用方式: 
npx -- <pkg>[@<version>] [args...] 
npx --package=<pkg>[@<version>] -- <cmd> [args...] 
npx -c '<cmd> [args...]' 
npx --package=foo -c '<cmd> [args...]'
// 详细介绍: 

从 npm 的可执行包执行命令

    npx [选项] <命令>[@版本] [命令的参数]... 
    
    npx [选项] [-p|--package <包>]... <命令> [命令的参数]... 
    
    npx [选项] -c '<命令的字符串>' 
    
    npx --shell-auto-fallback [命令行解释器] 

选项: 

    --package, -p 包安装的路径 [字符串] 
    
    --cache npm 缓存路径 [字符串] 
    
    --always-spawn Always spawn a child process to execute the command. [布尔] 
    
    --no-install 如果有包缺失,跳过安装 [布尔] 
    
    --userconfig 当前用户的 npmrc 路径 [字符串] 
    
    --call, -c 像执行 `npm run-script` 一样执行一个字符串 [字符串] 
    
    --shell, -s 执行命令用到的解释器,可选 [字符串] [默认值: false] 
    
    --shell-auto-fallback 产生“找不到命令”的错误码 [字符串] [可选值: "", "bash", "fish", "zsh"] 
    
    --ignore-existing 忽略 $PATH 或工程里已有的可执行文件,这会强制使 npx 临时安装一次,并且使用其最新的版本 [布尔] 
    
    --quiet, -q 隐藏 npx 的输出,子命令不会受到影响 [布尔] 
    
    --npm 为了执行内部操作的 npm 可执行文件 [字符串] [默认值: "~/.nvm/versions/node/v14.21.2/lib/node_modules/npm/bin/npm-cli.js"] 
    
    --node-arg, -n 调用 node 二进制时使用额外的 node 参数。 [字符串] 
    
    --version, -v 显示版本号 [布尔] 
    
    --help, -h 显示帮助信息 [布尔]

执行流程描述:

// 命令: 
$ npx <package> 

// 执行流程 
if <package> 已经做了 local install 
    // 1. 找到,local ./node_modules/<package> 目录下的 package.json 中的 bin 字段 指定的主bin <key>:<file path>; 
    // 2. 到 local ./node_modules/.bin 目录下,找打以 <key> 为名的二进制可执行文件,进行执行
    
else if <package> 未做 local install, 做了 global install 
    // 1. 寻找 local(本地) ./node_modules/<package> , 未找到。 
    // 2. 继续寻找 global(全局) ./node_modules/<package>, 找到; 
    // 3. 记录 <package> 目录下 package.json 中的 bin字段 指定的主bin <key> 值; 
    // 4. 到 global ./node_modules/.bin 目录下,找打以 <key> 为名的二进制可执行文件,进行执行; 
    
else if <package> 未做 local install, 未做 global install 
    // 1. 寻找 local(本地) ./node_modules/<package> , 未找到。 
    // 2. 寻找 global(全局) ./node_modules/<package> , 未找到。 
    // 3. 在 ~/.npm/_npx 目录下,做临时安装;生成临时目录[Dir](经过算法计算), 临时目录[Dir]下,生成 package.json 并 安装 <package> 到 node_modules, 
    //    并将 node_modules/<package> package.json 中的 bin字段 指定的主bin <key>:<file path>, <file path> 所指定的文件以 <key> 命名生成可执行的二进制文件; 
    // 4. 执行临时目录[Dir] ./node_modules/.bin 下找打以 <key> 为名的二进制可执行文件,进行执行; 
    // 5. 执行结束,删除临时目录[Dir];

测试:

  1. npx 执行本地安装包的 bin 命令
cd my-app 
npm install <pkg> 
npx <pkg> [arg...] 

# npx 会执行 ./node_modules/.bin/<pkg bin中命令> 二进制文件
  1. npx 执行全局安装包的 bin 命令
npm install 
<pkg> -g 
npx <pkg> [arg...] 

# npx 会执行全局 ./node_modules/.bin/<pkg bin中命令> 二进制文件
  1. npx [arg...] 过程
npx <pkg> [arg...] 

# step 1. 检测本地 ./node_modules/<pkg> 是否存在,存在,则找到包的 main bin 进行执行; 
# step 2. 检测全局 ./node_modules/<pkg> 是否存在,存在,则找到包的 main bin 进行执行; 
# step 3. a. 在 ~/.npm/_npx 目录下,做临时安装;生成临时目录[Dir](经过算法计算), 临时目录[Dir]下,生成 package.json 并 安装 <package> 到 node_modules, 
#         b. 并将 node_modules/<package> package.json 中的 bin字段 指定的主bin <key>:<file path>, <file path> 所指定的文件以 <key> 命名生成可执行的二进制文件; 
#         c. 执行临时目录[Dir] ./node_modules/.bin 下找打以 <key> 为名的二进制可执行文件,进行执行; 
#         d. 执行结束,删除临时目录[Dir];

示例:

# 当通过 npx 运行时,双连字符 -- 标志可以用来抑制 npm 对应该发送到被执行命令的开关和选项的解析: 
npx -- <bin command> --package=<package> 
# 等同 
npx --package=<package> -- <bin command> 

# 指定 --package 选项,可以执行与包名称不匹配的命令: 
npx --package=<package> -- <bin command> 

# 不指定 --package 选项,只会执行与包名称一致的命令 
npx <bin commond> -c 'test2' // 此时只会执行 <bin commond>, -c 后边的 'test2' 并不会执行 

# npx 相当于快速执行 bin 目录中的命令; node_modules/.bin 中存在的二进制文件,都可以通过 npx 执行;(前提是 源文件还在, 也就是对应 node_modules/<package>/package.json bin字段中所指定文件还在) 
# 如:node_modules/.bin 中包含 test1、test2 两个二进制文件; 
npx test1     # test1 被执行 
npx test2     # test2 被执行

参考:

npm 中文文档

npm 官方文档


npm-exec

功能:

从 npm 包运行命令;

概要:

npm exec -- <pkg>[@<version>] [args...] 
npm exec --package=<pkg>[@<version>] -- <cmd> [args...] 
npm exec -c '<cmd> [args...]' 
npm exec --package=foo -c '<cmd> [args...]' 

alias: x

描述:

  • 这个命令允许你从 npm 包(本地安装或远程获取)中运行任意命令。于 npx 功能基本相同;
  • v7.0.0 之后加入的命令;
  • 临时文件包存储位置 ~/.npm/_npx ;

用法:

1、直接使用包和参数命令

npm exec -- lerna init 
npm exec -- lerna@latest init

2、指定包 --package 和 命令参数

npm exec --package=lerna@latest -- lerna init

3、使用 --package 指定包 和 --call 指定命令

npm exec --package=lerna -c "lerna init"

4、只指定 --call 命令,不指定 --package

npm exec -c "lerna init" 
npm exec -c "echo hello"

如果不指定 --package ,那么-c的命令参数,需要 lerna 命令可以直接执行。


npx 和 npm exec 的区别?

语法对比:

npm exec:

npm exec -- <pkg>[@<version>] [args...] 
npm exec --package=<pkg>[@<version>] -- <cmd> [args...] 
npm exec -c '<cmd> [args...]' 
npm exec --package=foo -c '<cmd> [args...]' 

alias: x

npx :

npx -- <pkg>[@<version>] [args...] 
npx --package=<pkg>[@<version>] -- <cmd> [args...] 
npx -c '<cmd> [args...]' 
npx --package=foo -c '<cmd> [args...]'

结论:

从上述概要对比上来看, npm exec 和 npx 的语法是一样的,npx 语法相当于 npm exec 的简写;

执行效果对比

npm exec :

npm exec foo@latest bar --package=@npmcli/foo 

# 首先,npm 将首先解析 --package 选项,解析 @npmcli/foo 包
# 然后,它将在该上下文中执行以下命令: 
foo@latest bar

npx :

npx foo@latest bar --package=@npmcli/foo 

# npm 先解析 foo 包名
# 再执行 如下命令: 
foo bar --package=@npmcli/foo 

# 由于 --package 选项出现在位置参数之后,因此它被视为所执行命令的参数。

建议:

# npm exec 建议使用双连字符显式地告诉 npm 停止解析命令行选项和开关
# 因此,以下命令相当于上面的 npx 命令: 
npm exec -- foo@latest bar --package=@npmcli/foo

总结:

npxnpm exec 作用基本相同,只是解析选项的顺序有所不同;


npm init

功能:

  • npm init 主要作用,创建一个 package.json 文件;
  • npm init <initializer> 执行一个包的初始化器(本地 或 远程)

概要:

npm init [--force|-f|--yes|-y|--scope] 
npm init <@scope> (same as `npx <@scope>/create`) 
npm init [<@scope>/]<name> (same as `npx [<@scope>/]create-<name>`) 

aliases: create, innit

描述:

  1. npm init 传统方式作用,快速创建包的 package.json;

  2. npm init <initializer> 作用,可以利用 <initializer> 快速建立一个新包或现有的 npm 包;

    • 这里的 是一个名为 create-<initializer>npm 包,它将被 npm-exec 安装,然后执行它的主bin -- 可能是创建或更新 package.json,并运行其他与初始化相关的操作。

    • 注意:如果用户已经全局安装了 create-<initializer> 包,则 npm init 将使用该包。如果你想让 npm 使用最新版本,或者其他特定版本,你必须指定它:

      • npm init foo@latest  # 从注册中心获取并运行最新的 create-foo
      • npm init foo@1.2.3  # 专门运行 create-foo@1.2.3
    • 任何其他选项都将直接传递给命令,因此 npm init foo -- --hello 将映射到 npm exec -- create-foo --hello

用法:

npm init 传统使用:

# npm init 传统使用 

npm init 
# 初始化一个 package.json 文件, 内容命令交互界面手动设置 

npm init [-y|--yes] 
# 快速初始化一个 package.json 文件, 快速将内容直接写进了 package.json,无需手动设置;(同 npm init [-f|--force]) 

npm init [-f|--force] 
# 快速初始化一个 package.json 文件, 快速将内容直接写进了 package.json,无需手动设置;(同 npm init [-y|--yes]) 

npm init [--scope=<scopeName>] 
# 1. 初始化一个 package.json 文件,内容命令交互界面手动设置; 
# 2. 默认: package name: (@<scopeName>/<packageName>) 
# 
# [注]. 默认情况下,scoped package是私有的,想发私有 module 的话,需要充值成为私有模块用户; 
#      公共范围模块是免费的,不需要付费订阅。要发布公共范围模块,需在发布时设置访问选项 --access=public。此选项将保留为所有后续发布设置。 

// 小知识点:
// 发布公共的 scoped package: 

// 1. 命令行 方式:
$ npm publish --access=public 

// 2. package.json 方式:
{
    "publishConfig": { 
        "access": "public", 
        "registry": "https://registry.npmjs.org/" 
    }
}

npm init <initializer> 使用:

npm init <package-spec>:

// npm init <initializer> 使用: 

npm init <package-spec> 
// 1. 提示安装 create-<package>[@<version>] 
// > Ok to proceed? (y) 
// 按下 y键 
// 2. 他会被 npm exec <package>,然后执行它的 主bin; 

npm init <@scope> 
// 1. 提示安装 <@scope>/create[@version] 
// > Ok to proceed? (y) 
// 按下 y键 
// 2. 他会被 npm exec <@scope>/create[@version],然后执行它的 主bin; 

npm init <@scope>/<name> 
// 1. 提示安装 <@scope>/create-<name>[@version] 
// > Ok to proceed? (y) 
// 按下 y键 
// 2. 他会被 npm exec <@scope>/create-<name>[@version],然后执行它的 主bin;

init 命令被转换成相应的 npm exec / npx 操作,如下所示:

npm init foo 
// -> npm exec create-foo 
// -> npx create-foo 

npm init @usr 
// -> npm exec @usr/create 
// -> npx @usr/create 

npm init @usr/foo 
// -> npm exec @usr/create-foo 
// -> npx @usr/create-foo 

npm init @usr@2.0.0 
// -> npm exec @usr/create@2.0.0 
// -> npx @usr/create@2.0.0 

npm init @usr/foo@2.0.0 
// -> npm exec @usr/create-foo@2.0.0 
// -> npx @usr/create-foo@2.0.0

npxyarn createnpm create 的区别

应用方式:

create-react-app 为例,文档给了我们三种创建应用的方式,如下:

# npx 方式 
npx create-react-app my-app 

# init 方式 
npm init react-app my-app 

# create 方式 
npm create react-app my-app

执行分析

  1. npx 方式,先下载 create-react-app 最新包,执行可执行文件后,即删除;
  2. npm init 方式,先全局安装 create-react-app 最新包,若已存在,则升级到最新版本,然后执行包的可执行文件,执行完即删除;
  3. npm create 方式,npm createnpm init 的别名, 用法一样;(参考 官网文档
  4. yarn create 方式,先全局安装 create-react-app 的包,若已经存在,则升级到最新版本,然后执行包的可执行文件,执行完不进行删除,会保留在全局;

对比结果

  1. npm inityarn create 利用包名规则 create-* ,先全局下载到本地再执行
  2. npx <package> 没有包名约束,临时下载执行后删除
  3. 三者效果一样,均使用最新包进行命令的执行