JavaScript 包管理器简史(npm/yarn/pnpm)

前端那些趣事

共 9131字,需浏览 19分钟

 · 2022-01-16

针对现如今比较流行的三个 JavaScript 包管理器:npm、yarn、pnpm,做一次简短的概述和梳理。JavaScript Package Manager History Timeline

JS 的包管理器,除了咱们接下来要讲的 npm/yarn/pnpm 外,历史上还有过诸如 bower、jspm 等一些“过时”的包管理器,这里就不再赘述。

以下顺序以编年体的方式进行。

npm v1/v2

npm 1.0: Released

npm@2.0.0

npm 从最早期到 v7.3.0 之间的博客文章。之后的版本变更可以到 github 对应的版本 Changelog 上查找

不足

  • 依赖包嵌套的问题,导致 node_modules 体积越来越大

  • 语义话版本带来的依赖不确定的问题(这时还没有lockfile/npm-shrinkwrap.json 文件)

  • npm cache 功能不足,无离线模式

npm v3

2015 年 6 月 26 日 npm3 发布,v3.0.0 Changelog

npm3 完全重写了 npm 的安装程序。

一些大的改变

  • peerDependencies 不再导致隐式安装任何东西。相反,如果缺少包 peerDependencies,npm 现在会发出警告。

  • engineStrict

  • npm view

新特性

  • installpostinstall 生命周期脚本现在仅在安装了所有具有脚本依赖项的模块后执行

  • npm-shrinkwrap.json确保你在其中指定的模块完全按照指定的内容安装

  • 扁平化 node_modules

Yarn v0.x

2016 年 6 月 18 日,yarn 正式在 github 上提交代码,初始版本为 0.2.0 ,当时名字叫kpm(fbkpm)。

2016 年 10 月 11 日正式公开发行。Facebook 官方在 10 月 11 日发布的这篇文章(此时 yarn 已经稳定),介绍了 yarn 出现的缘由和特点:Yarn: A new package manager for JavaScript

文章开篇就说了,在使用 npm 的过程中,他们遇到了:一致性、安全性、离线安装和性能方面的问题。

接着介绍了 JavaScript 包管理在 Facebook 的演变,经过一系列对 npm 扩展的尝试和骚操作,最终 facebook 的工程师们想,是不是搞一个新的包管理器呢?在和社区的其他工程师的交流中,他们得知,大家面临很多一样的问题。于是乎在和 Exponent、Google 和 Tilde 等社区内工程师的合作下,诞生了Yarn。

Yarn 的特点

lockfiles(yarn.lock)

Yarn 通过使用 lockfiles 确定性和可靠的安装算法解决了这些关于版本控制和非确定性的问题。这些锁定文件将已安装的依赖项锁定到特定版本,并确保每次安装都会在所有机器上的 node_modules 中产生完全相同的文件结构。

安装过程分为三个步骤:

  • Resolution(解析):Yarn 通过向注册表发出请求并递归查找每个依赖项来开始解析依赖项。

  • Fetching:接下来,Yarn 在全局缓存目录中查找所需的包是否已经下载。如果没有,Yarn 会获取包的 tarball 并将其放在全局缓存中,这样它就可以脱机工作并且不需要多次下载依赖项。依赖项也可以作为 tarball 放置在源代码管理中,用于完全离线安装。

  • Linking:最后,Yarn 通过将全局缓存中所需的所有文件复制到本地 node_modules 目录中来将所有内容链接在一起。

所以通过干净地拆分以上这些步骤并获得确定性结果,Yarn 能够并行化操作,从而最大限度地提高资源利用率并使安装过程更快。

其他特点

除了使安装更快、更可靠之外,Yarn 还具有其他功能以进一步简化依赖项管理工作流程。

  • 与 npm 和 bower 工作流程兼容,并支持混合注册表。
  • 能够限制已安装模块的许可证以及输出许可证信息的方法。
  • 公开稳定的公共 JS API,并通过构建工具将日志抽象化以供使用。
  • 更加可读性、最小的、漂亮的 CLI 输出。

Yarn 要不要支持 symlinks(符号链接) 的讨论

