Yarn
Yarn 这个包管理器是在 2016 的时候由 Facebook、Google、Exponent 以及 Tilde 团队共同开发推出的。

当时 Yarn 的出现主要是为了解决 npm 在速度、安全性以及一致性方面的一些问题:
-
安装速度
-
确定性:
- 项目A ---> 直接依赖: libraryX(1.0)-----> 间接依赖:libraryY(1.3)
- 项目A ---> 直接依赖: libraryX(1.0)-----> 间接依赖:libraryY(2.0)
- Yarn 引入了一个 yarn.lock 锁文件
-
安全性
-
离线安装
指令对比:
npm | Yarn | 说明 |
---|---|---|
npm init | yarn init | 初始化项目 |
npm install/link | yarn install/link | 默认的安装依赖操作 |
npm install <package> | yarn add <package> | 安装某个依赖 |
npm uninstall <pacakge> | yarn remove <package> | 移除某个依赖 |
npm install <package> --save-dev | yarn add <pacakge> --dev | 安装开发依赖 |
npm update <package> --save | yarn upgrade <package> | 更新某个依赖 |
npm install <package> --global | yarn global add <pacakge> | 全局安装 |
npm publish/login/logout | yarn publish/login/logout | 发布/登录/登出 |
npm run <script> | yarn run <script> | 执行 script 命令 |
Yarn 的出现让 npm 团队也感受到了压力,做出了一定的改变。
例如从 npm v5 开始引入了名为 package-lock.json 的锁文件,类似于 Yarn 的 yarn.lock 文件。这确保了在不同环境中的依赖结构一致性。
思考🤔:package.json 和 package-lock.json 都是 npm 用于管理项目依赖的文件,两者有什么不同呢?
答案:package.json包描述文件,会包含直接依赖以及元数据信息,package-lock.json 包含所有的依赖信息(包含直接依赖和间接依赖)。
pnpm
在 Yarn 之后,又出现了 pnpm包管理器
pnpm 的优势主要表现在这么 3 个地方:
- 节省磁盘空间
- 解决幽灵依赖
- 原生支持Monorepo
1. 节省磁盘空间
使用 npm 时,如果你有 100 个项目都使用同一个依赖项,你会在磁盘上保存该依赖项的 100 份副本。而使用 pnpm,依赖项会存储在一个内容可寻址的存储中。

pnpm中有两个比较重要的概念:
- 硬链接
- 符号链接
硬链接
是指多个文件名指向同一个物理文件数据块。这意味着,无论你通过哪个硬链接访问文件,看到的内容都是相同的。删除一个硬链接不会影响其他硬链接,只有当所有硬链接都被删除后,文件数据才会真正从硬盘中移除。

符号链接
符号链接是一个特殊的文件,包含了指向另一个文件或目录的路径。它类似于快捷方式,访问符号链接时,操作系统会将其重定向到实际文件或目录。符号链接本身占用少量空间,但它指向的文件或目录仍然占据实际存储空间。

