詹姆斯·凯尔于 2016 年 11 月 24 日发布 James Kyle
Yarn 是我们建立的一个新的包管理器,旨在保持一致和可靠。在从互联网上安装数百或数千个第三方包时,您希望确保在每个系统中执行相同的代码。
Yarn 以两种关键方式在各台机器上保持一致性
- Yarn 使用一个确定性算法,可在将文件放置到需要放置的位置之前构建整个依赖项树。
- 安装过程中的重要信息存储在
yarn.lock
锁文件中,以便在每个安装依赖项的系统之间共享。
此锁文件包含已安装的每个依赖项的准确版本以及代码的校验和,以确保代码完全相同。
Yarn 需要每个依赖项的这些信息,因为包在不断发生变化。新版本的包总是在发布,而且由于 package.json
指定版本范围,因此您需要将它们锁定到单个版本。
这些版本范围存在,是因为 Yarn 遵循 语义化版本控制,或“语义版本”。语义版本是围绕“重大”或“非重大”更改而设计的版本控制系统。
当您有诸如 v1.2.3
之类的版本时,它会分为三个部分
- 主版本 (1.x.x) – 可能导致用户代码中断的更改
- 次要版本 (x.2.x) – 添加新功能的更改(但不应当中断用户代码)
- 修订版本 (x.x.3) – 修复先前版本中的错误的更改(但不添加新功能且不应当中断用户代码)
当某个软件包发布新版本时,作者会根据他们为传达所做的更改而提高主要版本、次要版本或修订版本。
软件包的用户通常应当欢迎次要版本和修订版本,但应当注意主要版本,因为它们可能会中断您的代码。
版本范围是一种指定希望接受的更改类型和希望阻止的版本的方法。然后,这些版本范围解析为单个版本,该版本可能是您已安装的版本或与您的版本范围匹配的最新已发布版本。
如果您不存储最终安装的版本,有人可能会安装相同的依赖项集,并根据他们安装的时间而得到不同的版本。这可能导致“在我的机器上有效”的问题,应当避免。
此外,由于软件包作者是人,他们可能会犯错,他们有可能在次要版本或修订版本中发布意外的重大更改。如果您在无意中安装此重大更改,则可能会产生不良后果,例如在生产环境中中断您的应用程序。
锁文件能针对您已安装的每个依赖项锁定版本。这可以避免“在我的机器上有效”的问题,并确保您不会意外获得一个不良依赖项。
它还作为一种安全预防措施:如果包作者是恶意或被恶意攻击,然后发布了不良版本,则您不希望此代码在您不知情的情况下最终运行。
库与应用程序
有两种主要类型的项目使用 Yarn
- 库 – 作为注册表中的软件包发布并且由用户安装的项目。(例如 React 或 Babel)
- 应用程序 – 仅使用其他软件包且通常构建某种产品的项目。(例如您公司的应用程序)
对于应用而言,大多数开发者都同意,锁定文件是好主意。但对于在构建库的时候使用它们是否合适,还存在一些疑问。
当你发布一个包含 yarn.lock
的软件包时,该库的任何用户都不会受到其影响。当你安装应用程序或库中的依赖项时,只会引用你自己的 yarn.lock
文件。依赖项内的锁定文件将被忽略。
出于两个原因,Yarn 以这种方式进行非常重要
- 你将永远无法更新子依赖项的版本,因为它们会被其他
yarn.lock
文件锁定。 - Yarn 将永远无法合并(重复删除)依赖项,以使兼容版本范围仅安装一个版本。
有些人很疑惑,如果锁定文件不能并且不应影响用户,那么为什么库应该使用锁定文件。更进一步来说,有些人认为在开发库时使用锁定文件会产生一种虚假的安全感,因为你的用户可能正在安装与你不同的版本。
从逻辑上讲,这似乎很有道理,但让我们深入了解这个问题。
开发依赖项
到目前为止,我们一直在讨论依赖项,好像只有一种类型的依赖项,但实际上有几种不同的类型。这些被分成两类
- 运行时——由项目的代码使用的依赖项,并在运行代码时需要。
- 开发——仅在直接处理项目时才需要的依赖项
当用户安装库时,只会安装运行时依赖项。只有在直接处理指定它们的项目时才会安装开发依赖项。
每种类型的依赖项都会创建一棵依赖项树,这些树是使用它们所必需的。
事实证明,大多数项目(库或应用程序)的开发依赖项远多于运行时依赖项。由项目的开发依赖项创建的依赖项树几乎总是总体的最大部分。
你可以将此归咎于 JavaScript 开发令人沮丧地复杂,但它似乎在每个生态系统中都是如此。你几乎总是需要更多代码来开发项目,而不是运行它们所需的代码。
在处理库时,开发依赖项更有可能中断,因为有可能中断的依赖项更多。
中断更改竞争
当一个软件包意外发布中断性修改时,它会开始计时,看谁会第一个发现它。最先安装中断性修改的人(很可能)会第一个发现它。
让我们想象我们有一个名为 left-pad
的软件包,它接受一个字符串,并在其前面添加指定数量的填充。这个小巧无害的软件包被一个真正的大项目使用,我们姑且称之为……Babel。
有一天,left-pad
的维护者决定做一件非常邪恶的事情,并填充其右侧。他们将其作为补丁版本发布,并迅速传遍了所有使用它的人。
请记住,Babel 是一个真正的大型项目,拥有成千上万的用户和数十位贡献者。让我们尝试找出谁将首先捕获填充错误。
- 查看构建历史,Babel 在 CI 中(带有
left-pad
依赖项)在过去 30 天内确切地安装了 103 次。 - 从存储库的统计数据来看,Babel 在相同的 30 天内被用户安装了 550 万次(带有
left-pad
)。
用不同的测量方式来说
- 平均每 7 小时,贡献者就会安装 Babel
- 平均每 0.5 秒,用户就会安装 Babel
在 left-pad
事件发生时,用户几乎立即就发现了。通过 GitHub 问题、推文、Facebook 消息、电子邮件、电话、烟雾信号或信鸽通知 Babel 贡献者。
Babel 贡献者不可能阻止用户受到每个错误的影响。为此,他们需要每隔 0.5 秒就能运行 CI,注意到错误,找到修复程序,发布新版本,并让每个用户升级。这样做根本不可能。
这看起来可能极端得无法广泛应用,但道理是一样的,只是数字不同。用户总是需要比贡献者更频繁地安装库。如果不是这样,则说明根本没人使用该库——我们不应该围绕没人使用的代码优化工作流。
用户测试
许多库开发人员非常努力地维护一个具有 100% 代码覆盖率的测试套件。此覆盖率可确保在测试期间至少运行一次每一行代码。
但这仍然无法发现所有问题。如果一个库只有少数用户,那么用户会比贡献者提供的任何测试套件对其进行更好的测试。
用户只是编写更多代码,并且他们编写的代码并不总是库作者所期望的。库越流行,用户发现的边缘情况就越多。用户会做一些你甚至认为不可能的事情——这会让你惊恐。这会让你质疑人性的善良。
即使贡献者比用户抢先发现了重大更改,他们也有很大的可能不会发现它。未经测试的边缘情况最有可能损坏。
贡献者的负担
现在让我们来谈谈在库中不使用锁定文件的一些社会影响。
如前所述,库依赖项中发生的重大更改大部分是开发依赖项。
其中一定百分比的重大更改将被常规贡献者发现并(希望)修复。但是,剩余的重大更改将被新的或不频繁的贡献者发现。
对于那些不是开源老手的人来说,首次为某个项目做出贡献是一件非常令人生畏的事情。如果潜在的新贡献者遇到的第一件事是构建或测试套件损坏的情况,他们可能会因此而望而却步,决定放弃回馈。
即使是向许多开源项目做出贡献的人,当他们只想修复漏洞或向库中添加一个小功能时,也不得不调试构建系统几个小时,这是一件可怕的事情。
用户的负担
当然,并非所有重大更改都是开发依赖项,而且从理论上讲贡献者可以在用户注意到之前发现重大更改。在那种(狭窄)情况下,我们不是把负担从贡献者转嫁到了用户身上吗?
首先,受此影响的不会是每个用户。我们很明确地认为应用程序应该使用锁定文件,如果它们使用锁定文件,那么它们就不会受到突然的重大更改的影响。
我们只关注第一次安装软件包或正在升级依赖项版本的用户。
对于升级其依赖项的用户,应该对他们进行培训,以注意重大更改,如果他们遇到重大更改,则回滚版本至正常状态并在库中打开问题。
我们添加的唯一真正糟糕的体验是针对新用户,这是公认的糟糕体验。你希望新用户获得最佳的体验。
但请记住,在大多数情况下,他们已经应对过这种负担。只有当提交者在用户遇到崩溃变化前识别出来时,我们才将额外的负担加在新用户身上。
同样重要的一点是,新用户只占总安装数量的一小部分。大多数时间里,首先识别出崩溃变化的都是现有用户,因为他们安装库的次数最多。
最后
对于 Yarn,每个人都应该遵循一个简单的通用规则:如果你在安装一个新依赖项或升级现有依赖项,则你应该检查以确保该软件包按预期工作,并且不会破坏你的代码。
无论你的库做什么,你都应该遵循该规则。
如果不使用锁定文件,情况会变得更加复杂:在应用或库中,如果没有锁定文件,则你每次安装或重新安装依赖项时都必须检查它们并确保所有内容仍然正常。否则,构建可能会中断,或测试可能会失败。你可能会在不自知的情况下破坏一些内容。你可能会遇到只在你自己的计算机上有效而对其他人无效的代码情况。
认为在库中不使用锁定文件可以在某种程度上防止用户遇到崩溃变化的想法,最好的情况是极少见的情况,而最坏的情况则是永远不会发生。
通过不在库中使用锁定文件,你唯一能实现的实际效果就是让项目更难被人提交。
我们应该作为一个社区来保持我们所有库的最新状态。我们应该去构建一些自动升级依赖项的工具,以便无痛操作。此前已经尝试过此类工具,但我们还可以做得更好。
请提交你的 yarn.lock
文件。