一张图概括前端包管理器历史:
前端包管理器除了npm、yarn、pnpm之外,还有bower、jspm等一些过时的包管理器,这里不再介绍。本编文章概述和梳理npm、yarn的历史。
注:以下内容按时间顺序
npm v1&v2
2011年5月npm v1.0发布
2014年9月npm v2.0发布
这篇文章详细讲了npm v1/v2/v3的工作原理
不足:
请看上篇文章
npm v3
2015年6月npm发布了v3。npm v3完全重写了npm的安装程序。
一些大的改变:
- peerDependencies 不再导致隐式安装任何东西。相反,如果缺少包 peerDependencies,npm 现在会发出警告。
- engineStrict
- npm view
新特性
install
和postinstall
生命周期脚本现在仅在安装了所有具有脚本依赖项的模块后执行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
npm v5 于 2017 年 5 月 26 日发布。npm@5 使 npm 向前迈进了一大步,在几乎所有常见情况下 显着提高了其性能,修复了由于架构而导致的一系列旧错误,代码更加健壮。并使搭建 monorepos 仓库更轻松,包的安装更加一致性及安全(增加了 package-lock.json)。
简而言之:
- 因为有了 package-lock.json,所以保障了安装包的一致性
- 性能得到了很大的提升
- 安装包目录现在最终会创建一个符号链接,并保存到 package-lock.json
- 重写了 Cache。(比较重要的部分,这里就不再赘述,感兴趣可移步至 changelog 探究)
- 重写后的 Cache 有很强的容错性并支持并发访问
- 对离线模式及缓存安装进行了优化
npm v6
发布于 2018 年 5 月 4 日。无重大变更。主要是一些优化及 BUG 修复。
npm v7
于 2020 年 10 月 13 日发布。无重大变更。做了一些优化及 BUG 修复。支持workspaces概念,作为 npm cli 的一组功能。
npm v8
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 支持的子依赖项
Yarn v1.x
2017 年 9 月 yarn1.0 发布,官方介绍文章
开篇介绍了 yarn 发布之后在社区中受欢迎的程度及取得的成就。
然后 1.0 版本带来了新特性。
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),更加保证依赖的可靠性。因为不再需要安装,构建速度也得到更大的提升
PS:pnp
模式同时解决了幻影依赖(Phantom dependencies)的问题,且更加严格。
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
相关文章: