前端开发者们每天都在接触 xxx install,包管理器是必不可少的工具。

本文会以尽量简洁的语言来描述当下主流包管理工具 npm、yarn、pnpm 的管理策略以及进化史,不涉及任何晦涩的代码。

npm - 先锋

2010 年 1 月,一款名为 npm 的包管理器诞生。

很多人认为 npm 是 node package manager 的缩写,其实不是,而且 npm 根本也不是任何短语的缩写。

npm 官方辟谣:

它的前身其实是名为 pm(pkgmakeinst) 的 bash 工具,它可以在各种平台上安装各种东西。

硬要说缩写的话,也应该是 node pm 或者 new pm。

嵌套的 node_modules 结构

npm 在早期采用的是嵌套的 node_modules 结构,直接依赖会平铺在 node_modules 下,子依赖嵌套在直接依赖的 node_modules 中。

比如项目依赖了A 和 C,而 A 和 C 依赖了不同版本的 B@1.0 和 B@2.0,node_modules 结构如下:

node_modules
├── A@1.0.0
│   └── node_modules
│       └── B@1.0.0
└── C@1.0.0└── node_modules└── B@2.0.0

如果 D 也依赖 B@1.0,会生成如下的嵌套结构:

node_modules
├── A@1.0.0
│   └── node_modules
│       └── B@1.0.0
├── C@1.0.0
│   └── node_modules
│       └── B@2.0.0
└── D@1.0.0└── node_modules└── B@1.0.0

可以看到同版本的 B 分别被 A 和 D 安装了两次。

依赖地狱 Dependency Hell

在真实场景下,依赖增多,冗余的包也变多,node_modules 最终会堪比黑洞,很快就能把磁盘占满。而且依赖嵌套的深度也会十分可怕,这个就是依赖地狱

扁平的 node_modules 结构

为了将嵌套的依赖尽量打平,避免过深的依赖树和包冗余,npm v3 将子依赖「提升」(hoist),采用扁平的 node_modules 结构,子依赖会尽量平铺安装在主依赖项所在的目录中。

node_modules
├── A@1.0.0
├── B@1.0.0
└── C@1.0.0└── node_modules└── B@2.0.0

可以看到 A 的子依赖的 B@1.0 不再放在 A 的 node_modules 下了,而是与 A 同层级。

而 C 依赖的 B@2.0 因为版本号原因还是嵌套在 C 的 node_modules 下。

这样不会造成大量包的重复安装,依赖的层级也不会太深,解决了依赖地狱问题,但也形成了新的问题。

幽灵依赖 Phantom dependencies

幽灵依赖是指在 package.json 中未定义的依赖,但项目中依然可以正确地被引用到。

比如上方的示例其实我们只安装了 A 和 C:

{"dependencies": {"A": "^1.0.0","C": "^1.0.0"}
}

由于 B 在安装时被提升到了和 A 同样的层级,所以在项目中引用 B 还是能正常工作的。

幽灵依赖是由依赖的声明丢失造成的,如果某天某个版本的 A 依赖不再依赖 B 或者 B 的版本发生了变化,那么就会造成依赖缺失或兼容性问题。

不确定性 Non-Determinism

不确定性是指:同样的 package.json 文件,install 依赖后可能不会得到同样的 node_modules 目录结构。

还是之前的例子,A 依赖 B@1.0,C 依赖 B@2.0,依赖安装后究竟应该提升 B 的 1.0 还是 2.0。

node_modules
├── A@1.0.0
├── B@1.0.0
└── C@1.0.0└── node_modules└── B@2.0.0
node_modules
├── A@1.0.0
│   └── node_modules
│       └── B@1.0.0
├── B@2.0.0
└── C@1.0.0

取决于用户的安装顺序。

如果有 package.json 变更,本地需要删除 node_modules 重新 install,否则可能会导致生产环境与开发环境 node_modules 结构不同,代码无法正常运行。

依赖分身 Doppelgangers

假设继续再安装依赖 B@1.0 的 D 模块和依赖 @B2.0 的 E 模块,此时:

  • A 和 D 依赖 B@1.0

  • C 和 E 依赖 B@2.0

以下是提升 B@1.0 的 node_modules 结构:

node_modules
├── A@1.0.0
├── B@1.0.0
├── D@1.0.0
├── C@1.0.0
│   └── node_modules
│       └── B@2.0.0
└── E@1.0.0└── node_modules└── B@2.0.0