2016 年 11 月 10 日,一个大佬开了一个 issue:Looking for brilliant yarn member who has first-hand knowledge of prior issues with symlinking modules。

问:「听@thejameskyle 说,在开源之前,Yarn 曾经做过 symlinks(符号链接)。然而,它破坏了太多的生态系统而不能被认为是一个有效的选择。」如果您是 yarn 成员,对实际损坏的内容有第一手消息,并且可以从技术上解释原因,我恳请您回答这个问题。

然后@sebmck 回答了他的问题:说 symlinks(符号链接),对现有的生态系统不能很好地兼容。

总结了几点:

符号链接的优点:

  • 稍微快一点的包安装。
  • 更少的磁盘使用

存在的缺点:

  • 操作系统差异
  • 通过添加多种安装模式来降低 Yarn 的确定性
  • 文件观察不兼容
  • 现有工具兼容性差
  • 由循环引起的递归错误,等等...

这个 issue,讨论了很长的篇幅,这里就不继续了,感兴趣的可以去观摩一下。最后显然 yarn 的团队暂时不打算支持使用符号链接。

npm 喊话 Yarn

yarn 发布后,npm 官方博客当天(2016 年 10 月 11 日)发表了一篇Hello, Yarn!

恭喜了 yarn 的开源。并对 yarn 团队及 facebook 为社区及整个 npm 生态做出的贡献给予了很高的评价。

npm v4

随后的 2016 年 10 月 21 日 npm 4 发布,并没有什么重大的变化。

做了以下更改:

  • npm search 重写为流结果,不再支持排序。
  • npm scripts 不再在运行脚本之前预先添加用于运行 npm 的 node 可执行文件的路径。添加 --scripts-prepend-node-path 选项来配置此行为。
  • npt 已被删除。
  • prepublish 已被弃用,取而代之的是 prepare。临时添加了一个 prepublishOnly 脚本,它只会在 npm publish 上运行。
  • 如果 npm outdated 发现任何过时的包,它会以退出代码 1 退出。
  • npm tag 已在弃用周期后删除。使用 npm dist-tag
  • 不再支持部分 shrinkwraps。除了 devDependencies, npm-shrinkwrap.json 被认为是一个完整的安装清单。
  • npm 的默认 git 分支不再是 master 分支。从现在开始,我们将使用最新的

npm v5/v6/v7/v8

从 v5-v8 无特别突破性的重大变更,所以此段我们放在一起讲

npm v5 于 2017 年 5 月 26 日发布。npm@5 使 npm 向前迈进了一大步,在几乎所有常见情况下 显着提高了其性能,修复了由于架构而导致的一系列旧错误,代码更加健壮。并使搭建 monorepos 仓库更轻松,包的安装更加一致性及安全(增加了 package-lock.json)。

简而言之:

  • 因为有了 package-lock.json,所以保障了安装包的一致性
  • 性能得到了很大的提升
  • 安装包目录现在最终会创建一个符号链接,并保存到 package lock
  • 重写了 Cache。(比较重要的部分,这里就不再赘述,感兴趣可移步至 changelog 探究)
  • 重写后的 Cache 有很强的容错性并支持并发访问
  • 对离线模式及缓存安装进行了优化

npm v6

v6.0.0 Changelog

发布于 2018 年 5 月 4 日。无重大变更。主要是一些优化及 BUG 修复。

npm v7

v7.0.0 Changelog

于 2020 年 10 月 13 日发布。无重大变更。做了一些优化及 BUG 修复。支持workspaces概念,作为 npm cli 的一组功能。

npm v8

v8.0.0 Changelog

2021 年 10 月 7 日发布了 v8.0.0,此版本的目的是放弃对旧 node 版本的支持和 删除对 require('npm') 的支持。没有其他突破性变化。

BREAKING CHANGES

删除对 node 10 和 11 的支持 将 node 12 和 14 中的支持上限提高到 LTS (^12.13.0/^14.15.0) 删除对 require('npm') 的支持 更新也删除 node10 支持的子依赖项

pnpm

2016 年 10 月 19 日发布的v0.42.0版本,增加了扁平树结构的支持。使用 yaml 格式存储 store 信息。

pnpm 的优势

2017 年 3 月 19 日 pnpm 作者发表了一篇Why should we use pnpm?来阐述了 pnpm 的优势。

