Yarn: 一款新的 JavaScript 包管理器

458 查看

译者前言

这两天被 Yarn 刷屏了?对于新工具,范范地道听途说或浅尝辄止,不如静下心来听听作者的心路历程。我读了多篇文章,感觉说得最清楚的还是 Facebook 发表的这篇官方介绍,于是翻译出来分享给大家。

在谈论 Yarn 时,有人在谈论前端圈的 “造轮子” 风气,有人在谈论 Facebook 工程师的 PR 能力,有人在谈论网络速度……但读完这篇文章之后,我看到的是前端工程、是开发体验、是思维方式。相信不论是一线工程师还是架构师,都会从 Facebook 团队的思考中找到启发。

在 JavaScript 社区里,开发者们共享出了成千上万的代码,这令我们不必自己从头开始编写最基础的组件、类库或框架。这些代码之间通常层层依赖,而这些依赖关系正是由包管理器来管理的。当前最流行的 JavaScript 包管理器是 npm 客户端,通过它,我们可以获取 npm 仓库中的 30 多万个包。超过 500 万名开发者在使用 npm 仓库,每个月产生 50 多亿次下载。

在 Facebook,我们已经成功地使用 npm 客户端多年了,但随着代码体积的不断增长和团队规模的不断壮大,我们在一致性、安全性和性能方面遇到了问题。我们曾尝试逐一解决这些问题,但最终,我们决定自己打造一套全新的解决方案,以一种更加可靠的方式来管理依赖。我们的工作成果叫作 Yarn——作为 npm 客户端的替代器,它更加快速、可靠、安全。

今天,我们很荣幸地宣布,Yarn 以开源的方式发布,它是由 Exponent、 Google、Tilde 与我们合作完成的。在使用 Yarn 时,开发者们还像以前一样从 npm 仓库那里获取资源,但安装速度更快,不同的机器的安装结果完全一致,甚至还可以在安全的离线环境中使用。Yarn 令开发者可以更加迅捷和从容地享受前人栽种的果实,进而集中精力打造自己的产品——这才是更加重要的事情。

JS 包管理方案在 Facebook 的演变

在包管理器出现之前,JavaScript 开发者平时所用的依赖并不多,这些依赖通常会直接存放在项目中,或通过 CDN 引入。npm 是第一个正式的 JavaScript 包管理器,在 Node.js 诞生后不久它就建立起来了,随后迅速成为全世界最流行的包管理器。从此,开源项目如雨后春笋般涌现,盛况空前。

Facebook 的很多项目,比如 React,就依赖了 npm 仓库中的代码。但是,随着内部规模的扩张,我们遇到了很多问题:不同机器或不同人所得到的安装结果并不一致,安装依赖所花费的时间也无法忽视;由于 npm 客户端在安装依赖包时会自动执行其中的脚本,安全性也令我们顾虑重重。我们不断尝试针对这些问题建立解决方案,但这些解决方案往往又不断引发新的问题。

关于理顺 npm 客户端的一系列尝试

最开始,我们遵循官方推荐的最佳实践,只把 package.json 文件提交到代码库,然后要求工程师们手动运行 npm install 来安装依赖。这种方法对工程师来说运作良好,但在我们的持续集成(CI)环境下就不灵了,因为 CI 环境需要运行在沙箱中,基于安全性和可靠性的考虑,它需要切断与互联网的连接。

后来我们改变了对策,把所有 node_modules 签入代码库。这种方法也运行了一段时间,但它也让一些原本简单的操作变得困难起来。举例来说,更新 Babel 的一个次版本号会产生一次 “80 万行” 级别的提交动作,这种情况很难处理,而且会触发一系列无意义的 lint 操作(诸如检查 UTF-8 字节序列、Windows 换行符、未优化的 PNG 图片等等)。把 node_modules 的变更合并进来往往要花费工程师一整天的时间。我们的版本控制团队也指出我们提交的 node_modules 目录直接导致元数据体积的飙升。比如 React Native 项目的 package.json 只列出了 68 个依赖,但在跑过了 npm install 之后,node_modules 目录下会产生 121,358 个文件。