可以看到 B@2.0 会被安装两次,实际上无论提升 B@1.0 还是 B@2.0,都会存在重复版本的 B 被安装,这两个重复安装的 B 就叫 doppelgangers。

而且虽然看起来模块 C 和 E 都依赖 B@2.0,但其实引用的不是同一个 B,假设 B 在导出之前做了一些缓存或者副作用,那么使用者的项目就会因此而出错。

yarn - 创新

2016 年,yarn 发布,yarn 也采用扁平化 node_modules 结构。它的出现是为了解决 npm v3 几个最为迫在眉睫的问题:依赖安装速度慢,不确定性。

提升安装速度

在 npm 中安装依赖时,安装任务是串行的,会按包顺序逐个执行安装,这意味着它会等待一个包完全安装,然后再继续下一个。

为了加快包安装速度,yarn 采用了并行操作,在性能上有显著的提高。而且在缓存机制上,yarn 会将每个包缓存在磁盘上,在下一次安装这个包时,可以脱离网络实现从磁盘离线安装。

lockfile 解决不确定性

yarn 更大的贡献是发明了 yarn.lock。

在依赖安装时,会根据 package.josn 生成一份 yarn.lock 文件。

lockfile 里记录了依赖,以及依赖的子依赖,依赖的版本,获取地址与验证模块完整性的 hash。

即使是不同的安装顺序,相同的依赖关系在任何的环境和容器中,都能得到稳定的 node_modules 目录结构,保证了依赖安装的确定性。

所以 yarn 在出现时被定义为快速、安全、可靠的依赖管理。而 npm 在一年后的 v5 才发布了 package-lock.json。

与 npm 一样的弊端

yarn 依然和 npm 一样是扁平化的 node_modules 结构,没有解决幽灵依赖依赖分身问题。

pnpm - 后浪

pnpm - performant npm,在 2017 年正式发布,定义为快速的,节省磁盘空间的包管理工具,开创了一套新的依赖管理机制,成为了包管理的后起之秀。

内容寻址存储 CAS

与依赖提升和扁平化的 node_modules 不同,pnpm 引入了另一套依赖管理策略:内容寻址存储。

该策略会将包安装在系统的全局 store 中,依赖的每个版本只会在系统中安装一次。

在引用项目 node_modules 的依赖时,会通过硬链接与符号链接在全局 store 中找到这个文件。为了实现此过程,node_modules 下会多出 .pnpm 目录,而且是非扁平化结构。

  • 硬链接 Hard link:硬链接可以理解为源文件的副本,项目里安装的其实是副本,它使得用户可以通过路径引用查找到全局 store 中的源文件,而且这个副本根本不占任何空间。同时,pnpm 会在全局 store 里存储硬链接,不同的项目可以从全局 store 寻找到同一个依赖,大大地节省了磁盘空间。

  • 符号链接 Symbolic link:也叫软连接,可以理解为快捷方式,pnpm 可以通过它找到对应磁盘目录下的依赖地址。

还是使用上面 A,B,C 模块的示例,使用 pnpm 安装依赖后 node_modules 结构如下:

node_modules
├── .pnpm
│   ├── A@1.0.0
│   │   └── node_modules
│   │       ├── A => <store>/A@1.0.0
│   │       └── B => ../../B@1.0.0
│   ├── B@1.0.0
│   │   └── node_modules
│   │       └── B => <store>/B@1.0.0
│   ├── B@2.0.0
│   │   └── node_modules
│   │       └── B => <store>/B@2.0.0
│   └── C@1.0.0
│       └── node_modules
│           ├── C => <store>/C@1.0.0
│           └── B => ../../B@2.0.0
│
├── A => .pnpm/A@1.0.0/node_modules/A
└── C => .pnpm/C@1.0.0/node_modules/C

<store>/xxx 开头的路径是硬链接,指向全局 store 中安装的依赖。

其余的是符号链接,指向依赖的快捷方式。

pnpm 官方图片也清晰地解释了这套机制:

未来可期

这套全新的机制设计地十分巧妙,不仅兼容 node 的依赖解析,同时也解决了:

  1. 幽灵依赖问题:只有直接依赖会平铺在 node_modules 下,子依赖不会被提升,不会产生幽灵依赖。

  2. 依赖分身问题:相同的依赖只会在全局 store 中安装一次。项目中的都是源文件的副本,几乎不占用任何空间,没有了依赖分身。

同时,由于链接的优势,pnpm 的安装速度在大多数场景都比 npm 和 yarn 快 2 倍,节省的磁盘空间也更多。

