为什么npm 7不再支持package-lock.json?

从我们宣布npm 7将支持文件的那一刻起yarn.lock,他们几次问相同的问题。听起来像是:“为什么要留下支持package-lock.json为什么不使用它yarn.lock呢?“ 这个问题的简短答案是:“因为它不能完全满足npm的需求。如果仅依靠它,这将降低npm形成最佳软件包安装方案的能力以及为该项目添加新功能的能力。” 在该材料中将更详细地给出答案。







yarn.lock



yarn.lock文件的基本结构



该文件yarn.lock是对程序包依赖性说明符和描述这些依赖性解析的元数据的对应关系的描述。例如:



mkdirp@1.x:
  version "1.0.2"
  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.2.tgz#5ccd93437619ca7050b538573fc918327eba98fb"
  integrity sha512-N2REVrJ/X/jGPfit2d7zea2J1pf7EAR5chIUcfHffAZ7gmlam5U65sAm76+o4ntQbSRdTjYf7qZz3chuHlwXEA==


在这篇文章中报告了以下内容:“任何对依赖的依赖mkdirp@1.x都应完全按照此处指示的解决。”如果有几个软件包依赖mkdirp@1.x,则所有这些依赖关系都将以相同的方式解决。



在npm 7中,如果项目中存在文件yarn.lock,则npm将使用其中包含的元数据。字段值resolved将告诉npm从何处下载软件包,并且这些字段值integrity将用于根据预期内容检查收到的内容。如果将软件包添加到项目中或从项目中删除,则内容将相应地更新yarn.lock



同时,与以前一样,npm创建一个文件package-lock.json如果该文件存在于项目中,它将用作有关依赖关系树的结构(窗体)的权威信息源。



这里的问题是:“如果yarn.lock对Yarn的软件包管理器足够好,为什么npm不能仅使用此文件?”



安装依赖项的确定性结果



当使用相同的文件yarn.lock和相同的版本的Yarn 时,保证使用Yarn安装软件包的结果相同。使用不同版本的Yarn可能导致程序包文件在磁盘上的位置不同。



该文件yarn.lock保证确定性的依赖关系解析。例如,如果foo@1.x允许使用foo@1.2.3,则在使用相同文件的情况下yarn.lock,这将在所有版本的Yarn中始终发生。但这(至少本身)并不等于保证依赖关系树的结构具有确定性!



考虑以下依赖关系图:



root -> (foo@1, bar@1)
foo -> (baz@1)
bar -> (baz@2)


这是几个依赖关系树图,每个都可以认为是正确的。



树号1:



root
+-- foo
+-- bar
|   +-- baz@2
+-- baz@1


树号2:



+-- foo
|   +-- baz@1
+-- bar
+-- baz@2


该文件yarn.lock无法告诉我们要使用哪个依赖关系树。如果在包中root执行命令require(«baz»)(这是不正确的,因为此依赖关系未反映在依赖关系树中),则该文件yarn.lock无法保证此操作的正确执行。这是文件可以提供的确定性形式package-lock.json,但不能yarn.lock



当然,实际上,由于Yarn,在文件中yarn.lock,因此需要所有信息来选择适当的依赖版本,只要每个人都在使用相同版本的Yarn,选择就是确定性的。这意味着版本选择始终以相同的方式进行。除非有人对其进行更改,否则代码不会更改。应该注意的是,在创建依赖关系树时,Yarn非常聪明,不会受到有关软件包清单的加载时间差异的影响。否则,将无法保证结果的确定性。



由于这是由Yarn算法的功能确定的,而不是由磁盘上可用的数据结构(没有标识将使用的算法)确定的,因此确定性的保证从本质上比它给出的保证要弱。package-lock.json包含对存储在磁盘上的依赖关系树的结构的完整描述。



换句话说,Yarn如何构建依赖关系树受yarn.lockYarn本身的文件和实现的影响在npm中,只有文件会影响依赖关系树的外观package-lock.json因此package-lock.json,使用不同版本的npm时,中描述的项目结构变得更加难以意外破坏。而且,如果对文件进行了更改(可能是错误或有意更改),则在将其修改后的版本添加到使用版本控制系统的项目存储库中时,这些更改将在文件中清晰可见。



嵌套依赖项和依赖项重复数据删除



