本页介绍 pnpm 通过 Content-Addressable Store 和硬链接实现磁盘共享的机制,解释严格 node_modules 结构对幽灵依赖的消除,以及在 React Native / Expo 项目中必须设置 node-linker=hoisted 的原因。
概述
pnpm 的核心优势在于磁盘效率和依赖隔离,但 React Native / Expo 生态对 node_modules 结构有现实要求,因此实践中通常不能直接使用最严格的默认模式。对 RN 项目来说,最重要的结论是:继续使用 pnpm,但在 .npmrc 中显式设置 node-linker=hoisted,用兼容性换取工程可运行性。
Expo 项目中的标准配置
# .npmrc
node-linker=hoisted
这一行是目前 Expo 官方文档推荐的 pnpm 兼容配置。加上这一行之后,pnpm install 的行为对 RN 生态来说更接近 npm,但你依然保留 CAS Store 和硬链接带来的空间与速度优势。
Content-Addressable Store:pnpm 的核心差异点
pnpm 的根本设计与 npm/yarn 1 不同,不是在每个项目里复制文件,而是维护一个全局 Content-Addressable Store,默认位置是 ~/.pnpm-store/。
“Content-Addressable”的意思是:文件按其内容的哈希值命名和存储。lodash@4.17.21 的某个文件不管被多少个项目用到,在磁盘上只存一份。项目的 node_modules 里通过**硬链接(hard link)**指向这份文件——不是复制,是同一个 inode。
这带来的直接好处:
- 磁盘空间:10 个项目依赖同一版本的 React,只占一份 React 的空间。
- 安装速度:如果 Store 里已有该文件,安装几乎是瞬间完成的硬链接操作。
- 幂等性:文件内容被哈希锁定,不会出现”同名文件内容被覆盖”的情况。
严格 node_modules 结构:告别幽灵依赖
npm 和 yarn 1 默认把所有依赖**扁平化(hoist)**到 node_modules/ 根目录。这看起来节省空间,但带来了一个隐蔽的问题:幽灵依赖(Phantom Dependency)。
举例:你的 package.json 没有声明 lodash,但你的某个直接依赖用了它,扁平化后 lodash 会出现在根 node_modules/ 里。结果:你的代码可以 import 'lodash' 并且跑通——直到某天那个直接依赖升级了,悄悄改了 lodash 版本,你的代码开始出 bug,而你在 package.json 里根本找不到 lodash。
pnpm 的严格模式解决了这个问题:根 node_modules/ 只放直接依赖,其余依赖在 .pnpm/ 子目录下通过 symlink 组织。你没有声明的包,代码 import 时会直接报错——这是 pnpm 的设计哲学:让错误早点暴露。
React Native 生态的兼容性问题
pnpm 严格模式在 React Native + Expo 项目里会撞墙。
根因:RN 生态的一些包(比如 @expo/metro-runtime、部分 Metro 插件、react-native 内部模块)依赖未在 package.json 里声明的内部路径——它们假设 react-native 一定在根 node_modules/ 下可以直接访问。在 npm/yarn 1 的扁平结构里这没问题,但 pnpm 严格模式下这些路径解析会直接失败。
典型报错模式:Metro bundler 启动时报找不到某个模块,或者 require 某个 RN 内部路径时抛出 MODULE_NOT_FOUND。
解决方案:在项目根目录的 .npmrc 中设置:
node-linker=hoisted
这让 pnpm 在安装时使用扁平化的 node_modules 结构(和 npm 行为一致),放弃了严格依赖隔离,但保留了 CAS Store 和硬链接带来的磁盘空间和安装速度优势。这是在 RN 生态现实约束下的务实选择,不是 pnpm 的”降级”——你依然在用 pnpm,只是换了链接模式。
包管理器横向对比
| 特性 | npm | yarn 1 | yarn 4 (PnP) | pnpm | bun |
|---|---|---|---|---|---|
| node_modules 结构 | 扁平化 | 扁平化 | 无 node_modules | 严格 symlink | 扁平化 |
| 全局 Store | 无 | 无 | 无 | 有(CAS) | 有 |
| 幽灵依赖 | 有 | 有 | 无 | 无(默认) | 有 |
| RN/Expo 兼容 | 原生支持 | 原生支持 | 需要额外配置 | 需 node-linker=hoisted | 基本支持 |
| 安装速度 | 慢 | 中 | 快 | 快 | 最快 |
| 磁盘效率 | 差 | 差 | 好 | 好 | 好 |
| 成熟度 | 高 | 高 | 中 | 高 | 低(2024) |
yarn 4 PnP(Plug’n’Play):更激进的方案,完全不生成 node_modules,改用 .yarn/cache 里的 zip 文件 + 运行时 resolver。理论上最严格、最快,但 RN 生态几乎不支持——Metro 无法处理 PnP 的模块解析方式。
bun:速度最快,但 2024 年对 RN/Expo 的兼容性还不稳定,生产项目谨慎使用。
为什么不直接用 npm? npm 没有 CAS,每次安装都在项目目录里完整复制文件。在有多个 RN 项目的机器上,磁盘消耗会很显著。pnpm +
node-linker=hoisted是”兼容性”和”效率”之间的平衡点。
参见
- Corepack PM 版本管理
参考
版本说明
- 本页基于 2026-04-13 调研结果整理,适用范围以当前工具版本为准。