引言
作为前端开发程序员,框架千千万,但我们永远都要与包管理工具打交道,他是我们维护项目依赖包的核心工具。
没了它,我们需要自己去官网或 github 找对应的包去下载压缩包,再解压,再引入到我们项目目录下,对于一个中型项目而言此操作要重复 200+ 次,如同回到石器时代,依赖包的版本也将变得难以管理,更何况在当下单仓多包 monorepo 架构盛行的时代,对我们前端这无疑是一场灾难。
因此,掌握并理解包管理工具显得尤为重要,在我们接触前端项目中,最常见的三位兄弟npm、yarn、pnpm想必都很熟悉,但它们的核心发展历程是怎样的呢?三者有什么区别?什么情况下应当用哪一种呢?下面让我们来谈谈吧。
npm
作为我们前端入门路上最常见的包管理工具,npm是随着node官方一起出现的,在我们环境中,安装了node就会自动安装npm,不需要额外安装。
npm v1-v2 嵌套式结构
早期npm v1-v2的包管理,也就是node_modules以嵌套式结构为主,及依赖层层嵌套,如下:
node_modules/
├── package-a/
│ └── node_modules/
│ ├── lodash@4.17.0
│ └── package-b/
│ └── node_modules/
│ └── lodash@4.16.0
└── package-c/
└── node_modules/
└── lodash@4.17.0
这样造成的问题也很明显:
- 重复依赖导致
node_modules巨大 - 路径很长(Windows 路径长度限制)
- 内存浪费
- 安装缓慢
npm v3 扁平化结构
到了v3版本,官方采用扁平化的结构来管理node_modules,就是将每个包的依赖在首次安装时提升到父目录,这样再遇到相同的依赖向上查找已有版本的依赖是否可以满足,若满足即可共用,若不满足重新单独安装在所属子目录下,示例如下:
# 第一步:先安装 debug@^4.0.0
node_modules/
└── debug@4.3.4/ # 被提升到顶层
# 第二步:安装 my-module(依赖 debug@^3.0.0)
检查:现有 debug@4.3.4 是否满足 ^3.0.0?
答案:不满足!4.3.4 不在 ^3.0.0 范围内
结果:重新安装
node_modules/
├── debug@4.3.4/ # 原来提升的
├── my-module/
│ └── node_modules/
│ └── debug@3.2.7/ # 单独安装!✓
这样的既能减少资源的浪费,也能实现依赖的共享。
但是这种方式仍有问题:
- 幻影依赖:由于包的依赖被提升至父目录
node_modules下,也就意味着某些未在package.json中声明的依赖也可直接被引用 - 不确定性:比如这个版本中包的依赖的提升,取决于包安装的先后顺序,意味着不同的安装顺序会有不同的
node_modules的目录结构,除此之外还有包的安装源等也会造成安装的不确定性,也称之为依赖不幂等
npm v5 lock机制
到了v5版本,为了解决不确定性,也就是依赖不幂等的问题,官方效仿yarn,采用package-lock.json来对安装依赖进行严格的限制,从而让怎么安装依赖变得有理可依
package-lock.json中核心几个字段:
version:包的唯一版本号resolved:依赖包的安装源integrity:用于验证包是否失效的完整性hash值,由两部分组成: 加密hash函数-摘要dgest,加密函数有两种sha512或者sha1,dgest等于base64(hashfn(content))dev:是否为开发时依赖项requires:当前包的dependencies依赖项dependencies:当前包的node_modules依赖树(比如:某个子依赖包存在多版本时,当前包下生成的node_modules结构)
采用lock文件锁定对依赖的安装方式,解决了v3版本不确定性的问题,但是幻影依赖这个问题仍然存在。
yarn
yarn是在 2016 年由 FaceBook 团队开发的包管理工具,旨在解决当时npm速度慢、不确定性、安全性的问题
yarn v1 独有的lock机制
2016年,yarn 以独特的lock机制横空出世,解决了当时npm v3依赖不确定性的问题(后续npm v5直接照抄),下面对比npm的方式来谈
核心特性:
lock机制:不同于npm v5的package-lock.json,yarn采用的是自己的yarn.lock文件,以自己独有的规范锁定依赖,但本质是一样的- 离线缓存机制:当时的
npm在离线模式下无法npm install,而yarn的策略是优先使用缓存,所以在缓存中有依赖的情况下,离线模式也可以yarn install - 网络性能更好:简单来说就是速度快,
npm采用串行机制,而yarn采用并行下载的策略,同时也有命中缓存的机制,使得yarn在下载速度优于npm
yarn v1.x Workspaces支持
引入Workspaces支持了monorepo架构,实现了多包共享依赖,并且能进行统一版本管理
// package.json
{
"private": true,
"workspaces": ["packages/*", "apps/*"]
}
yarn v2 完全重写
2020 年,yarn架构完全重写,由之前类似npm的架构重写为自己的架构,代号Berry,支持丰富的插件架构,这也为往后yarn不断的性能及功能升级奠定了基础
pnpm
pnpm(performant npm)一个现代化的 JavaScript 包管理器,它的核心目标是解决 npm 和 Yarn 的磁盘空间和性能问题
下面是它的核心特性:
硬链接
pnpm管理下的项目,会依据package.json中的依赖项配置进行,如果是首次安装的包,会装到全局.pnpm目录下统一管理,后续遇到相同符合版本要求的依赖,直接硬链接到全局存储下的依赖包,从而极大的减少了依赖的重复使用
符号链接
主要是指对于依赖包与虚拟存储的一一映射关系
这里引入虚拟存储的概念,实质上可以理解为硬链接到全局存储的标识,也就是说项目在使用依赖包时,从当前项目的node_modules中寻找,找到了这个依赖包会符号链接到这里的虚拟存储,再由此虚拟存储硬链接到全局存储对应的真正依赖包
这是pnpm包管理项目寻找依赖的模式:
# 用户代码中的 require('react')
# 映射关系:
应用层 require('react')
↓
node_modules/react (符号链接)
↓
.pnpm/react@18.2.0/node_modules/react (硬链接到全局存储)
↓
全局存储中的实际文件
至于为什么需要这个符号链接呢?
这是为了解决幻影依赖的问题。
我们这样来想,如果没有符号链接,项目node_modules目录下都是虚拟存储,也就是直接硬链接到全局存储的依赖,如果是这样,应用层使用一个依赖包只要全局存储下有,无论是我们项目中package.json声明的依赖包还是其子包,都可以直接访问使用,这便是幻影依赖。
通过使用符号链接,它将作为应用层访问真正依赖包的入口,那么对于未在package.json中声明的依赖将不会提供符号链接,也就是没有这个入口,那么这时便会报错认为无此依赖,从而解决了幻影依赖的问题。
支持monorepo
pnpm通过设置workspace工作空间的机制来支持monorepo架构
pnpm管理的项目,会要求有一个pnpm-workspace.yaml文件,通过这个文件来声明那些目录(项目/包)可以作为独立的工作空间,也就是理解为一个子模块或子包
packages:
- "packages/*"
小结
好了,到此这三位兄弟的核心特性已经做了初步的介绍,从宏观的视角来看似乎pnpm的优势确实巨大,因为归根结底包管理工具就是工具,为什么称之为工具?是因为它是服务于程序员的,所以评判一个工具优劣的核心标准便是我们的使用体验,当然了,这也受限于不同的团队的不同使用场景。
下面是一个总结的一个使用判断树,大家可以看一乐:
开始选择包管理器
↓
是否个人/学习项目?
├── 是 → 用 npm(最方便)
↓
否,是公司/团队项目
↓
现有项目用什么?
├── 有用 → 保持统一
↓
无现有项目
↓
项目需求:
1. 需要 Monorepo?
├── 是 → Yarn (Workspaces 最成熟)
2. 磁盘空间紧张?
├── 是 → pnpm (节省90%空间)
3. 安装速度要求高?
├── 是 → pnpm (最快)
4. 需要最佳兼容性?
├── 是 → npm (最兼容)
5. 需要插件生态?
├── 是 → Yarn (插件系统丰富)
↓
都有需求? → 按优先级选择
感谢你的阅读!