此外,当文件yarn.lock无法准确反映解决依赖关系的结果时,存在一整套情况,涉及嵌套依赖关系和依赖项重复数据删除,实际上,npm将使用该文件。而且,即使在npm使用yarn.lock元数据作为源的情况下也是如此。尽管npm将其yarn.lock用作可靠的信息源,但npm并不将此文件视为有关对依赖版本施加限制的权威信息。



在某些情况下,Yarn会创建具有高度重复数据包的依赖关系树,而我们不需要它。结果,证明在这种情况下严格遵循Yarn算法远非理想的解决方案。



考虑以下依赖关系图:



root -> (x@1.x, y@1.x, z@1.x)
x@1.1.0 -> ()
x@1.2.0 -> ()
y@1.0.0 -> (x@1.1, z@2.x)
z@1.0.0 -> ()
z@2.0.0 -> (x@1.x)


该项目root取决于1.x软件包的版本xy并且z软件包y取决于x@1.1z@2.xz版本1 软件包没有依赖项,而版本2软件包则具有依赖项x@1.x



根据此信息,npm生成以下依赖关系树:



root (x@1.x, y@1.x, z@1.x) <--   x@1.x
+-- x 1.2.0                <-- x@1.x   1.2.0
+-- y (x@1.1, z@2.x)
|   +-- x 1.1.0            <-- x@1.x   1.1.0
|   +-- z 2.0.0 (x@1.x)    <--   x@1.x
+-- z 1.0.0


z@2.0.0取决于x@1.x,同样可以说root。该文件yarn.lock映射到x@1.xc 1.2.0。但是,如果包依赖z也指定了,x@1.x则将解析为x@1.1.0



其结果是,即使依赖x@1.x在描述yarn.lock其中规定,它应该解析到包的版本1.2.0,还有第二个解析结果x@1.x,以包的版本1.1.0



如果使用带标志运行npm --prefer-dedupe,则系统将进一步执行一步,仅安装一个依赖项实例x,这将导致以下依赖关系树的形成:



root (x@1.x, y@1.x, z@1.x)
+-- x 1.1.0       <-- x@1.x       1.1.0
+-- y (x@1.1, z@2.x)
|   +-- z 2.0.0 (x@1.x)
+-- z 1.0.0


这样可以最大程度地减少依赖项的重复,生成的依赖项树固定在file中package-lock.json



由于该文件yarn.lock仅捕获解析依赖性的顺序,而不捕获结果的包树,因此Yarn将生成如下的依赖性树:



root (x@1.x, y@1.x, z@1.x) <--   x@1.x
+-- x 1.2.0                <-- x@1.x   1.2.0
+-- y (x@1.1, z@2.x)
|   +-- x 1.1.0            <-- x@1.x   1.1.0
|   +-- z 2.0.0 (x@1.x)    <-- x@1.1.0   , ...
|       +-- x 1.2.0        <-- Yarn     ,    yarn.lock
+-- z 1.0.0


x使用Yarn时 ,程序包在依赖关系树中出现3次。当使用npm且没有其他设置时-2次。并且当使用标志时--prefer-dedupe-仅一次(尽管依赖性树不是软件包的最新版本,也不是最佳版本)。



从每个程序包将收到满足规定要求的那些版本的依赖关系的意义上来说,所有这三个结果依赖关系树都可以认为是正确的。但是我们不想创建重复项太多的程序包树。想想如果发生什么情况x-这是一个有很多依赖项的大型程序包!



结果,npm只能通过一种方式优化程序包树,同时保持创建确定性和可再现的依赖树。此方法包括使用锁定文件,该文件的形成和使用原理与根本不同yarn.lock



记录执行结果的用户意图



如前所述,在npm 7中,用户可以使用该标志--prefer-dedupe应用依赖关系树生成算法,在此过程中,优先级将赋予依赖项重复数据删除,而不是始终安装最新的软件包版本的愿望。--prefer-dedupe在需要将重复数据包减到最少的情况下,该标志通常是理想的。



如果使用此标志,则上面示例的结果树将如下所示:



root (x@1.x, y@1.x, z@1.x) <--   x@1.x 
+-- x 1.1.0                <-- x@1.x   1.1.0   
+-- y (x@1.1, z@2.x)
|   +-- z 2.0.0 (x@1.x)    <--   x@1.x
+-- z 1.0.0


