浅谈前端包管理工具 - npm、yarn、pnpm

29 阅读7分钟

引言

作为前端开发程序员,框架千千万,但我们永远都要与包管理工具打交道,他是我们维护项目依赖包的核心工具。

没了它,我们需要自己去官网或 github 找对应的包去下载压缩包,再解压,再引入到我们项目目录下,对于一个中型项目而言此操作要重复 200+ 次,如同回到石器时代,依赖包的版本也将变得难以管理,更何况在当下单仓多包 monorepo 架构盛行的时代,对我们前端这无疑是一场灾难。

因此,掌握并理解包管理工具显得尤为重要,在我们接触前端项目中,最常见的三位兄弟npmyarnpnpm想必都很熟悉,但它们的核心发展历程是怎样的呢?三者有什么区别?什么情况下应当用哪一种呢?下面让我们来谈谈吧。

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 v5package-lock.jsonyarn采用的是自己的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 (插件系统丰富)
    ↓
都有需求? → 按优先级选择

感谢你的阅读!