现代包管理器的进化史

现代包管理器的进化史

【招聘】字节跳动巨量星图/TCM团队持续招聘前端工程师,详情可见文后

现在很多项目都会使用 pnpm 替代 npm or yarn 来做包管理,众所周知 pnpm 的宣传里有一个很吸引人的点是可以更小更快的安装我们的依赖。那么

  • 为什么 pnpm 能做到更小更快呢?
  • 除了更小更快他还解决了哪些问题让大家得以青睐呢?

要回答这些问题,就得了解这些年包管理的几场重大”变革“ ,于是便有了今天的分享。



Npm v1 & v2


刚开始 npm 的设计非常的简单:有一个标准的包管理器供大家下载和查阅,将开发人员从那个“网上下载资源,在手动解压添加近项目”的年代中解放出来。所以初版 npm 使用了很简单嵌套结构来进行版本管理。
我们假设有这样两个项目:

  • App1 依赖 packageA 和 packageC,而 packageA 和 packageC 都依赖了 packageB;
  • App2 依赖 packageB 和 packageD,packageD 依赖 packageE,packageE 依赖 packageF......

那么这两个项目的 nodule_packages 目录结构如下:


这样做带来的问题也显而易见:

  1. 项目里会反复安装相同的依赖:比如 App1 下重复安装了 packageB
  2. 会带来依赖地狱:比如 App2 下的 D,E,F......
  3. 不同的项目之间会重复安装相同的包:比如 App1 和 APP2 都安装了packageB

网上为此有一些经典的梗:



Npm v3


为了解决上述的问题,npm v3 完全重写了 npm 的安装程序,采用扁平化的方式,将主依赖项和子依赖都装到 node_modules 一级目录下,还是上面那个例子,此刻 node_modules 的目录结构如下:


这时项目内的重复安装和依赖地狱看似都得到了不错的解决,但此刻又带来了一些新的问题:

不完全解决的重复依赖

我们假设 App1 又有了新的依赖 packageG 和 packageH,packageG、packageH 都依赖 packageB 的 v2.0 版本,那么此刻 node_modules 的依赖可能是以下结构:


由于A、C 和 G、H 依赖的是不同版本的 B,所以子依赖项提升时只会提升 v1.0 版本的 packageB,v2.0 版本的 packageB依旧挂在G,H下。显而易见,G 和 H 依旧安装了两份同样的 packageB v2.0。

不确定性

发现我上述说:“node_modules的依赖可能是以下结构”,因为 node_modules 的结构还可能是下面这种:


这个例子里 packageB v2.0 得到了提升,A 和C 下挂着 Bv1.0。
那么到底是提升 Bv1.0 还是提升 Bv2.0?取决于用户的安装顺序(install操作),这种不确定性为项目的依赖问题解决带来了极大的困难,不同的成员install的结果不一定是一样的......

隐式依赖(又名幽灵依赖)

在上述的例子里,虽然 App1 没有直接声明引用 packageB,但项目里依旧可以正常的使用。原因是 npm3 将所有的依赖都平铺到node_modules 下,因此 require 函数可以查找到它,这样也给项目带来了一些风险:

  • 阅读困难:没有在 package.json 里定义却可以引入这个包,why???
  • 引入的版本不确定性:像上面这个例子,App1 require 的 packageB 是 v1.0 版本还是 v2.0 版本完全取决于A C G H的依赖,如果有一天A C G H版本更新依赖 packageBv5.0 版本了,App1 在不知情的情况下依赖的版本从 v1.0 升级到 v5.0,或许会给项目带来意想不到的问题。


Yarn


2016年,Facebook在官网上发布了这篇文章:Yarn: A new package manager for JavaScript 。文章开篇就说了,在使用 npm 的过程中,他们遇到了:一致性、安全性、离线安装和性能方面的问题
那我们看看yarn是怎么解决的:

  1. 一致性&安全性:增加 lockfiles(yarn.lock):记录所有被安装依赖的版本号,安装时将优先参考 lock 文件的提供的版本:
  2. 离线安装:每次从网络下载一个依赖包时,yarn都会将其放在本地的全局缓存中,下次下载会优先在全局缓存目录中查找,如果有,将其copy到当前目录下;
  3. 性能问题:并行安装。无论是 npm 还是 yarn,在安装时都会执行一系列任务。npm 是按照队列执行每一个 package:当前的package 安装完成后,再去执行下一个package,而 yarn 是同步执行所有的任务。

我们上述的不确定性得到了很好的解决,还顺带提升了安装速度,提供了离线安装等功能。所以当 yarn 一发布,就受到了广泛的关注,当天,npm 官方博客当天发表了一篇Hello, Yarn!。恭喜了 yarn 的开源,并对 yarn 团队及 facebook 为社区及整个 npm 生态做出的贡献给予了很高的评价,后续的 npm v5 也吸纳了 yarn 优秀的 lock 和缓存机制。

