Skip to content

npm / yarn / pnpm / cnpm 区别

包管理器的选择看起来只是换一个 CLI 命令,但它们的本质差异在 node_modules 的目录拓扑结构——不同策略直接影响依赖解析的正确性、磁盘占用、安装速度,以及 Monorepo 场景下的可用性。


历史动机

npm 是最早的 Node.js 包管理器,随 Node 一起发布。yarn(2016)、pnpm(2017)、cnpm 的出现各有原因:

  • yarn:npm v3/v4 时代,安装速度慢、lockfile 不可靠、离线模式缺失。Facebook 需要一个确定性的、更快的替代品。
  • pnpm:解决 npm/yarn 扁平化 node_modules 带来的幽灵依赖(phantom dependencies)和磁盘冗余问题。核心理念是"用硬链接和符号链接模拟严格的依赖拓扑"。
  • cnpm:国内网络环境下 npm install 经常失败,阿里巴巴维护的镜像工具,解决的是网络可达性问题而非依赖解析策略问题。

node_modules 拓扑:核心差异

同一个项目 package.json,三种包管理器安装后的 node_modules 结构完全不同。

npm / yarn classic:扁平提升(Hoisting)

text
node_modules/
├── A@1.0.0          ← 被提升到顶层
├── B@2.0.0          ← 被提升到顶层
├── C@1.0.0          ← 被提升到顶层
└── A/               ← A 的依赖
    └── node_modules/
        └── D@1.0.0  ← 只有和顶层冲突的才嵌套在这里

策略:将所有依赖尽量提升到 node_modules 顶层,版本冲突时才嵌套。

这导致一个问题——幽灵依赖。你的代码可以直接 require('C'),即使 package.json 里没有声明 C。它只是因为被 A 依赖而被提升到了顶层。开发环境能跑,但换个包管理器(或 pnpm)就挂。

text
node_modules/
├── .pnpm/                      ← 所有包的真实存储位置(硬链接到全局 store)
│   ├── A@1.0.0/node_modules/   ← A 只能看到它声明的依赖
│   │   ├── D@1.0.0 → ../../D@1.0.0
│   │   └── B@2.0.0 → ../../B@2.0.0
│   ├── B@2.0.0/
│   ├── C@1.0.0/
│   └── D@1.0.0/
├── A → .pnpm/A@1.0.0/node_modules/A   ← 只有 package.json 中声明的直接依赖
└── B → .pnpm/B@2.0.0/node_modules/B