主要是采用硬连接和软连接的方式,提高了安装速度、节约了磁盘空间、避免了“幽灵依赖”。而且 yarn 支持的:安全、离线模式、更快的速度,pnpm 都支持,而且速度还要更快。

pnpm@1.0 于2017 年 6 月 28 日 发布

v1.0.0 Changelog

pnpm version 1 is out!

初衷

节约磁盘空间并提升安装速度
  • pnpm 创建从全局存储到项目下 node_modules 文件夹的硬链接,从而提高安装速度并节约磁盘空间

  • 解决依赖分身的问题,节省磁盘空间

  • 创建非扁平化的 node_modules 文件夹

  • 基于符号链接的 node_modules 结构,解决“幽灵依赖”的问题。(Symbolic link(符号链接/软链))

官网的项目初衷有更详细的讲解

PNPM 的局限

  • npm-shrinkwrap.jsonpackage-lock.json 被忽略。与 pnpm 不同,npm 可以多次安装相同的 name@version ,并且具有不同的依赖项组合。npm 的锁文件旨在反映平铺的 node_modules 布局,但是,由于 pnpm 无法创建类似的布局,因此它无法遵循 npm 的锁文件格式。但是,如果您希望将锁定文件转换为 pnpm 的格式,请看 pnpm import。
  • pnpm 不能发布带有 bundledDependencies 的 npm 包。目前,也没有计划添加对 bundledDependencies 支持,因为它们在发布中的工作效率不高,即使在 npm 上也是如此。相反,我们建议您使用一个实际的打包器,例如 webpack、rollup 或 ESBuild。
  • Binstubs(在 node_modules/.bin中的文件)总是 shell 文件,而不是指向 JS 文件的符号链接。创建 shell 文件是为了帮助支持插件的 CLI 的程序在特殊的 node_modules 结构中能够正确地找到它们的插件。这是很少有的问题,如果您希望文件是 JS 文件,请直接引用原始文件,如 #736 所示。
  • Node 的 --preserve-symlinks 标志在使用 pnpm 的项目中执行时,无法正常工作。

版本更迭

随着 pnpm 的版本更迭,现在已经到了 v6.x 版本。历代的版本更新,其刚开始的核心思想与初衷并没有发生变化。并不断的进行 bug 的修复、性能的优化、新特性/命令的增加以及解决包生态的兼容性问题。

Yarn v1.x

Yarn Workspaces

workspaces 对 monorepo 仓库更加友好。它可以让人们自动聚合来自多个 package.json 文件的所有依赖项,并一次性安装它们。它还在根目录使用单个 yarn.lock 文件,将它们全部锁定。此外,Yarn 将在碰巧相互依赖的所有工作区之间创建符号链接,以便最终在所有项目中始终使用最新的代码。

通过防止大型项目的较小部分之间的包重复来实现更快、更轻的安装。

同时还提到了 lerna,一个流行的单一存储库版本管理工具。

Auto-merging of lockfiles

自动合并yarn.lock文件的冲突

Selective version resolutions

Yarn 现在允许在项目的 package.json 文件中定义一个 resolutions 字段,该字段指示 Yarn 使用某些子依赖项的特定版本,而不管其依赖项设置的原始模式如何

yarn 2.x

2020 年 1 月 25 日,yarn 2.0.0 发布,带来了许多大的变动和特性

为什么要开发 v2 版本

  • Yarn 的代码变得越来越难维护和扩展。由于这个技术原因,Yarn 需要一个更加现代化的代码架构来满足新需求的开发。
  • 欢迎社区开发者贡献代码。采用基于插件(Plugin)的模块化(Modular)代码架构,让开发者不用搞懂 Yarn 的核心代码就可以通过实现插件的方式来为 Yarn 添加新的功能。

新特性

由于官方介绍篇幅过长,这里只列出重点,更详细介绍,移步官方介绍文章

  • 可读性更高的 CLI 输出
  • 更好的 workspaces 支持
  • New Command: yarn dlx(与 npx 类似)

Plug'n'Play/pnp

重点介绍下Plug'n'Play

零安装 (Zero-Installs),更加保证依赖的可靠性。因为不再需要安装,构建速度也得到更大的提升

