npm<v3
node 依赖管理器的起源,在 npm 前两个版本中,依赖无限嵌套,各种重复,被调侃质量比黑洞还大。
yarn v1
yarn,代表 Yet Another Resource Negotiator。v1 版本现在被称为 yarn classic,当年拥有不少注目特性,DX(开发体验)↑ 安全 ↑ 安装速度 ↑,创新点:
- 依赖提升(hoisting)、依赖安装扁平化
- 引入 lock 文件
- 有一些 npm 没有的命令,例如更新依赖到最新版本
- 支持 monorepo
- 离线缓存
但是引入了两个问题:
点击上面两个链接有详细解析,下面用中文总结几句。
依赖分身
依赖提升只能提升一个版本,其他版本依然安装在嵌套的 node_modules 中,会重复安装。所以如果项目里要求各种固定版本,依然是要安装一大堆重复的东西。
不过好消息是按照正常开发习惯和开发周期,下面这一大堆其实都只需要安装一次,以下是真实项目的 lock 文件的一部分:
安装了一大堆不同版本的 lodash 其实都可以使用 4.17.21
,它符合所有版本要求。不确定版本要求的写法可以在 npm semver calculator 查询。
幽灵依赖
因为依赖提升这个特性,非直接依赖的包你也能直接使用,因为全都平铺在顶层 node_modules,这种可以依赖你没直接依赖的包的情况就是幽灵依赖。
以前开发一个库的时候就遇到这个问题,开发的时候默认引入了某个工具,开发时一切正常,但是打包出来别人用的时候缺了一个依赖。
npm>=v3
npm3 之后的版本向 yarn 学习:
- 跟进了 hoisting
- 跟进了 lock 文件
How npm3 Works 比较详细地解析了 npm3 的改进。
然而……该有的问题还是没解决。
pnpm
pnpm 使用软硬链接的巧妙结合同时解决以上两个问题。
- 所有依赖有一个全局仓库,被引用的会硬链(hard link)到项目
- 对于依赖之间的关系,使用软链(Symlink)链接,实现最短路径查询依赖
官网给出这样的例子:
带 <store>/
的是硬,带 ../../
的是软。即使依赖深度是 foo > bar > qar
依然可以保持这样的浅层结构。顶层只有一个 foo,没有幽灵依赖,项目用到的版本都在 .pnpm
平铺,不会造成依赖分身。
yarn>=v2
可以使用激进的 Plug’n’Play (PnP)。
Yarn generates a single .pnp.cjs file instead of the usual node_modules folder containing copies of various packages.
PnP 不用 node.js 自己去找依赖位置了,直接用 .pnp.cjs
告诉它所有依赖的准确位置,这个准确位置是一个中央仓库,所以也不需要把依赖复制到项目的 node_modules 里。PnP 出现之初有很多工具都不支持这种依赖安装方法,现在兼容性估计好很多了。但是只要 pnpm 的速度还能接受,个人认为还是直接用传统 node_modules 吧。
对比
很惊讶,根据 JavaScript package managers compared: npm, Yarn, or pnpm? 中的性能测试结果,npm v8.1.2 已经比 yarn v1.23.0 快了,首次安装的速度 pnpm 仍然拉开两位前辈一大截。打破常规的 PnP 自然是最快的,但是要在项目中使用它,还得看项目能不能兼容 PnP。(虽然 pnpm 的软硬链有些包也不支持)