在这种情况下,npm看到即使x@1.2.0是满足要求的软件包的最新版本x@1.x,也可以选择替代x@1.1.0选择此版本将减少依赖关系树中软件包的重复。



如果我们没有在锁定文件中修复依赖关系树的结构,那么团队中每个在项目上工作的程序员都必须以与其他团队成员相同的方式来设置其工作环境。只有这样,他才能获得与其他人相同的结果。如果可以类似的方式更改依赖关系树构建机制的“实现”,这将为npm用户提供基于其自身特定需求优化依赖关系的严重机会。但是,如果创建树的结果取决于系统的实现,那么就不可能创建确定性的依赖树。这就是文件的使用所导致的yarn.lock



以下是一些高级npm设置如何导致创建不同的依赖关系树的示例:



  • --legacy-peer-deps,强制npm完全忽略的标志peerDependencies
  • --legacy-bundling,一个标志告诉npm他甚至不应该尝试使依赖关系树更“平坦”。
  • --global-style,一个标志,由于该标志,所有传递依赖项都被设置为更高级别的依赖项文件夹中的嵌套依赖项。


当我们为用户提供定制构建依赖树的机制的能力时,捕获并修复依赖关系解析的结果以及使用相同算法生成依赖树的期望是行不通的。



固定完成的依赖关系树的结构使我们可以为用户提供这样的机会,同时又不破坏构建确定性和可复制的依赖关系树的过程。



性能和数据完整性



该文件package-lock.json不仅在需要确保依赖性树的确定性和可重复性时有用。此外,我们依靠此文件来跟踪和存储包元数据,从而大大节省了时间,否则,仅使用package.json,它就可以与npm注册表一起使用。由于该文件的可能性yarn.lock非常有限,因此它没有我们需要不断下载的元数据。



在npm 7中,该文件package-lock.json包含npm完全构建项目的依赖关系树所需的所有内容。在npm 6中,该数据的存储不是那么方便,因此,当我们遇到一个旧的锁定文件时,我们必须向系统加载其他工作,但是对于一个项目,此操作只能执行一次。



结果,即使在yarn.lock 并且已经编写了有关依赖关系树的结构的信息,我们必须使用另一个文件来存储其他元数据。



未来机会



如果我们考虑到各种在磁盘上放置依赖项的新方法,我们在这里所说的内容可能会发生巨大变化。这些是pnpm,纱2 /浆果和PnP纱。



我们正在npm 8上工作,将探索一种基于虚拟文件系统的方法来构建依赖关系树。这个想法是基于Tink建模的,并且该想法在2019年被证实可以使用。我们还在讨论移动到pnpm所使用的结构之类的想法,尽管从某种意义上说,这比使用虚拟文件系统要更具戏剧性。



如果所有依赖项都在某个中央存储库中,并且嵌套的依赖项仅由符号链接或虚拟文件系统表示,那么对依赖关系树的结构进行建模对我们而言就不是一个重要的问题。但是我们仍然需要更多的元数据,超出了文件所能提供的范围yarn.lock因此,更新和简化现有文件格式更有意义package-lock.json,而不是完全过渡到yarn.lock



这不是一篇可以称为“关于yarn.lock的危险”的文章



我想指出,据我所知,Yarn可靠地生成正确的项目依赖树。而且,对于特定版本的Yarn(在撰写本文时,这适用于所有新版本的Yarn),与npm一样,这些树是完全确定的。



一个文件yarn.lock足以使用相同版本的Yarn创建确定性依赖树。但是考虑到许多工具中使用了这种机制,我们不能依赖依赖于程序包管理器实现的机制。当您考虑文件格式的实现时,情况更是如此。yarn.lock没有在任何地方正式记录。 (这不是Yarn特有的问题; npm处于相同情况。记录文件格式是一件很重要的事情。)



从长远来看,确保构建高度确定性依赖树的可靠性的最佳方法是记录依赖关系解析的结果。不要依赖这样的信念,即程序包管理器的将来实现在解决依赖关系时将遵循与先前实现相同的路径。这种方法限制了我们设计优化的依赖树的能力。



与依赖关系树的最初固定结构的差异应是用户明确表达的愿望的结果。这种偏差应自行记录下来,对依赖树结构上以前记录的数据进行更改。



只有package-lock.json或类似此文件的机制才能赋予npm这样的功能。



您在JavaScript项目中使用什么软件包管理器?






All Articles