但也存在一些弊端:

  1. 由于 pnpm 创建的 node_modules 依赖软链接,因此在不支持软链接的环境中,无法使用 pnpm,比如 Electron 应用。

  2. 因为依赖源文件是安装在 store 中,调试依赖或 patch-package 给依赖打补丁也不太方便,可能会影响其他项目。

yarn Plug’n’Play - 探索

2020 年 1 月,yarn v2 发布,也叫 yarn berry(v1 叫 yarn classic)。它是对 yarn 的一次重大升级,其中一项重要更新就是 Plug’n’Play(Plug'n'Play = Plug and Play = PnP,即插即用)。

npm 与 yarn 的依赖安装与依赖解析都涉及大量的文件 I/O,效率不高。开发 Plug’n’Play 最直接的原因就是依赖引用慢,依赖安装慢。

抛弃 node_modules

无论是 npm 还是 yarn,都具备缓存的功能,大多数情况下安装依赖时,其实是将缓存中的相关包复制到项目目录中 node_modules 里。

而 yarn PnP 则不会进行拷贝这一步,而是在项目里维护一张静态映射表 pnp.cjs。

pnp.cjs 会记录依赖在缓存中的具体位置,所有依赖都存在全局缓存中。同时自建了一个解析器,在依赖引用时,帮助 node 从全局缓存目录中发现依赖,而不是查找 node_modules。

这样就避免了大量的 I/O 操作同时项目目录也不会有 node_modules 目录生成,同版本的依赖在全局也只会有一份,依赖的安装速度和解析速度都有较大提升。

pnpm 在 2020 年底的 v5.9 也支持了 PnP。

脱离 node 生态

pnp 比较明显的缺点是脱离了 node 生态。

  • 因为使用 PnP 不会再有 node_modules 了,但是 Webpack,Babel 等各种前端工具都依赖 node_modules。虽然很多工具比如 pnp-webpack-plugin 已经在解决了,但难免会有兼容性风险。

  • PnP 自建了依赖解析器,所有的依赖引用都必须由解析器执行,因此只能通过 yarn 命令来执行 node 脚本。

总结

目前还没有完美的依赖管理方案,可以看到在依赖管理的发展过程中,出现了:

  • 不同的 node_modules 结构,有嵌套,扁平,甚至没有 node_modules,不同的结构也伴随着兼容与安全问题。

  • 不同的依赖存储方式来节约磁盘空间,提升安装速度。

  • 每种管理器都伴随新的工具和命令,不同程度的可配置性和扩展性,影响开发者体验。

  • 这些包管理器也对 monorepo 有不同程度的支持,会直接影响项目的可维护性和速度。

库与开发者能够在这样优化与创新的发展过程中互相学习,站在巨人的肩膀上继续前进,不断推动前端工程领域的发展。

参考

  • https://mp.weixin.qq.com/s/CYQQKvy9MaGHdpSHpBJwzw by 王辰新(朔宸)

  • https://blog.logrocket.com/javascript-package-managers-compared/ by Sebastian Weber

  • https://wxsm.space/2021/npm-history/ by wxsm

  • https://loveky.github.io/2019/02/11/yarn-pnp/ by LOVEKY

  • https://juejin.cn/post/7097906848505806885 by 高尚的尚

  • https://juejin.cn/post/7001794162970361892 by zoomdong

  • https://juejin.cn/post/6932046455733485575 by 神三元

  • https://juejin.cn/post/6844903814038831118 by 荒山

  • https://juejin.cn/post/7060844948316225572 by 酒窝yun过去了

  • https://zhuanlan.zhihu.com/p/353208988 by 松若章

  • https://segmentfault.com/a/1190000017075256 by caoweiju

  • https://segmentfault.com/a/1190000009709213 by meikidd

  • https://pnpm.io/zh/symlinked-node-modules-structure

  • https://pnpm.io/zh/blog/2020/05/27/flat-node-modules-is-not-the-only-way

- END -

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