node_modules 的问题

过去的安装方式很简单:运行 yarn install Yarn 会生成一个 node_modules 目录,由于其内置的 node 解析算法,Node 可以使用该目录。在这种情况下,Node 不必首先了解什么是“包”:它只根据文件进行推理。“这个文件在这里存在吗?不在的话,让我们看看父 node_modules。它在这里存在吗?仍然不在的话,继续......”,它一直运行,直到找到正确的。由于以下几个原因,此过程非常低效:

node_modules 目录通常包含大量文件。生成它们通常要耗费 yarn install 所需时间的 70% 以上。即使有预先存在的安装也不能完全的解决,因为包管理器仍然必须将 node_modules 的内容与其应该包含的内容进行区分。

因为 node_modules 生成是一个 I/O-heavy 操作,包管理器没有太多余地来优化它,而不仅仅是做一个简单的文件复制——即使它可能使用硬链接或写时复制,它在进行一堆系统调用来操作磁盘之前,仍然需要区分文件系统的当前状态。

因为 Node 没有包的概念,它也不知道文件是否要被访问。完全有可能您编写的代码一天在开发环境中正常,但后来在生产中却损坏了,因为你忘记在 package.json 中列出你的依赖项之一。

即使在运行时,Node 解析也必须进行一堆 stat 和 readdir 调用,以确定从哪里加载每个所需的文件。这是非常浪费的,这也是启动 Node 应用程序需要花费如此多时间的部分原因。

最后, node_modules 文件夹的设计是不切实际的,因为它不允许包管理器正确地删除重复的包。即使可以使用一些算法来优化树布局(提升),我们仍然无法优化某些特定模式 - 不仅导致磁盘使用率高于所需,而且某些包在内存中多次实例化。

解决 node_modules 所存在的问题

Yarn 已经知道关于你的依赖树的一切——它甚至为你将它安装在磁盘上。那么,为什么要由 Node 来查找您的包在哪里呢?相反,包管理器的工作应该是通知解释器,包在磁盘上的位置并管理包之间甚至包版本之间的任何依赖关系。这就是创建 Plug'n'Play 的原因。

在这种安装模式下(从 Yarn 2.0 开始的默认设置),Yarn 生成单个 .pnp.cjs 文件,而不是通常包含各种包副本的 node_modules 文件夹。.pnp.cjs 文件包含各种映射:一个将包名称和版本链接到它们在磁盘上的位置,另一个将包名称和版本链接到它们的依赖项列表。有了这些查找表,Yarn 可以立即告诉 Node 在哪里可以找到它需要访问的任何包,只要它们是依赖树的一部分,并且只要这个文件在你的环境中加载。

这种方法有多种好处:

安装现在几乎是即时的。Yarn 只需要生成一个文本文件(而不是潜在的数万个)。主要瓶颈成为项目中依赖项的数量而不是磁盘性能。

由于减少了 I/O 操作,安装更加稳定和可靠。尤其是在 Windows 上(批量写入和删除文件可能会触发与 Windows Defender 和类似工具的各种意外交互),I/O 繁重的 node_modules 操作更容易失败。

依赖树的完美优化(又名完美提升)和可预测的包实例化。

生成的 .pnp.cjs 文件可以作为零安装工作的一部分提交到您的存储库,首先无需运行 yarn install。

更快的应用程序启动!Node 解析不必像以前一样遍历文件系统层次结构(而且很快就根本不需要这样做了!)。

pnpMode:strict/loose

pnpMode 为”strict”(解决幽灵依赖的问题),这种情况下所有用到的依赖都必须显式地声明在 package.json 中,也就是说如果你的模块声明了 webpack 作为依赖,webpack 中声明了 acorn 作为依赖,在”strict”模式下你的模块不能直接引入 acron,否则会报错。而”loose”模式下是允许的,但却不是推荐用法。

nodeLinker: pnp/node-modules

nodeLinker 设置为”node-modules”就和 Yarn 1 和 npm 的方式安装依赖没什么区别了,所有依赖仍然存在于 node_moduels 目录下,这些情况下也不会生成.pnp.js 文件,因为不需要从 zip 包中解析依赖了。

