一、背景

最近公司私服开启鉴权,对于前端来说,带来一些非常重要的影响和变化:

  1. 不管拉取 npm 包还是发布 npm 包,均需要鉴权,具体就是 .npmrc 配置信息需要增加 auth token 信息;
  2. 使用 yarn 出现偶然下载包失败问题以及必现的无法发包问题,而 npm 和 pnpm 可以;
  3. 现有组件库工程(所使用的工具链为 yarn + lerna)发包时会出现 401 鉴权问题,甚至有时连安装依赖包也有问题。

基于以上三个问题,我们做了一些尝试:

  1. 将 yarn 从 1.x 升级到 2.x,也就是说 yarn 2.x + lerna,不行;❌
  2. 将 yarn 改成 pnpm,使用 pnpm + lerna 组合,也出现了一些问题;❌
  3. 将 yarn + lerna 组合改成 pnpm + workspace + [pnpm publish],成功。✅

最后,我们决定彻底地将 yarn + lerna 组合切换成 pnpm + workspace + changesets 。

二、什么是 pnpm ?

pnpm 是新一代的包管理工具,是目前较为先进的包管理器。按照官网说法,可以实现节约磁盘空间并提升安装速度和创建非扁平化的 node_modules 文件夹两大目标,具体原理可以参考 pnpm 官网。

以下是官方给的一张关于 pnpm 的原理图:

pnpm 原理图

所以,综合上图,我就现在知道 pnpm 为什么说是先进的包管理器了吧。

首先,最大的优点是节省磁盘空间,一个包全局只保存一份,剩下的都是软硬连接,这必然节省不少磁盘空间,并且使用软链接的方式创建非扁平的 node_modules 嵌套关系。

其次就是快,因为通过链接的方式而不是复制,自然会快。

三、pnpm 对比 npm/yarn 有什么优势?

  1. 速度快

pnpm 安装包的速度究竟有多快?先以 React 包为例来对比一下:

pnpm 原理图

可以看到,作为黄色部分的 pnpm,在绝多大数场景下,包安装的速度都是明显优于 npm/yarn,速度会比 npm/yarn 快 2-3 倍。

对 yarn 比较熟悉的同学可能会说,yarn 不是有 PnP 安装模式吗?直接去掉 node_modules,将依赖包内容写在磁盘,节省了 node 文件 I/O 的开销,这样也能提升安装速度。

接下来,我们以这样一个仓库为例,我们来看一看 benchmark 数据,主要对比一下 pnpm 和 yarn PnP:

pnpm 原理图

从中可以看到,总体而言,pnpm 的包安装速度还是明显优于 yarn PnP 的。

  1. 高效利用磁盘空间

pnpm 内部使用基于内容寻址的文件系统来存储磁盘上所有的文件,这个文件系统出色的地方在于:

  • 不会重复安装同一个包。用 npm/yarn 的时候,如果 100 个项目都依赖 lodash,那么 lodash 很可能就被安装了 100 次,磁盘中就有 100 个地方写入了这部分代码。但在使用 pnpm 只会安装一次,磁盘中只有一个地方写入,后面再次使用都会直接使用 hardlink(硬链接)。
  • 即使一个包的不同版本,pnpm 也会极大程度地复用之前版本的代码。举个例子,比如 lodash 有 100 个文件,更新版本之后多了一个文件,那么磁盘当中并不会重新写入 101 个文件,而是保留原来的 100 个文件的 hardlink,仅仅写入那一个新增的文件。
  1. 支持 monorepo

随着前端工程的日益复杂,越来越多的项目开始使用 monorepo。之前对于多个项目的管理,我们一般都是使用多个 git 仓库,但 monorepo 的宗旨就是用一个 git 仓库来管理多个子项目,所有的子项目都存放在根目录的 packages 目录下,那么一个子项目就代表一个 package。

pnpm 与 npm/yarn 另外一个很大的不同就是支持了 monorepo,体现在各个子命令的功能上,比如在根目录下 pnpm add A -r, 那么所有的 package 中都会被添加 A 这个依赖,当然也支持 --filter 字段来对 package 进行过滤。

pnpm 通过 pnpm-workspace.yaml 配置定义工作空间目录,并能够使您从工作空间中包含 / 排除目录。

  1. 安全性高