关键设计:

  • .pnpm/ 下每个包有自己的 node_modules,只包含它声明过的依赖
  • 顶层 node_modules/ 只有 package.json 中列出的依赖
  • 所有真实文件通过硬链接指向全局 store(~/.pnpm-store/

你的代码 require('C') 会直接报错——C 不在你的 package.json 里,顶层就不可见。这是对幽灵依赖的彻底消除。

yarn v2/v3(Berry):Plug'n'Play

yarn 从 v2 开始引入了 PnP 模式,完全抛弃 node_modules

text
项目根目录/
├── .pnp.cjs          ← 依赖解析表(映射包名到磁盘路径)
└── .yarn/
    └── cache/        ← 包文件的压缩缓存(zip 格式)

Node 通过 require('./pnp.cjs') 拦截模块解析,不再遍历 node_modules 目录。安装几乎瞬间完成(无需 I/O 密集型文件写入),但代价是需要 Node 补丁或兼容层,部分工具链不兼容。


维度对比

安装速度

场景npmyarn (classic)pnpmyarn (PnP)
冷安装(无 cache)
热安装(有 cache)快(硬链接免复制)极快(免 I/O)
CI/CD(有 lockfile)极快

pnpm 的热安装快,因为它只做链接创建而不复制文件。yarn PnP 极快,因为它根本不需要生成 node_modules

磁盘占用

  • npm / yarn:每个项目的 node_modules 是独立的完整副本。10 个项目用同一个版本的 React → 磁盘上有 10 份。
  • pnpm:所有包存在全局 store(~/.pnpm-store/),项目的 node_modules 里是硬链接。10 个项目共用同一份 React 文件。对 Monorepo 和多项目开发者来说磁盘节省非常显著。
  • yarn PnP:包以 zip 压缩格式存储在 .yarn/cache/ 中,也会跨项目共享。

幽灵依赖(Phantom Dependencies)

这是 pnpm 的核心卖点:

text
npm/yarn 的问题:
  package.json: { dependencies: { "express": "^4" } }
  代码中: const dayjs = require('dayjs')  ← dayjs 不在 package.json 里!
  结果: npm/yarn 下能跑(express 依赖了 dayjs,被提升到了顶层)
        pnpm 下直接报错(dayjs 在 .pnpm/express@4/node_modules/ 里,对你不可见)

幽灵依赖让项目隐式地依赖了依赖的依赖。有一天 express 不再依赖 dayjs,你的代码就坏了——但你的 package.json 从没声明过它,根本意识不到这个风险。

pnpm 的严格模式强制你显式声明所有直接依赖,这在工程上是更健康的做法。

Lockfile

包管理器Lockfile特点
npmpackage-lock.jsonv5 后稳定,格式冗长
yarn classicyarn.lock类 YAML,可读性好
pnpmpnpm-lock.yaml类 YAML,记录依赖拓扑
cnpm无 lockfile(早期)/ 兼容 npm lockfile镜像工具,不改变解析逻辑

Lockfile 的核心价值是确定性安装——CI 和生产环境安装的依赖版本和开发者本地完全一致。cnpm 早期版本不支持 lockfile,这导致开发环境和生产环境版本可能不一致,是一个工程上的硬伤。新版本已修复。

Monorepo 支持

  • npm workspaces(v7+):基本可用,功能偏少,缺少高级 filtering 和并行执行能力。
  • yarn workspaces:最早的 workspace 方案。配合 yarn workspaces foreach 可以实现按依赖拓扑顺序执行命令。
  • pnpm workspaces:当前工程实践的主流选择。pnpm --filter 的语法灵活(pnpm --filter "./packages/*" --filter "!packages/docs" run build),配合 pnpm deploy 可以做生产部署的精简拷贝。
  • cnpm:不支持 workspace。

Monorepo 场景下 pnpm 的优势最明显:硬链接机制让 packages/ 下的几十个包共享依赖,避免大量重复安装。pnpm publish --filterpnpm deploy 提供了生产级的 mono-package 发布能力。

安全性

  • npmnpm audit 内置漏洞扫描。npm v7+ 支持 --install-strategy=nested(但几乎没人用)。
  • yarnyarn audit,规则和 npm audit 同源。yarn Berry 支持 yarn npm audit
  • pnpmpnpm audit。严格依赖拓扑本身就降低了供应链攻击面——你在 package.json 里没声明的东西,代码里就用不了。
  • cnpm:无内置安全审计。

cnpm 的定位

cnpm 和前面三者在不同维度上竞争——不是依赖解析策略的差异,而是网络层的问题。

text
cnpm = npm 客户端 + 淘宝镜像源(registry.npmmirror.com)

核心功能:

  • 同步:每 10 分钟从 npm 官方 registry 同步,国内下载速度大幅提升
  • 命令行cnpm install 本质上还是 npm 的依赖解析逻辑,换个 registry 而已
  • npmmirror.com:淘宝 NPM 镜像站,提供 Web 界面和 API

2020 年后,npm 官方 registry 在国内的访问稳定性已有改善,加上 nrm(npm registry manager)可以快速切换镜像源,cnpm 的独立 CLI 价值降低。现在更常见的做法是直接用 npm/pnpm 配淘宝镜像:

bash
npm config set registry https://registry.npmmirror.com
# 或
pnpm config set registry https://registry.npmmirror.com

选型建议

个人项目 / 小团队

pnpm。安装快、磁盘省、避免幽灵依赖。CLI 和 npm 高度兼容(pnpm installpnpm run 等),迁移成本几乎为零。

Monorepo

pnpmpnpm workspaces + --filter + 硬链接共享依赖,当前 Monorepo 工具链的最优解。Turborepo、Nx 等工具默认推荐 pnpm。

兼容性优先的项目

npm。作为 Node 内置工具,兼容性最好。CI/CD 环境无需额外安装。如果项目依赖了一些老旧的或不兼容 pnpm 严格模式的包,npm 的灵活性反而成了优势。

遗留项目(yarn classic)

如果你的项目已有 yarn.lock 且团队习惯 yarn,没必要迁移。yarn classic 在确定性安装和速度上的改进已经解决了当初它要解决的问题。但如果是新项目,yarn classic 的安装速度优势已被 pnpm 追平甚至超越。

国内网络问题

不需要装 cnpm CLI。直接用 npm 或 pnpm 配置淘宝镜像源即可。cnpm 的价值已经从"必备工具"退化为"镜像服务的另一种访问方式"。


总结

包管理器之争归根到底是 node_modules 拓扑策略之争:

  • npm / yarn classic 选择了扁平提升——换来便利,留下幽灵依赖的隐患
  • pnpm 选择了符号链接模拟严格拓扑——牺牲了一点兼容性,换来正确的依赖隔离和磁盘效率
  • cnpm 解决的是网络问题而非解析问题——镜像方案在当前环境下已不再强依赖独立 CLI
  • yarn Berry(PnP)选择了彻底抛弃 node_modules——最大胆的方案,生态兼容性仍需时间验证

如果今天开一个新项目且没有特殊约束,默认选 pnpm。不是因为它是"最好的",而是因为它在你还没意识到问题的时候,就已经阻止了问题发生。