pnpm、npm 与 Yarn Workspace 对比:为什么 Monorepo 更适合 pnpm?
pnpm、npm 与 Yarn Workspace 对比:为什么 Monorepo 更适合 pnpm?
在包含多个应用和公共包的 Monorepo 中,包管理器不仅负责“安装依赖”,还会影响依赖边界、磁盘占用、本地联调、批量执行和 CI 构建方式。
本文从实际工程角度比较 pnpm、npm 和 Yarn 的 Workspace 能力,并解释为什么 pnpm 经常成为中大型前端 Monorepo 的选择。
需要先说明:pnpm 并不是在所有方面都绝对优于 npm 或 Yarn。三者目前都具备 Workspace 能力。pnpm 的主要优势来自内容寻址存储、相对严格的依赖隔离,以及适合多包仓库的 workspace 协议和过滤执行能力。
一、Workspace 解决什么问题?
假设一个佣金系统同时包含四个应用:
1 | |
四个应用需要独立启动、构建和上线,但又要复用请求封装、API、类型、通用组件、业务组件和公共样式。
Workspace 的核心价值是:
- 在一个仓库中管理多个应用和包;
- 让应用直接引用仓库内的公共包;
- 统一安装和锁定依赖版本;
- 对多个子包批量或按范围执行命令;
- 降低跨仓库同步、临时发包和重复实现的成本。
二、传统 npm/Yarn Classic 的依赖提升
传统 npm 和 Yarn Classic 通常会尽可能将依赖提升到仓库根目录的 node_modules:
1 | |
依赖提升可以减少重复安装,也让 Node.js 的模块查找更加方便,但它可能掩盖“幽灵依赖”问题。
例如,crm-pc 没有在自己的 package.json 中声明 lodash,但其他子包安装了它,导致根目录存在 lodash:
1 | |
这种代码可能在当前仓库中正常运行,但在以下场景中失败:
- 子包被单独发布或迁移;
- 依赖安装顺序发生变化;
- 包管理器升级或提升结果变化;
- CI 和本地安装结构不一致。
现代 npm 和 Yarn 已经持续改善 Workspace 和依赖管理能力,因此不能简单地把该问题归结为“npm/Yarn 一定存在,pnpm 一定不存在”。它更多反映不同包管理器默认依赖布局和约束强度的差异。
三、pnpm 如何存储和链接依赖?
pnpm 使用内容寻址存储保存依赖。相同内容通常只需要在磁盘中保存一份,多个项目再通过硬链接或符号链接复用。
概念上可以理解为:
1 | |
项目仍然具有 node_modules,但其中的包会通过链接关联到 .pnpm 结构及内容存储,而不是简单地为每个项目复制完整依赖。
这使 pnpm 在多个应用使用相同版本依赖时,通常具有更好的磁盘利用率和重复安装效率。
四、pnpm Workspace 的主要优势
1. 更高的磁盘利用率和安装效率
在 Monorepo 中,多个应用经常使用相同版本的 React、TypeScript、Axios 等依赖。pnpm 的内容寻址存储能够减少重复下载和文件复制。
项目越多、依赖重合度越高,这种存储方式越有价值。
不过,不应把结论表述为“pnpm 永远比 npm 和 Yarn 快”。安装速度还会受到以下因素影响:
- 是否命中本地缓存;
- 网络和 Registry 状态;
- 生命周期脚本数量;
- 锁文件是否发生变化;
- 文件系统和 CI 缓存策略。
更准确的说法是:
在依赖重合度较高的多应用 Monorepo 中,pnpm 的内容寻址存储通常能够减少重复下载和文件复制,改善安装效率和磁盘利用率。
2. 相对严格的依赖隔离
pnpm 的 node_modules 结构更容易限制子包访问没有明确声明的依赖。
例如:
1 | |
如果 crm-pc 使用了 lodash,就应该在自身 package.json 中声明,而不能依赖其他子包“顺便”安装的版本。
更明确的依赖边界可以带来:
- 子包独立构建时更加可靠;
- 依赖关系更容易追踪;
- 减少本地正常、CI 或发布后失败的问题;
- 提高公共包迁移和复用的可靠性。
需要注意,Yarn Berry 的 Plug’n’Play 也可以提供严格的依赖约束,甚至比传统 node_modules 更严格。因此,合理表述是:
pnpm 在保留
node_modules使用方式的同时,提供了相对严格的依赖隔离。
3. 使用 workspace: 协议明确引用本地包
应用可以通过 workspace: 协议引用仓库内的公共包:
1 | |
workspace:* 明确表达:该依赖必须解析为当前 Workspace 内的本地包,而不是从 Registry 下载同名版本。
它可以:
- 避免意外引用远程同名包;
- 修改共享包后直接在应用中联调;
- 清晰展示本地包之间的依赖关系;
- 减少跨包联调时发布临时版本的成本;
- 在发布阶段按照规则转换为正式版本范围。
npm 和 Yarn 现在同样具有 Workspace 和本地包链接能力。pnpm 的优势不是“独家支持”,而是 workspace: 协议与其多包依赖管理体验结合得较好。
4. 适合多包仓库的过滤执行
pnpm 可以递归执行 Workspace 命令:
1 | |
也可以只对某个应用执行:
1 | |
过滤能力适合以下场景:
- 四个应用独立启动和构建;
- 本地只启动当前开发的应用;
- CI 按目标应用执行任务;
- 缩小测试、Lint 和排错范围;
- 对目标包及其依赖执行命令。
但 pnpm 的过滤执行不等于完整的“变更影响分析”。如果需要根据 Git 变更自动计算复杂的受影响项目范围,通常还需要 Turborepo、Nx 或自研工具配合。
5. 统一锁文件和依赖治理
pnpm Workspace 通常使用统一的 pnpm-lock.yaml,可以带来:
- 所有应用使用一致的依赖解析结果;
- CI 安装具有更好的确定性;
- 公共依赖升级更容易统一评估;
- Code Review 可以集中查看依赖变化;
- 减少多个仓库分别维护锁文件的成本。
统一锁文件也有代价:
- 锁文件可能较大;
- 单个包升级依赖也可能修改统一锁文件;
- 多分支同时升级依赖时可能产生冲突。
这是统一治理收益与变更耦合成本之间的取舍。
五、pnpm 与 npm Workspace 对比
现代 npm 已支持 Workspace,因此 npm 完全可以管理普通的多包项目。
| 对比项 | pnpm | npm Workspace |
|---|---|---|
| 依赖存储 | 内容寻址存储,通过链接复用 | 传统 node_modules 安装与提升为主 |
| 磁盘利用率 | 依赖高度重合时通常更优 | 简单项目中足够,可能存在更多重复文件 |
| 依赖隔离 | 默认结构相对严格 | 更偏向传统提升模型 |
| 本地包引用 | workspace: 协议表达清晰 |
支持 Workspace 和本地链接 |
| 多包过滤执行 | --filter 和递归命令较成熟 |
具备 Workspace 命令能力,复杂过滤体验有所不同 |
| 使用成本 | 需要团队安装和统一 pnpm 版本 | Node.js 默认附带,上手成本低 |
npm 的主要优势包括:
- Node.js 默认附带;
- 团队学习和安装成本低;
- 普通项目配置简单;
- Workspace 能力已经可以满足许多中小项目。
因此,选择 pnpm 的理由不应是“npm 不能做 Monorepo”,而应该是:
pnpm 在多应用、共享包、依赖复用、依赖边界和过滤执行方面更符合当前项目需求。
六、pnpm 与 Yarn Workspace 对比
讨论 Yarn 时,需要区分 Yarn Classic 和 Yarn Berry。
Yarn Classic
Yarn Classic 主要采用传统 node_modules 和依赖提升模式。相较于 Yarn Classic,pnpm 通常具有:
- 更高效的内容寻址存储;
- 相对更严格的依赖边界;
- 较清晰的 Workspace 协议;
- 更适合多包范围执行的过滤能力。
Yarn Berry
Yarn Berry 的能力同样很强,包括:
- Workspace;
- Plug’n’Play;
- Constraints;
- 插件体系;
- 面向大型 Monorepo 的依赖治理能力。
Plug’n’Play 不依赖传统 node_modules,可以提供非常严格的依赖管理和快速解析。但某些依赖 Node.js 传统模块结构的工具可能需要额外适配。
pnpm 则在保留 node_modules 使用习惯的同时,利用链接和内容寻址存储改善依赖管理。因此,对依赖传统 Node.js 工具链的团队来说,迁移和理解成本通常更可控。
| 对比项 | pnpm | Yarn Classic | Yarn Berry/PnP |
|---|---|---|---|
| 依赖结构 | 链接化 node_modules |
传统提升式 node_modules |
可使用 PnP,不依赖传统 node_modules |
| 依赖边界 | 相对严格 | 较容易受提升影响 | 非常严格 |
| 工具兼容方式 | 接近传统 Node.js 生态 | 传统工具兼容性好 | 部分工具可能需要 PnP 适配 |
| 存储效率 | 内容寻址存储 | 传统缓存与安装 | 具有自身缓存和零安装等能力 |
| 多包治理 | Workspace、filter | Workspace | Workspace、Constraints、插件体系 |
不能简单得出“pnpm 全面优于 Yarn Berry”的结论。对于需要高级约束、零安装或已深度使用 Yarn 插件体系的团队,Yarn Berry 可能更合适。
七、React Native 项目的注意事项
React Native 的 Metro 历史上对 Monorepo、符号链接和模块解析有额外要求。
使用 pnpm 时,React Native 项目可能需要关注:
- Metro 是否正确监听 Workspace 中的共享包;
- 是否配置
watchFolders; - React 和 React Native 是否出现重复实例;
- Metro 是否能够解析 pnpm 的链接化依赖结构;
.ios.tsx、.android.tsx等平台文件的解析;- 公共包是否需要预编译;
- Native 依赖是否允许被提升或需要特殊配置。
因此,不应笼统声称“pnpm 对 React Native 的兼容性一定优于 Yarn”。更准确的结论是:
pnpm 适合多包依赖治理,但 React Native 子包仍需要结合 Metro 和 Native 依赖进行实际验证与配置。
八、不同技术栈为什么难以统一类型检查?
一个 Monorepo 可能同时包含:
- JavaScript;
- TypeScript;
- React;
- React Native。
这些子包的检查条件不同。
JavaScript 子包
JavaScript 包可能完全不执行 TypeScript 检查,也可能使用:
1 | |
普通 TypeScript 子包
普通 TS 工具包主要关注:
- TypeScript 类型;
- 模块解析;
- 声明文件;
- 编译目标;
- 是否生成 declaration。
它不一定需要 JSX 或浏览器 DOM 类型。
React 子包
React Web 应用通常需要处理:
.tsx;- JSX 编译模式;
- React 类型;
- DOM 类型;
- Vite 或其他 Web 构建环境声明。
1 | |
React Native 子包
React Native 同样使用 React 和 JSX,但它不是浏览器环境:
- 不应默认依赖完整 DOM;
- 使用 React Native 平台类型;
- 可能包含
.ios.tsx和.android.tsx; - Metro 的模块解析与 Web 构建工具不同;
- Native 模块可能带有额外类型约束。
如果在根目录使用一份统一的 tsconfig 检查全部子包,可能出现:
- 为了兼容所有技术栈而放宽配置,导致检查不准确;
- 强制使用同一配置,产生大量与当前运行环境无关的报错;
- 每次检查都扫描整个仓库,随着规模增长造成性能问题。
更合理的方式是:
1 | |
pnpm Workspace 解决的是多包依赖管理和本地协作问题,不会自动解决不同技术栈的类型检查问题。后者需要结合 TypeScript Compiler API、项目引用、构建工具或独立检查工具处理。
九、项目选型时可以如何表达?
下面是一段相对完整且不过度夸大的项目表述:
我们比较了 npm、Yarn 和 pnpm,最终选择 pnpm workspace。项目中包含四个应用和多个共享包,存在大量相同依赖,pnpm 的内容寻址存储能够减少重复下载和磁盘占用;相对严格的依赖隔离也能帮助我们发现子包使用了但没有在自身
package.json中声明的幽灵依赖,使应用独立构建时更加可靠。同时,我们通过workspace:协议引用仓库内的共享包,让公共能力修改后可以直接在多个应用中联调,并通过 filter 能力按应用执行启动和构建。综合依赖治理、安装效率和多包开发体验,pnpm 更符合这个项目的需求。
如果项目实际没有使用某项能力,例如 --filter、严格依赖治理或 React Native 的 Metro 配置,就不应把它描述为已经落地的成果。技术选型的可信度来自真实场景和实际取舍,而不是罗列工具的全部特性。
十、总结
pnpm 适合 Monorepo 的核心原因可以概括为:
- 通过内容寻址存储减少重复依赖;
- 通过相对严格的依赖结构改善依赖边界;
- 通过
workspace:协议支持本地公共包联调; - 通过递归和 filter 能力按范围执行任务;
- 在保留
node_modules使用方式的同时,提供较成熟的多包管理体验。
但包管理器选型仍应结合以下因素:
- 团队已有工具链;
- Node.js 和包管理器版本;
- CI 缓存方案;
- React Native、Metro 等特殊工具;
- 是否需要 Yarn Berry 的 PnP、Constraints 或插件体系;
- 仓库规模和依赖重合度。
最合理的选择不是“功能最多的包管理器”,而是最符合团队工程约束、并且能够长期稳定维护的方案。