重要信息:本文档介绍了 Yarn 1(经典版)。
有关 Yarn 2+ 文档和迁移指南,请参阅 yarnpkg.com。

依赖管理正确方式

2018 年 4 月 18 日发布,作者 Maël Nison

假设我们要编写一个 React 插件。由于我们需要 require react 软件包,因此我们将其添加到依赖项中,如下所示

{
  "name": "my-awesome-plugin",
  "dependencies": {
    "react": "^16.0.0"
  }
}

然后,我们运行 yarn install,一切都工作正常,我们很高兴,将软件包发布到万维网上,然后...

有人尝试安装它时,却产生错误。太不酷了。

我们开始收到来自用户报告称,其依赖项树中 React 出现多次 - 一次作为其项目的顶级依赖项,另一次作为我们的插件的依赖项。这肯定不对!由于这两个 package.json 文件列出了 React 的兼容版本,因此软件包管理器肯定应该将它们合并为一个,对不对?事实证明,没这么简单。让我们看看发生了什么事,以及如何解决这个问题!

如果您只对解决方案感兴趣,而不想知道为什么它是这样工作的,请移至最后一段!否则,请继续阅读!

语义故事

请看,package.json 中的字段都有含义。这个不新鲜。但是它们确切含义是什么?它们的语义是什么?借用 C/C++ 词汇表,哪些行为是规范定义的,哪些行为是 实现定义的,或者更糟的是 未定义行为?为了回答这个问题,让我们用通俗易懂的英语来解释这些字段及其保证

  • dependencies 对象保证,对于每个条目,您的软件包都将能够通过 require() 访问指定版本的依赖项。它还保证,来自这些依赖项的所有已导出的 bin 将可用于您的脚本(在运行 yarn run <script name> 时)。

  • devDependencies 对象保证,对于每个条目,您的软件包都将能够通过 require() 访问指定版本的依赖项,前提是您的软件包位于依赖项树的顶部,并且安装尚未在生产模式中运行(--production)。

  • peerDependencies 对象保证,对于每个条目,任何需要您的软件包的软件包都将被要求提供本文档中列出的依赖项副本,理想情况下该版本与您请求的版本匹配。它还保证您将能够通过 require() 访问此依赖项,最后 它还保证 require() 的返回版本和实例与如果您的父级自行 require() 它所获得的版本和实例完全相同

这就是关键所在。当您指定一个依赖项时,软件包管理器无需从依赖项树中的任何地方为您提供完全相同的版本!这样做是一种优化,虽然我们尽力从树中删除重复的软件包,但并非总是可行或可能。由于我们无法保证这一点,您不应依赖于我们这样做。根据规范,如果我们认为可能产生更好的结果,我们允许从一个版本更改为另一个版本的行为。从技术上讲,我们甚至可以完全禁用提升!

为什么不能保证?第一个原因是提升包并不总是可行的。考虑以下情况:Foo 和 Bar 都依赖 HelloWorld@1.0.0,而顶级包依赖 Foo、Bar 和 HelloWorld@2.0.0。由于 Node 解析的工作方式,Foo 和 Bar 的 HelloWorld 副本无法被提升(它们会与顶级包所需的版本冲突),这意味着它们 require() 调用的结果将会是不同的实例。

另一个原因是优化提升的不同事物——您是想让您的依赖关系树使用最新可能版本——这或许意味着一个更稳定的应用程序?还是您希望它针对大小进行优化,并且尽可能地合并所有包,即使这意味着不安装最新补丁?最终,您的包管理器通常采用启发式方法来做出决策,这有时可能导致接受与您的预期不同的折衷方案。

改善

既然我们知道使用常规的 dependencies 域并不会总是将我们的依赖关系与其他包使用的相似依赖关系合并,那么我们应该用什么代替?简短的答案是对等依赖关系

请看,对等依赖关系具有以下特定属性,以确保在此列出的包的依赖关系应该与依赖关系树中父包使用的依赖关系完全相同。更好的是,它们还保证您将始终获得确切相同的实例,即使我们禁用了提升!这就是它们存在的意义。

因此,无需先报告,以下是我们应该做的

{
  "name": "my-awesome-plugin",
  "peerDependencies": {
    "react": "^16.0.0"
  }
}

当然,稍有不便:如果我们的用户尚未将 react 添加到其自己的依赖关系中,他们现在需要这么做。如果他们不这么做,他们将收到警告,并且我们无法访问它。

不过,当谈到插件时,对等依赖关系总是更好!它们为我们的用户提供了完全控制权,让他们可以决定要使用哪种核心库版本(此处为 React),并且保证它们将与任何在此基础上添加的包(如我们的插件)始终如一地共享。React 将不再在依赖关系树中重复多次,从而减小捆绑大小,并避免失败的 instanceof 检查,因为对象来自 React 的不同实例。

一条简单规则

最后,以下规则说明您应该使用依赖关系还是对等依赖关系

  • 您是否正在附加到其他事物,如插件?如果是,请为其使用对等依赖关系。
  • 您的依赖关系是否是可以替换为由您自己实现的内容?如果是,那是依赖关系。