之前在使用 npm/yarn 的时候,由于 node_modules 的扁平结构,如果 A 依赖 B, B 依赖 C,那么 A 当中是可以直接使用 C 的,但问题是 A 当中并没有声明 C 这个依赖。因此会出现这种非法访问的情况。但 pnpm 脑洞特别大,自创了一套依赖管理方式,很好地解决了这个问题,保证了安全性,具体怎么体现安全、规避非法访问依赖的风险的,主要表现为幽灵依赖。

小结

总而言之,pnpm 与 npm/yarn 比较,可以通过以下表格总结出来。

pnpm 原理图

四、什么是 changesets ?

在 workspace 中对包版本管理是一个非常复杂的工作,原来 yarn 还可以使用 lerna,遗憾的是 pnpm 没有提供内置的解决方案,一部分开源项目在自己的项目中自己实现了一套包版本的管理机制,比如 Vue3、Vite等。

pnpm 推荐了两个开源的版本控制工具:

  • changesets
  • rush

这里我们采用了 changesets 来做依赖包的管理。选用 changesets 的主要原因还是文档更加清晰一些,感觉上手比较容易。

按照 changesets 文档介绍的,changesets 主要是做了两件事:

Changesets hold two key bits of information: a version type (following semver), and change information to be added to a changelog.

简而言之就是管理包的 version 和生成 changelog。

五、为什么要使用 changesets ?

1. lerna 发包方案缺陷

早期我们的组件库版本中采用了 lerna 这一套的发包方案,但随着频繁的使用和深入的研究发现,这套方案随之带来了不少问题:

  • ignoreChanges 不能做到文件的完全忽略,存在优先级问题;
  • lerna version 根据 commit 以及 tag 更新出来的包版本不符合预期;
  • 生成的 CHANGELOG 文件信息不完整;
  • lifecycle scripts 经常命中一些用户自定义的 script(例如 publish 等);
  • CI 中自动化发包场景需要很高的定制成本;
  • lerna 本身不支持 workspace 协议,导致基于 pnpm 开发的一些仓库无法使用。

2. changesets 的基本工作流程及优势

Changesets 提供了简单、轻量的版本控制和发布方案,其工作流原理如下。

pnpm 原理图

整个流程可以理解为四部曲:

第一步,changeset init。通过执行执行该命令,可以在项目根目录下生成一个 .changeset 目录,里面会生成一个 changesets 的 config 文件,一般项目初始化时,执行一遍即可,后续不用再执行该操作。

第二步,changeset add 或者 changesetadd 在 changesets 中算得上比较关键的命令之一了,它会根据 monorepo下的项目来生成一个 changeset 文件,里面会包含前面提到的 changeset 文件信息(更新包名称、版本层级、CHANGELOG 信息。

第三步,changeset version。 这个命令这里可以当作 Bump Version(版本号升级或更新操作)来理解,这里本质上做的工作是消耗 changesets 文件并且修改对应包版本以及依赖该包的包版本,同时会根据之前 changeset 文件里面的信息来生成对应的 CHANGELOG 信息。

第四步,changeset publish。本质上就是对 npm publish 做了一次封装,同时会检查对应的 registry 上有没有对应包的版本,如果已经存在了,就不会再发包了,如果不存在会对对应的包版本执行一次 npm publish

另外,changesets 支持 pre 版本非 pre 版本 的操作切换,需要在第二步之前执行 changeset pre enterchangeset pre exit 等操作。

小结

总而言之,主要优点在于提供了很大的自主权在使用者手中,在复杂的业务场景下能够做出一些合适的调整,例如用户可以自行修改 changesets 文件、changelog 文件、甚至是 Bump Version 后不满意的版本。

相比较于 lerna 提供的比较理想化的方案而言,changeset 本身是一套泛用性很强的方案,而且比较适合当下 monorepo 工作流场景下的一些运作方式,虽然本身还存在着不少的缺点 。

总结

综上所述,为什么使用 pnpm + workspace + changesets 替代 yarn + lerna 构建我们组件库,可以总结为以下几个点。

  1. 使用 pnpm 解决私服开启鉴权带来的一些非常重要的影响和变化;
  2. 充分利用 pnpm 不可替代的速度快、高效利用磁盘空间、天然支持 monorepo(支持 workspace 配置)、 安全性高(比如解决幽灵依赖问题、node_modules 嵌套过深问题)等优势;
  3. changesets 带来更加简单、更加轻量的版本控制和发布方案,使得版本管理及发布流程更多的自主权掌握在使用者手中。