至此,包管理机制的大楼看似已经建成,但依旧有两朵乌云飘荡在上空:

  1. 多项目之间的复用还是没有解决
  2. 隐式依赖



Pnpm

多项目间的复用

如何解决多项目间的复用呢?既然都有了本地缓存,可不可以不要复制一份依赖到项目里呢?
pnpm在解决这个问题上采取了硬链接的方式:硬链接与平时我们使用比较多的软链接不同的是,他会直接指向磁盘中原始文件所在的地址。
在下面这个例子中,App1 和 App2 都依赖了 Bv1.0,并且各自的 node_modules 都占用了 1MB 的空间,那么看起来像是一共占用了2MB 的空间,实际上他们指向的是相同的磁盘空间,所以 Bv1.0 在两个项目里总共也就占用了 1MB,而不是 2MB。
同时,这种方式还很好的解决了上面提到的 “npmv3 不完全的重复依赖” 问题。


由于绝大部分的依赖包都是通过硬链接串联到项目里的node_modules下,节省了网络下载的开销和复制磁盘的开销,这也是pnpm速度一骑绝尘的原因:



同一个依赖多个版本的复用

对于同一个依赖包的不同版本,则仅有版本之间不同的文件会被存储起来。例如:Bv1.0 包含 100 个文件,Bv2.0发布时只有一个文件有修改(version2.js),那么 pnpm 本地缓存时只会缓存 version2.js 到存储中,而不会因为一个文件的修改而保存依赖包的所有文件。




创建非扁平的node_modules目录

如何解决隐式依赖呢,既然打平会让 require 能正确访问哪些它本不该访问的依赖,那我直接不打平不就好了......


在默认情况下,pnpm 的 node_modules 一级目录只存在 package.json 里显式声明的依赖,其他依赖的依赖都放在.pnpm下。
乍一眼看,.pnpm下的目录结构和 npmv3 下的 node_modules 很像,但他每个package下有一个node_modules,打开看里面的结构和 npmv1&v2的目录又有点像,有当前package下所有的依赖,实际上这些都是通过 软连接(符号链接)串联起来的,可以看上面官网给出的示例图。这样既完美杜绝隐式依赖,又能方便的查看当前依赖的目录结构,同时还不会增加存储空间。

monorepo支持

Pnpm 天然支持 monorepo 项目,除了可以指定 workspace,它还提供了很多指令能够方便的对 workspace 下的项目做依赖管理,这里就不多做赘述了。



存在的问题

  • 非扁平化破坏性的结构和必须使用自身锁文件pnpm-lock.yaml,都给迁移带来了写成本;
  • 软连接的兼容性,存在一些不能使用的场景;
  • 不同应用的依赖是硬链接到同一份文件上,如果在调试时修改了某个依赖的文件,可能无意间影响了其他项目。


后记


其实发展到现在,npm,yarn以及pnpm 后面都陆陆续续迭代了很多的功能和优化,速度上也越发相似,大家都在互相学习,互相进步,也正是这种追求极致和坦诚清晰的氛围,开源社区才会越来越好的吧~


【招聘】字节跳动巨量星图/TCM团队招聘前端工程师


关于我们


字节商业化旗下巨量星图前端团队,依托抖音/TT丰富的达人生态及产品能力,高效连接创作人与广告主,激发优质创作的营销价值,三年时间从小项目成长为集团独立业务,我们期待你的加入!
如何帮助创作者更好的变现?_哔哩哔哩_bilibili
关于团队/业务更多详细介绍可以参考 前端的天堂 or 加班的地狱?说说我在字节的这一年


如果你

  • 崇尚自由,想和优秀的人一起做优秀的事;
  • 热爱前端,热爱技术,追求极致;
  • 有自驱力,有责任心;
  • 王者荣耀玩家,团队人均王者段位,快来和我们一起上分!

欢迎加入我们,我们大量招聘前端工程师,社招/校招/实习生不限,能力层级不限,工作地点上海/北京/杭州/山景城都行!
期待那个优秀的你加入,大星图团队!


问题咨询

如果想了解更多关于业务、团队和岗位的信息,可以加寸志(过气网红)微信:island205,邮件:cunzhi@bytedance.com,和 TL 直接沟通!


简历直投

记得备注定向大星图前端团队哦!


前端开发工程师(上海)- 创意与生态
job.toutiao.com/s/NSMLh2N
星图平台前端开发工程师(杭州)- 创意与生态
job.toutiao.com/s/NSMYNmm
Frontend Engineer, TikTok Ads Creative & Ecosystem(山景城)
job.toutiao.com/s/NS6KNHt
Frontend Engineer, TikTok Creator Marketplace(洛杉矶)
job.toutiao.com/s/NSMjkrH
前端开发实习生(上海/可转正)- 创意与生态
job.toutiao.com/s/NS65GKU
前端开发实习生(杭州/可转正)- 创意与生态
job.toutiao.com/s/NS6K9n2

编辑于 2022-04-11 11:23