在 pnpm 中,直接依赖使用硬链接,而间接依赖使用符号链接。下面来做一个和 npm 安装包的对比:
两个项目 ProjectA 和 ProjectB,它们都依赖同一个库 libraryX。
传统的 npm 安装方式
ProjectA 和 ProjectB 都会在各自的 node_modules 文件夹中创建一个独立的 libraryX 目录,并且这些目录里包含了相同的文件内容。即使 libraryX 的版本完全相同,它们仍然会各自占用磁盘空间。
# 安装依赖
cd ProjectA
npm install libraryXcd ../ProjectB
npm install libraryX# 结果:
# ProjectA/node_modules/libraryX/ -> 这是一个完整的libraryX文件
# ProjectB/node_modules/libraryX/ -> 这是另一个完整的libraryX文件
这样,libraryX 的文件在磁盘上被重复存储了两次,即使它们的内容完全一样。
pnpm 使用硬链接的方式
当你使用 pnpm 安装 libraryX 时,pnpm 会将 libraryX 的文件存储在一个全局的内容地址存储(例如 ~/.pnpm-store)中,而不是在每个项目中都完整复制一份。
然后,pnpm 会为 ProjectA 和 ProjectB 中的 libraryX 创建硬链接。硬链接指向全局存储中的同一个物理文件,因此即使在 ProjectA 和 ProjectB 中都有 libraryX 的文件,这些文件在磁盘上只存储了一次。
# 使用 pnpm 安装依赖
cd ProjectA
pnpm install libraryXcd ../ProjectB
pnpm install libraryX# 结果:
# ~/.pnpm-store/libraryX/ -> 这是libraryX的实际物理文件,存储在全局内容地址存储中
# ProjectA/node_modules/libraryX/ -> 这是指向全局存储的硬链接
# ProjectB/node_modules/libraryX/ -> 这是另一个指向全局存储的硬链接
符号链接的使用
pnpm 在处理间接依赖时,会使用符号链接。
例如,假设 libraryX 本身依赖 libraryY,而 libraryY 也存储在全局内容地址存储中。此时 pnpm 会在 libraryX 中创建一个符号链接,指向全局存储中的 libraryY,而不是将 libraryY 的文件直接复制到 libraryX 中。这进一步减少了文件的重复存储。
# 符号链接示例:
# ProjectA/node_modules/libraryX/node_modules/libraryY -> 这是一个符号链接,指向全局存储中的libraryY
思考🤔:如果不同的项目依赖同一个包(libraryX)的不同版本,应该怎么处理?
答案:在全局仓库下分别存储每个版本的 libraryX. 但是这里有一个优化,仅存储不同版本之间不同的文件。
实战演练
使用 pnpm create vue@latest 命令分别创建两个 Vue 项目,查看依赖结构。
pnpm store path
2. 解决幽灵依赖
所谓幽灵依赖,是指当一个包(A)依赖于另一个包(B)时,后者会被放置在前者的 node_modules 目录中。这意味着一个包可能会意外地访问并使用另一个包的依赖,即使它没有在自己的 package.json 文件中声明这些依赖。
实战演练
演示 npm 和 pnpm 对于幽灵依赖的处理。
幽灵依赖会存在的问题:
- 难以理解的依赖关系
- 潜在的错误
3. 原生支持Monorepo
目前企业中搭建 Monorepo 项目方案,常见有这么几种:
- Lerna
- Yarn + Workspace
- pnpm + Workspace
4. 相关指令
- 安装 pnpm:可以使用 npm 或者 yarn 进行安装,npm install -g pnpm
- 创建新项目:pnpm init
- 添加依赖:pnpm add <package>
- 添加所有依赖:pnpm install
- 升级依赖:pnpm update <package>
- 删除依赖:pnpm remove <package>
包的隔离和提升
这是一张来自于 pnpm 官方给出的和其他包管理器之间的 对比图,如下:

pnpm 默认策略是包隔离,老牌的 npm 的默认策略是包提升。
- 包隔离:是指在项目中,每个依赖包都有自己独立的安装环境,这样可以避免不同依赖之间的冲突。这个概念尤其重要,当不同的依赖包需要相同的子依赖但不同版本时,如果没有良好的隔离机制,就可能导致依赖版本冲突,进而导致项目运行错误或行为异常。
- 包提升:是指将依赖关系中某些包提升到更高的目录层次,以减少冗余,节省磁盘空间。
示例:假设我们有一个项目 MyApp,该项目依赖两个包 PackageA 和 PackageB(这两个包是直接依赖),这两个包又有相同的间接依赖:
- PackageA 依赖 lodash@4.17.21
- PackageB 依赖 lodash@3.10.1
在没有包隔离的情况下,传统的包管理工具(例如 npm 早期版本)可能会尝试将 lodash 的一个版本提升到项目的 node_modules 根目录。如果 lodash@4.17.21 被安装在根目录下,那么 PackageB 依赖的 lodash@3.10.1 就会被忽略,导致 PackageB 无法正常运行。
而 pnpm 默认采用的就是包隔离策略,自然不存在上面的问题。
思考🤔:包提升本质上是为了节省磁盘空间,pnpm 采用包隔离的话磁盘空间会有浪费么?
答案:不会,因为pnpm有全局的存储空间,最终不同版本的依赖都是存储在全局空间里面,本地项目通过硬链接连接到对应版本的包。