为了让 npm 的这一套工作流适应 Facebook 的团队人数和代码量,我们动用了最后一招。我们决定把整个 node_modules 目录打成一个压缩包,上传到专用的 CDN 上,这样工程师和 CI 服务器都可以下载、解压并得到完全一致的结果了。这个方案一上,我们就可以从版本控制系统中清掉成千上万的文件了;但这同时也导致工程师不论是在拉取新代码时,还是在编写新代码时,都需要保持网络连接。

我们还不得不应付 npm 的 shrinkwrap 功能所带来的问题,因为我们使用 shrinkwrap 来锁定依赖版本。Shrinkwrap 文件并不是默认生成的,一旦工程师在提交代码前忘记生成这个文件,则这个文件跟代码就不同步了。为此我们还特意写了一个工具,用来校验 shrinkwrap 文件的内容跟 node_modules 的内容是否匹配。这个 shrinkwrap 文件是由一堆乱序字段所组成的巨型 JSON 数据,因此每次修改都会产生一次巨型的、无法 review 的提交。为了缓解这个问题,我们又需要写一个额外的脚本来把所有的字段排好序。

这还没完呢。在 npm 中,更新一项单独的依赖其实还会按照 “语义化版本”(semantic versioning)的规则更新一堆无关的依赖。这使得每次代码变更都比预期的要多,而我们所能做的也就是上述这一系列的下策。

构建新的客户端

痛定思痛,我们决定不再围绕 npm 客户端来构建基础设施,而是从一个更高的高度来审视整件事情。如果我们针对当下的痛点来打造一款全新的客户端会怎么样?我们伦敦办事处的 Sebastian McKenzie 同学开始推进这个想法,很快我们就发现这条道路充满光明。

在推进这个计划的同时,我们也开始跟业内的工程师们广泛交谈。我们发现大家遇到的问题其实都差不多,也都尝试过差不多的解决方案,效果也差不多都是按下个葫芦浮起个瓢。接下来的事情就变得水到渠成了——我们集合社区的力量来解决共通的问题,产出一个适用于每个人的解决方案。由此我们收到了来自 Exponent、Google 和 Tilde 公司的工程师的帮助,共同开发出了 Yarn 客户端;为了确保它也适用于 Facebook 之外的使用场景,我们还在所有主流的 JS 框架身上验证了它的性能。今天,我们怀着激动的心情,将它推荐给整个社区。

Yarn 介绍

Yarn 是一款新的包管理器,它将取代原有的基于 npm 客户端(或其它包管理器)的工作流,但同时又保留了与 npm 仓库的兼容性。它具备原有工作流的所有功能,但相比之下更加快速、安全、可靠。

所有包管理器的核心功能都是从一个通用仓库中获取包,然后安装到开发者的本地环境中。所谓 “包”,就是一套解决特定任务的代码。一个包可能依赖、也可能不依赖其它包。一个典型的项目可能会在它的依赖树中涉及数十、数百甚至数千个包。

这些依赖包都是有版本的,它们按照 “语义化版本”(semver)的规则被安装进来。Semver 定义了一套版本描述规范,用于表明每个版本的变更类型:是 API 行为出现了向后不兼容的破坏性变更、是增加了一个新功能、还是修复了一个 bug。但是,semver 是否奏效,取决于包的开发者是否遵守规则。在理想情况下,即使依赖包没有锁定版本,包的不同类型的变更版本都会在 semver 的约定下以不同的方式被正确地安装进来。

架构设计

在 Node 的生态系统中,依赖会被安装到你的项目中的 node_modules 目录中。不过,其内部的文件结构和实际的依赖关系树并不完全对应,因为安装过程存在重复依赖的合并机制。npm 客户端在把依赖安装到 node_modules 目录时存在不确定性。这种 “不确定性” 是指,由于安装依赖的顺序不同,你得到的 node_modules 目录的内部结构可能跟别人不一样。这种差异可能会导致 “我电脑上是好的” 之类的 bug,而这类 bug 往往是极难定位的。

