目录

包版本控制

pub 包管理器 帮助您处理版本控制。本指南解释了版本控制的历史和 pub 的处理方法。

将其视为高级信息。若要了解 pub 的设计原因,请继续阅读。如果您想使用 pub,请参阅其他文档

现代软件开发,特别是 Web 开发,严重依赖于大量使用现有的代码。这包括您过去编写的代码,也包括来自第三方的代码,从大型框架到小型实用程序库,应有尽有。一个应用程序依赖于数十个不同的包和库的情况并不罕见。

这种功能的强大程度不可低估。当您看到小型 Web 初创公司在几周内构建了一个拥有数百万用户的网站的故事时,他们能够实现这一目标的唯一原因是开源社区为他们提供了大量的软件。

但这并非免费的:代码重用存在挑战,特别是重用您不维护的代码。当您的应用程序使用由其他人开发的代码时,如果他们更改了代码怎么办?他们不想破坏您的应用程序,您当然也不想。我们通过版本控制来解决此问题。

一个名称和一个数字

#

当您依赖于外部代码时,您不会只说“我的应用程序使用widgets”。您会说,“我的应用程序使用widgets 2.0.5”。名称和版本号的组合唯一标识了一个不可变的代码块。更新widgets 的人可以随意进行更改,但他们承诺不会触碰任何已发布的版本。他们可以发布2.0.63.0.0,但这不会对您有任何影响,因为您使用的版本保持不变。

当您确实想获取这些更改时,您始终可以将应用程序指向widgets 的更新版本,并且您不必与这些开发人员协调即可进行操作。但是,这并没有完全解决问题。

本指南中讨论的版本号可能与包文件名中设置的版本号不同。它们可能包括-0-beta。这些符号不会影响依赖项解析。

解决共享依赖项

#

当您的依赖关系图实际上只是一个依赖时,依赖于特定版本的效果很好。如果您的应用程序依赖于一组包,而这些包又依赖于它们自己的依赖项,依此类推,那么只要这些依赖项不重叠,一切都将按预期工作。

请考虑以下示例

dependency graph

因此,您的应用程序使用widgetstemplates,而这两个都使用collection。这称为共享依赖项。现在,如果widgets 要使用collection 2.3.5,而templates 要使用collection 2.3.7,会发生什么?如果它们不同意版本怎么办?

未共享库(npm 方法)

#

一种选择是让应用程序同时使用两个版本的collection。它将具有两个不同版本的库副本,widgetstemplates 将分别获取它们想要的版本。

这就是npm 对 node.js 所做的操作。它对 Dart 有效吗?请考虑以下情况

  1. collection 定义了某个Dictionary 类。
  2. widgets 从其collection 副本(2.3.5)获取它的实例。然后将其传递给my_app
  3. my_app 将字典发送到templates
  4. 然后,templates 将其发送到其版本的collection2.3.7)。
  5. 接收它的方法为此对象有一个Dictionary 类型标注。

就 Dart 而言,collection 2.3.5collection 2.3.7 是完全不相关的库。如果您从一个库中获取Dictionary 类的实例,并将其传递给另一个库中的方法,则它将成为一个完全不同的Dictionary 类型。这意味着它将无法匹配接收库中的Dictionary 类型标注。糟糕。

由于此原因(以及尝试调试具有相同名称的不同版本内容的应用程序会带来的麻烦),我们认为 npm 的模型不适合 Dart。

版本锁定(死胡同方法)

#

相反,当您依赖于一个包时,您的应用程序仅使用该包的单个副本。当您有共享依赖项时,所有依赖于它的东西都必须就使用哪个版本达成一致。如果它们不一致,您将收到错误。

但这实际上并没有解决您的问题。当您确实收到该错误时,您需要能够解决它。因此,假设您在前面的示例中遇到了这种情况。您想使用widgetstemplates,但它们使用的是不同版本的collection。您该怎么办?

答案是尝试升级其中一个。templates 需要collection 2.3.7。您是否可以升级到与该版本兼容的widgets 的更新版本?

在很多情况下,答案将是“否”。从开发widgets 的人的角度来看。他们想发布一个带有对代码的新更改的新版本,并且他们希望尽可能多的人能够升级到它。如果他们坚持使用当前版本的collection,那么使用当前版本widgets 的任何人都可以放弃使用此新版本。

如果他们要升级collection 的依赖项,那么所有升级widgets 的人也都必须升级,无论他们是否愿意。这很痛苦,因此您最终会不愿升级依赖项。这称为版本锁定:每个人都希望向前移动他们的依赖项,但没有人能迈出第一步,因为这会迫使其他人也这样做。

版本约束(Dart 方法)

#

为了解决版本锁定,我们放宽了包对其依赖项施加的约束。如果widgetstemplates 都可以为collection 指定一个它们支持的版本范围,那么这将为我们提供足够的回旋余地,让我们将依赖项向前移动到更新的版本。只要它们范围之间存在重叠,我们仍然可以找到一个让它们都满意的版本。