深入浅出 npm yarn pnpm 包管理机制相关推荐

  1. 【总结】1409- 深入浅出 npm yarn pnpm 包管理机制

    前端开发者们每天都在接触 xxx install,包管理器是必不可少的工具. 本文会以尽量简洁的语言来描述当下主流包管理工具 npm.yarn.pnpm 的管理策略以及进化史,不涉及任何晦涩的代码. ...

  2. nodejs第五天 npm yarn pnpm 包管理器

    文章目录 npm package.json 安装包 全局安装 配置镜像 yarn 安装使用 镜像配置 pnpm 使用 镜像 npm node中的包管理器叫做npm(node package manag ...

  3. js读取外部json指定字段值完整代码_前端工程化 剖析npm的包管理机制(完整版)...

    导读 现如今,前端开发的同学已经离不开 npm 这个包管理工具,其优秀的包版本管理机制承载了整个繁荣发展的NodeJS社区,理解其内部机制非常有利于加深我们对模块开发的理解.各项前端工程化的配置以加快 ...

  4. 前端工程化 - 剖析npm的包管理机制

    导读 现如今,前端开发的同学已经离不开 npm 这个包管理工具,其优秀的包版本管理机制承载了整个繁荣发展的NodeJS社区,理解其内部机制非常有利于加深我们对模块开发的理解.各项前端工程化的配置以加快 ...

  5. npm包管理机制引质疑:又一安装程序中发现恶意代码,开发者账户频遭劫持

    铜灵 发自 凹非寺 量子位 出品 | 公众号 QbitAI npm行不行,包管理机制行不行? 最新的一次npm包被篡改事件,让开发者的这两个疑问更加强烈了. 最新中枪的是纯函数式编程语言Purescr ...

  6. 重学前端——npm yarn pnpm

    npm yarn pnpm npm NPM 是最初由 Node.js 项目开发的 JavaScript 包管理器.它使开发人员能够更轻松地在不同项目之间共享代码,并在自己的项目中使用其他人的代码.安装 ...

  7. 尤雨溪推荐神器 ni ,能替代 npm/yarn/pnpm ?简单好用!源码揭秘!

    1. 前言 大家好,我是若川.最近组织了源码共读活动,感兴趣的可以加我微信 ruochuan12 想学源码,极力推荐之前我写的<学习源码整体架构系列>jQuery.underscore.l ...

  8. python包管理机制_Go 1.5之前的多种包管理机制简介(

    在 Go 语言中,我们可以使用go get命令安装远程仓库中托管的代码,不同于 Ruby Gem.pypi 等集中式的包管理机制, Go 语言的包管理系统是去中心化的.简单来讲,go get命令支持任 ...

  9. Go 语言本身提供的包管理机制

    在 Go 语言中,我们可以使用go get命令安装远程仓库中托管的代码,不同于 Ruby Gem.pypi 等集中式的包管理机制, Go 语言的包管理系统是去中心化的.简单来讲,go get命令支持任 ...

最新文章

  1. 零基础可以学python吗-零基础适合学Python吗?小白能否学会Python?
  2. 【控制】《复杂运动体系统的分布式协同控制与优化》-方浩老师-第11章-意图场模型干预下的人机共享控制
  3. 准备 overlay 网络实验环境 - 每天5分钟玩转 Docker 容器技术(49)
  4. php创建压缩文件 保存路径,通过php生成zip压缩文件,支持文件和压缩包路径查找...
  5. rootca.pem 微信支付api 秘钥验证
  6. Treap原理和实现方法
  7. pb将datawindow数据导出EXCEL
  8. json入门 PHP,PHP开发基础教程之JSON
  9. parseConf(配置文件解析器)
  10. ASCLL码对照表02(可显示字符)
  11. 黄山归来不看岳:《Java开发手册(黄山版)》新增 11 条规约
  12. Android平台的音乐资源管理与播放
  13. LabVIEW32位和64位的兼容性
  14. CT原理与技术(生物医学工程专业)
  15. 【博学谷学习记录】超强总结,用心分享丨一种创新的表格
  16. WPF发布程序后未授予信任的解决办法
  17. 「 WEB测试工程师 」岗位一面总结
  18. win10电脑的时间怎样设置
  19. 偏最小二乘,主成分分析,主成分回归,奇异值之间的关系
  20. 现代OpenGL教程 02——贴图

热门文章

  1. 一段真实在个人经历 给那些迷失方向的朋友 转帖
  2. 古月居ROS入门21讲-基础概述
  3. 万字长文揭秘今日头条、抖音的推荐算法原理!
  4. 【安装配置】流泪!!!!Windows下装faiss
  5. 盲注 mysql 密码_Mysql盲注技巧
  6. 关于 win8.1 激活问题
  7. Lumerical官方案例、FDTD时域有限差分法仿真学习(七)——纳米孔阵列(Nanohole array)
  8. Halcon 图片格式RGB转灰度
  9. 再到关键阻力位,欧元何去何从
  10. Java static静态变量只有一个,被类拥有