Yarn 解决上述版本问题和不确性问题的方案是引入 lockfile(锁定文件),并启用了一套新的安装算法,以此达到一致、可靠的结果。这个 lockfile 会把所有已安装的依赖锁定在一个固定的版本上,确保每次安装所产生的 node_modules 目录的文件结构在不同机器上总是一致的。这个 lockfile 采用一种简明的格式来书写,其字段是有序的,以确保每次更新都是最小化的、易于 reivew 的。

整个安装过程被分解为以下三个步骤:

  1. 解析:Yarn 首先开始解析依赖关系。它向包仓库发出请求,并递归地查询各层依赖。
  2. 获取:接下来,Yarn 会在一个全局的缓存目录中查找当前所需的包是不是已经下载过了。如果还没有,Yarn 会把这个包的 tarball 拉下来,并把它存放在全局缓存中,这样它下次就可以离线安装了,无需重复下载。依赖包也可以以 tarball 的形式存放到版本控制系统中,以实现完全的离线安装。
  3. 链接:最后,Yarn 会把所需的所有文件从缓存中复制到本地的 node_modules 目录中,这样所有东西就链接为一个整体了。

由于我们把安装过程清晰地拆解开来,消灭了安装结果的不确定性,Yarn 天生具备并行操作的能力。这种并行能力可以最大化资源的利用率,提升安装速度。在 Facebook 的某些项目中,Yarn 可以带来一个数量级的性能提升,安装耗时从几分钟缩短到几秒。Yarn 内建互斥特性,以确保同时运行多个 CLI 实例也不会相互冲突、相互污染。

在整个安装过程中,Yarn 还提供了严格的安全保障。你可以精确控制某个包的某个生命周期脚本是否运行。包的校验信息(checksum)也会保存在 lockfile 中,确保你每次安装得到的都是同一个包。

其它特性

除了让包的安装变得更加快速和可靠以外,Yarn 还提供了如下特性,进一步简化了依赖管理的工作流。

  • 同时兼容 npm 和 Bower 工作流,支持混用多种仓库类型。
  • 可以限制依赖包的授权类型,并且可以输出依赖包的授权信息。
  • 暴露一个稳定的 JS API,提供抽象化的日志信息以便与其它构建工具集成。
  • 提供可读的、最小化的、美观的 CLI 输出信息。

应用到生产环境

在 Facebook,我们已经将 Yarn 用于生产环境了,而且它跑得也确实不错。我们的很多 JavaScript 项目都是由它来完成依赖安装和包管理的。每当一个项目完成迁移后,工程师们都会获得离线工作的能力,进而加快工作流。我们提供了一个页面(https://yarnpkg.com/en/compare),用来展示不同条件下 Yarn 和 npm 在安装耗时方面的对比。

demo

如何上手

最简单的上手方式就是运行以下命令:

npm install -g yarn

yarn

新的 yarn CLI 将会在你的开发工作流中替代 npm。在各种场景下,Yarn 要么提供了一个对等的命令,要么提供了一个功能相似的新命令:

  • npm installyarn在不加任何参数的情况下,yarn 命令将会读取你的 package.json 文件,从 npm 仓库拉取包,然后存放到你的 node_modules 目录中。其效果等同于直接运行 npm install
  • npm install --save yarn add我们去掉了 npm install 命令产生 “隐形依赖” 的行为,只保留了显式安装行为。运行 yarn add 将等同于运行 npm install --save

未来

我们这一群人走到一起构建了 Yarn,目的在于解决社区共通的问题;我们的愿景也很明确,希望 Yarn 成为一个真正的社区项目,人人均可从中受益。Yarn 已经在 GitHub 开源,我们也已经准备好迎接来自 Node 社区的帮助:开始使用、交换想法、编写文档、互相扶持,共同建立一个强大的社区来推动它的发展。我们相信 Yarn 已经迈出了坚实的第一步,而且你的参与会让它变得更好!