一、背景
最近公司私服开启鉴权,对于前端来说,带来一些非常重要的影响和变化:
- 不管拉取 npm 包还是发布 npm 包,均需要鉴权,具体就是 .npmrc 配置信息需要增加 auth token 信息;
- 使用 yarn 出现偶然下载包失败问题以及必现的无法发包问题,而 npm 和 pnpm 可以;
- 现有组件库工程(所使用的工具链为 yarn + lerna)发包时会出现 401 鉴权问题,甚至有时连安装依赖包也有问题。
基于以上三个问题,我们做了一些尝试:
- 将 yarn 从 1.x 升级到 2.x,也就是说 yarn 2.x + lerna,不行;❌
- 将 yarn 改成 pnpm,使用 pnpm + lerna 组合,也出现了一些问题;❌
- 将 yarn + lerna 组合改成 pnpm + workspace + [pnpm publish],成功。✅
最后,我们决定彻底地将 yarn + lerna 组合切换成 pnpm + workspace + changesets 。
二、什么是 pnpm ?
pnpm 是新一代的包管理工具,是目前较为先进的包管理器。按照官网说法,可以实现节约磁盘空间并提升安装速度和创建非扁平化的 node_modules 文件夹两大目标,具体原理可以参考 pnpm 官网。
以下是官方给的一张关于 pnpm 的原理图:
所以,综合上图,我就现在知道 pnpm 为什么说是先进的包管理器了吧。
首先,最大的优点是节省磁盘空间,一个包全局只保存一份,剩下的都是软硬连接,这必然节省不少磁盘空间,并且使用软链接的方式创建非扁平的 node_modules 嵌套关系。
其次就是快,因为通过链接的方式而不是复制,自然会快。
三、pnpm 对比 npm/yarn 有什么优势?
- 速度快
pnpm 安装包的速度究竟有多快?先以 React 包为例来对比一下:
可以看到,作为黄色部分的 pnpm,在绝多大数场景下,包安装的速度都是明显优于 npm/yarn,速度会比 npm/yarn 快 2-3 倍。
对 yarn 比较熟悉的同学可能会说,yarn 不是有 PnP 安装模式吗?直接去掉 node_modules,将依赖包内容写在磁盘,节省了 node 文件 I/O 的开销,这样也能提升安装速度。
接下来,我们以这样一个仓库为例,我们来看一看 benchmark 数据,主要对比一下 pnpm 和 yarn PnP:
从中可以看到,总体而言,pnpm 的包安装速度还是明显优于 yarn PnP 的。
- 高效利用磁盘空间
pnpm 内部使用基于内容寻址的文件系统来存储磁盘上所有的文件,这个文件系统出色的地方在于:
- 不会重复安装同一个包。用 npm/yarn 的时候,如果 100 个项目都依赖 lodash,那么 lodash 很可能就被安装了 100 次,磁盘中就有 100 个地方写入了这部分代码。但在使用 pnpm 只会安装一次,磁盘中只有一个地方写入,后面再次使用都会直接使用 hardlink(硬链接)。
- 即使一个包的不同版本,pnpm 也会极大程度地复用之前版本的代码。举个例子,比如 lodash 有 100 个文件,更新版本之后多了一个文件,那么磁盘当中并不会重新写入 101 个文件,而是保留原来的 100 个文件的 hardlink,仅仅写入那一个新增的文件。
- 支持 monorepo
随着前端工程的日益复杂,越来越多的项目开始使用 monorepo。之前对于多个项目的管理,我们一般都是使用多个 git 仓库,但 monorepo 的宗旨就是用一个 git 仓库来管理多个子项目,所有的子项目都存放在根目录的 packages 目录下,那么一个子项目就代表一个 package。
pnpm 与 npm/yarn 另外一个很大的不同就是支持了 monorepo,体现在各个子命令的功能上,比如在根目录下 pnpm add A -r
, 那么所有的 package 中都会被添加 A 这个依赖,当然也支持 --filter
字段来对 package 进行过滤。
pnpm 通过 pnpm-workspace.yaml 配置定义工作空间目录,并能够使您从工作空间中包含 / 排除目录。
- 安全性高
之前在使用 npm/yarn 的时候,由于 node_modules 的扁平结构,如果 A 依赖 B, B 依赖 C,那么 A 当中是可以直接使用 C 的,但问题是 A 当中并没有声明 C 这个依赖。因此会出现这种非法访问的情况。但 pnpm 脑洞特别大,自创了一套依赖管理方式,很好地解决了这个问题,保证了安全性,具体怎么体现安全、规避非法访问依赖的风险的,主要表现为幽灵依赖。
小结
总而言之,pnpm 与 npm/yarn 比较,可以通过以下表格总结出来。
四、什么是 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 提供了简单、轻量的版本控制和发布方案,其工作流原理如下。
整个流程可以理解为四部曲:
第一步,changeset init
。通过执行执行该命令,可以在项目根目录下生成一个 .changeset
目录,里面会生成一个 changesets 的 config 文件,一般项目初始化时,执行一遍即可,后续不用再执行该操作。
第二步,changeset add
或者 changeset
。add
在 changesets 中算得上比较关键的命令之一了,它会根据 monorepo下的项目来生成一个 changeset 文件,里面会包含前面提到的 changeset 文件信息(更新包名称、版本层级、CHANGELOG 信息。
第三步,changeset version
。 这个命令这里可以当作 Bump Version
(版本号升级或更新操作)来理解,这里本质上做的工作是消耗 changesets 文件并且修改对应包版本以及依赖该包的包版本,同时会根据之前 changeset 文件里面的信息来生成对应的 CHANGELOG 信息。
第四步,changeset publish
。本质上就是对 npm publish
做了一次封装,同时会检查对应的 registry 上有没有对应包的版本,如果已经存在了,就不会再发包了,如果不存在会对对应的包版本执行一次 npm publish
。
另外,changesets 支持 pre 版本
和 非 pre 版本
的操作切换,需要在第二步之前执行 changeset pre enter
、changeset pre exit
等操作。
小结
总而言之,主要优点在于提供了很大的自主权在使用者手中,在复杂的业务场景下能够做出一些合适的调整,例如用户可以自行修改 changesets 文件、changelog 文件、甚至是 Bump Version 后不满意的版本。
相比较于 lerna 提供的比较理想化的方案而言,changeset 本身是一套泛用性很强的方案,而且比较适合当下 monorepo 工作流场景下的一些运作方式,虽然本身还存在着不少的缺点 。
总结
综上所述,为什么使用 pnpm + workspace + changesets 替代 yarn + lerna 构建我们组件库,可以总结为以下几个点。
- 使用 pnpm 解决私服开启鉴权带来的一些非常重要的影响和变化;
- 充分利用 pnpm 不可替代的速度快、高效利用磁盘空间、天然支持 monorepo(支持 workspace 配置)、 安全性高(比如解决幽灵依赖问题、node_modules 嵌套过深问题)等优势;
- changesets 带来更加简单、更加轻量的版本控制和发布方案,使得版本管理及发布流程更多的自主权掌握在使用者手中。