兼容性

请注意,相关的 CLI 工具,需要兼容 pnp 模式,因为前端库(例如 react、vue、lodash 等)不会重新实现 Node 解析,因此不需要任何特殊逻辑来利用 pnp。

诸如 Babel、webpack4.x、ESlint、VScode 等需要用到 node 解析的工具或者编辑器,需要结合相关 pnp 生态的插件才能正常工作。

无疑Plug'n'Play是 v2 最重大的一个新特性。当然新的架构对开源社区更加友好,势必让 yarn 在往后的发展更加迅猛。

Yarn v3

2021 年 8 月 6 日 Yarn v3 发布,老规矩——官方发了一篇文章来介绍它。

重大更改主要是对一些小细节的优化

  • 不再支持 Node 10
  • Plug'n'Play hooks 是 .pnp.cjs (vs .pnp.js)
  • 虚拟文件夹现在是 virtual (vs $$virtual)
  • editor SDKs 移至 @yarnpkg/sdks
  • 支持 node 的exports字段

性能

经过了各种调整以后,解决了 Yarn 中一些大的资源消耗的问题。安装速度得到了改进(在某些情况下 比 pnpm 更快)。而且脚本的执行往往具有自然开销,由于 2.4 及更早版本中存在错误,导致随着项目的增加开销逐渐增加。已经解决了这种情况,现在开销应该是恒定的。

New node_modules linkers(新的node_modules链接器)

Yarn 是围绕几个接口构建的。其中之一称为“链接器”,它告诉 Yarn 如何在磁盘上安装软件包。这就是我们如何做到在不更改太多代码的情况下支持 PnP 和 node_modules 安装的原因。

这种架构的一个优点是它允许我们如何有效地迭代替代安装策略。对于此版本,larixer 实现了一个新的实验性 nmMode 设置,可用于指示链接器使用特定的复制方案:

  • hardlinks-local:当在同一个项目中多次发现使用同一个包时,将使用硬链接(但前提是它们具有完全相同的版本)。
  • hardlinks-global:将在相同的文件上使用硬链接(甚至跨不同版本!),但也会使它们指向全局内容可寻址目录。这类似于 pnpm 所做的。请注意,如果缓存损坏(例如因为您手动编辑它),Yarn 将在后续安装时自动修复它。

我自己一直在尝试 pnpm 风格的链接器。它尚未发布,因为可能会导致维护起来的复杂性增加,所以持谨慎态度。(v3.1.0 中已支持 pnpm 风格的链接器)

其他改进

  • 改进脚本的 shell 支持
  • ESBuild 支持

我们现在使用 ESBuild 来生成 Yarn 包,并因此努力确保与 Plug'n'Play 安装的良好兼容性。增加新的@yarnpkg/esbuild-plugin-pnp包。速度提升 6 倍

  • 将命令行框架升级到 Clipanion 3

即将到来的

  • 核心包(corepack)集成
  • PnP 模式下的 ESM 支持
  • 内置 CLI 完成
  • 变更日志生成
  • 改进性能
  • pnpm 风格的链接器,等等

结语

对于这三个包管理器的简单梳理告一段落。纵观发展,大家都是在互相学习和优化,为 JavaScript 社区的发展注入了活力,感谢开源精神。

npm 作为 node 老师的亲传弟子,随总有人诟病,但影响力依然不减。对于后起之秀的新特性,为保住江湖地位,也不得不跟上小老弟们的步伐。

Yarn 就像一个学贯中西的绝顶高手,在精研 npm 的传统招式的同时,又自研独门绝技,同时又吸收新时代新理念。“全都要”的架势,咄咄逼人。

pnpm 一招硬连接、软连接独步天下。比 Yarn 还快的速度,给足了压力。敢于开拓的年轻人,未来可期。

JavaScript 包管理器们的故事还在继续!

本文转自:https://zhuanlan.zhihu.com/p/451025256 作者:reahink


请你喝杯🍵 记得三连哦~

1.阅读完记得给🌲 酱点个赞哦,有👍 有动力

2.关注公众号前端那些趣事,陪你聊聊前端的趣事

3.文章收录在Github frontendThings 感谢Star✨

浏览 38
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报