这就是bundler 所采用的模型,也是 pub 的模型。当您在 pubspec 中添加依赖项时,您可以指定一个可接受的版本范围。如果widgets 的 pubspec 如下所示

yaml
dependencies:
  collection: '>=2.3.5 <2.4.0'

您可以选择版本2.3.7 作为collection的版本。一个具体的版本可以满足widgetstemplates包的约束条件。

语义版本

#

当您向您的包添加依赖项时,您有时可能希望指定允许的一系列版本。您如何知道要选择哪个范围?您需要向前兼容,因此理想情况下,该范围包含尚未发布的未来版本。但是,您如何知道您的包将与尚未存在的某个新版本一起使用呢?

为了解决这个问题,您需要就版本号的含义达成一致。假设您依赖的包的开发人员说:“如果我们进行任何向后不兼容的更改,我们承诺增加主版本号。” 如果您相信他们,那么如果您知道您的包与他们的2.3.5版本一起使用,您就可以依赖它一直使用到3.0.0版本。您可以将您的范围设置为

yaml
dependencies:
  collection: ^2.3.5

为了使此工作,我们需要提出这一套承诺。幸运的是,其他聪明人已经完成了这项工作,并将其命名为语义版本控制

它描述了版本号的格式,以及当您增加到更高版本号时的确切 API 行为差异。Pub 要求版本以这种方式格式化,并且为了与 pub 社区良好配合,您的包应该遵循它指定的语义。您应该假设您依赖的包也遵循它。(如果您发现它们没有,请告诉它们的作者!)

虽然语义版本控制不承诺在1.0.0之前的版本之间有任何兼容性,但 Dart 社区约定是将这些版本也视为语义版本。每个数字的解释只是向下移动一位:从0.1.20.2.0表示一个重大更改,到0.1.3表示一个新功能,到0.1.2+1表示一个不影响公共 API 的更改。为简单起见,避免在版本达到1.0.0后使用+

我们几乎拥有了处理版本控制和 API 演变所需的所有部分。让我们看看它们如何协同工作以及 pub 如何工作。

约束求解

#

当您定义您的包时,您会列出它的直接依赖项。这些是您的包使用的包。对于每个包,您都会指定您的包允许的版本范围。这些依赖包可能也有自己的依赖项。这些被称为传递依赖项。Pub 会遍历这些依赖项并构建您应用程序的整个依赖项图。

对于图中的每个包,pub 会查看所有依赖于它的包。它将所有版本约束收集在一起,并尝试同时解决它们。基本上,它会将它们的范围相交。然后,pub 会查看该包已发布的实际版本,并选择满足所有这些约束条件的最新版本。

例如,假设我们的依赖项图包含collection,并且三个包依赖于它。它们的版本约束是

>=1.7.0
^1.4.0
<1.9.0

collection的开发人员发布了这些版本

1.7.0
1.7.1
1.8.0
1.8.1
1.8.2
1.9.0

在所有这些范围内最适合的最高版本号是1.8.2,因此 pub 选择了它。这意味着您的应用程序以及您的应用程序使用的每个包都将使用collection 1.8.2

约束上下文

#

选择包版本会考虑每个依赖于它的包这一事实有一个重要的后果:将为包选择的具体版本是使用该包的应用程序的全局属性。

以下示例显示了这意味着什么。假设我们有两个应用程序。以下是它们的 pubspec

yaml
name: my_app
dependencies:
  widgets:
yaml
name: other_app
dependencies:
  widgets:
  collection: '<1.5.0'

它们都依赖于widgets,其 pubspec 是

yaml
name: widgets
dependencies:
  collection: '>=1.0.0 <2.0.0'

other_app包直接依赖于collection本身。有趣的是,它对它的版本约束与widgets不同。

这意味着您不能只查看widgets包来确定它将使用哪个版本的collection。它取决于上下文。在my_app中,widgets将使用collection 1.9.9。但在other_app中,widgets将因其他约束而被迫使用collection 1.4.9

这就是为什么每个应用程序都有自己的package_config.json文件:为每个包选择的具体版本取决于包含应用程序的整个依赖项图。

导出依赖项的约束求解

#

包作者必须谨慎定义包约束。考虑以下场景

dependency graph

bookshelf包依赖于widgetswidgets包当前为 1.2.0,通过export 'package:collection/collection.dart'导出collection,并且为 2.4.0。pubspec 文件如下

yaml
name: bookshelf
dependencies:
  widgets: ^1.2.0
yaml
name: widgets
dependencies:
  collection: ^2.4.0

然后,collection包更新到 2.5.0。collection的 2.5.0 版本包含一个名为sortBackwards()的新方法。bookshelf可能会调用sortBackwards(),因为它是由widgets公开的 API 的一部分,尽管bookshelf只是传递依赖于collection

由于widgets有一个 API,它没有反映在它的版本号中,因此使用bookshelf包并调用sortBackwards()的应用程序可能会崩溃。

导出 API 会导致该 API 被视为在包本身中定义,但当 API 添加功能时,它不能增加版本号。这意味着bookshelf无法声明它需要支持sortBackwards()widgets版本。

因此,在处理导出包时,建议包作者对依赖项的上限和下限保持更严格的限制。在这种情况下,widgets包的范围应该缩小

yaml
name: bookshelf
dependencies:
  widgets: '>=1.2.0 <1.3.0'
yaml
name: widgets
dependencies:
  collection: '>=2.4.0 <2.5.0'

这转化为widgets的 1.2.0 下限和collection的 2.4.0 下限。当有人发布collection的 2.5.0 版本时,pub 会将widgets更新到 1.3.0 并更新相应的约束条件。

使用此约定可以确保用户拥有两个包的正确版本,即使其中一个是间接依赖项。

锁定文件

#

所以,一旦 pub 解决了你应用程序的版本约束,接下来会怎样?最终结果是你的应用程序直接或间接依赖的每个包的完整列表,以及与你的应用程序约束兼容的该包的最佳版本。

对于每个包,pub 会获取该信息,从它计算出一个内容哈希,并将两者都写入你的应用程序目录中名为pubspec.lock锁文件中。当 pub 为你的应用程序构建.dart_tool/package_config.json文件时,它会使用锁文件来知道要引用哪个版本的每个包。(如果你想知道它选择了哪些版本,你可以阅读锁文件来找到答案。)

pub 做的下一件重要的事情是停止触碰锁文件。一旦你的应用程序有了锁文件,pub 就会一直保持不变,直到你告诉它为止。这一点很重要。这意味着你的应用程序不会在你没有意愿的情况下,突然开始使用随机包的新版本。一旦你的应用程序被锁定,它就会保持锁定状态,直到你手动告诉它更新锁文件。

如果你的包是用于应用程序的,你需要将你的锁文件提交到你的源代码控制系统中!这样,你的团队中的每个人在构建你的应用程序时,都会使用每个依赖项的完全相同的版本。你也会在部署应用程序时使用它,这样你就可以确保你的生产服务器使用与你开发时完全相同的包。

出现问题时

#

当然,所有这些都假设你的依赖项图是完美的,没有缺陷。即使有了版本范围、pub 的约束求解和语义版本控制,你也永远不能完全避免版本itis 的危害。

你可能会遇到以下问题之一

您可以有不相交的约束

#

假设你的应用程序使用widgetstemplates,两者都使用collection。但widgets要求使用1.0.02.0.0之间的版本,而templates想要使用3.0.04.0.0之间的版本。这些范围甚至没有重叠。没有一个可能的版本可以满足这些要求。

您可以有范围不包含已发布版本的范围

#

假设在将所有对共享依赖项的约束条件放在一起之后,你的范围缩小到>=1.2.4 <1.2.6。它不是一个空范围。如果有一个依赖项的版本1.2.4,你就会很完美。但也许他们从未发布过那个版本。相反,他们直接从1.2.3跳到1.3.0。你的范围里没有东西。

您可以有一个不稳定的图

#

到目前为止,这是 pub 版本求解过程中最具挑战性的部分。该过程被描述为构建依赖项图,然后解决所有约束并选择版本。但实际上并非如此。如何在选择任何版本之前构建整个依赖项图?pubspec 本身是特定于版本的。同一个包的不同版本可能具有不同的依赖项集。

当你选择包的版本时,它们会改变依赖项图本身的形状。随着图的变化,可能会改变约束条件,这会导致你选择不同的版本,然后你就会再次回到同一个循环中。

有时这个过程永远不会稳定下来,无法找到稳定的解决方案。凝视深渊

yaml
name: my_app
version: 0.0.0
dependencies:
  yin: '>=1.0.0'
yaml
name: yin
version: 1.0.0
dependencies:
yaml
name: yin
version: 2.0.0
dependencies:
  yang: '1.0.0'
yaml
name: yang
version: 1.0.0
dependencies:
  yin: '1.0.0'

在所有这些情况下,都没有一组具体的版本适合你的应用程序,当这种情况发生时,pub 会报告错误并告诉你发生了什么。它绝对不会把你置于一种奇怪的状态,让你认为事情可以工作,但实际上却无法工作。

总结

#

总结

  • 尽管代码重用有优势,但包需要能够独立发展。
  • 版本控制使这种独立性成为可能。依赖于单个具体的版本缺乏灵活性。再加上共享的依赖项,会导致版本锁定。
  • 为了应对版本锁定,你的包应该依赖于一系列版本。然后,pub 会遍历你的依赖项图,并为你选择最佳版本。如果它无法选择合适的版本,pub 会提醒你。
  • 一旦你的应用程序为它的依赖项确定了一组稳定的版本,这组版本就会在锁文件中被固定下来。这确保了每台运行你的应用程序的机器都使用所有依赖项的相同版本。

要了解有关 pub 版本求解算法的更多信息,请参阅 Medium 上的PubGrub文章。