[译] Facebook发布了新的 Node 模块管理器 Yarn,或将取代 npm 客户端

原文链接:http://blog.zhangjd.me/2016/10/12/yarn-a-new-package-manager-for-javascript/

9788D64E-EA7E-43BF-ADDF-A736BD2D8216.png

Facebook 发布 Node 模块管理器 Yarn,用于取代 npm 客户端。它支持离线安装,并让依赖的安装行为更合理。

在 JavaScript 社区中,工程师们互相分享成千上万的代码,帮助我们节省大量编写基础组件、类库或框架的时间。每个代码包可能都依赖于其他代码,而代码间的依赖关系则由包管理器负责维护。目前最流行的 JavaScript 包管理器是npm 客户端,在 npm 仓库中提供了多达 30 万的软件包。据统计,已有超过 500 万的工程师使用 npm 仓库,其软件包下载量达到了 50 亿次/月。

在 Facebook 中,我们多年来一直在使用 npm 客户端并取得了成功,但随着代码仓库与团队人数的增长,我们在一致性、安全性以及性能方面遇到了挑战。在尝试解决每个方面的问题后,我们最终决定着手打造一套新的客户端解决方案,以帮助我们更可靠地管理依赖。我们把这个客户端工具称为 Yarn —— 更加快速、可靠、安全的 npm 客户端的替代品。

我们在此荣幸地宣布,我们与 Exponent、 Google 和 Tilde 进行了合作,并开源 Yarn 项目。工程师在使用 Yarn 时,依然需要访问 npm 仓库,但 Yarn能够更快速地安装软件包和管理依赖关系,并且可以在跨机器或者无网络的安全环境中保持代码的一致性。Yarn 提高了开发效率,并解决了共享代码时面临的一些问题,使得工程师们可以专注在构建新产品以及新特性上。

JavaScript 包管理方式在 Facebook 的演变

在包管理工具出现之前,JavaScript 工程师们通常依赖的项目并不多,因此会把依赖直接存储在工程目录或上传到 CDN 上。在 Node.js 出现后不久,第一个主流的 JavaScript 包管理工具 npm 被引入进来,并很快成为了最受欢迎的包管理工具之一。从此,新的开源项目不断涌现,工程师们比起以前更加乐于分享代码了。

在 Facebook 中,我们有很多项目都要依赖 npm 仓库上的代码,比如 React。但随着内部规模的扩大,我们面临着以下挑战:在跨平台与跨用户之间安装依赖时的代码一致性问题、在安装依赖时花费太长时间、以及 npm 客户端自动执行某些依赖库的代码所导致的安全性问题。我们尝试过寻找这些问题的解决方案,但在这个过程中通常又会引起一些新的问题。

尝试修改 npm 客户端

在开始阶段,我们遵循了最佳实践,在代码仓库中只跟踪了 package.json 文件的变化,并要求工程师手动运行 npm install 命令安装依赖。这种模式在开发人员的电脑上没有问题,但在持续集成环境中遇到了困难,因为出于安全与可靠性的考虑,持续集成环境需要进行沙箱隔离,不能进行联网,因此也无法安装依赖。

接下来,我们尝试在代码仓库中跟踪整个 node_modules 目录的文件变化。虽然这种方式有效,却使得一些简单操作变得复杂化了。比如,对 babel 更新一个次要版本号时,会产生多达 800,000 行的提交记录,此外由于 lint 规则的存在,引起无效的 utf-8 字节序列、windows 换行符、非 png 压缩图片等问题时,将会导致工程师经常需要花费一整天的时间合并 node_modules 目录的文件。而我们负责源码控制的团队也指出,跟踪 node_modules 目录会引入过多的元数据。比如 React Native 的 package.json 文件目前只列出了68项依赖,但在运行npm install 后,node_modules 目录整整包含了 121,358 个文件。

最后,为了有效组织 Facebook 逐渐增长的工程师人数以及管理需要安装的代码量,我们尝试修改 npm 客户端。我们决定压缩整个 node_modules 目录,并上传到内部 CDN,然后我们的工程师与持续集成系统都能从 CDN 上下载并解压文件,从而保证了代码一致性。这样我们就可以从源码控制系统中删除数以万计的文件了,但不足之处是工程师现在不仅在拉代码时需要联网了,构建也同样需要联网。

我们还试图为 npm 的 shrinkwrap 功能寻求优化方案,这个工具是用来锁定依赖版本号的。但 Shrinkwrap 功能的文件默认不会生成,如果开发者忘记了生成这一步骤,文件就不会被同步更新,因此我们编写了一个工具,以确定Shrinkwrap 的文件内容和 node_modules 目录中的文件相符。这些文件由大量的 JSON 块组成,并且键名是无序的,因此每次更改通常会导致Shrinkwrap 文件的内容大幅变化,难以进行代码审查。为减缓这一问题,我们还需要借助一个额外的脚本,对所有条目进行排序。

最后,通过 npm 升级单个依赖包时,基于 语义化版本号 规则,npm 通常会连同其他无关依赖一起更新。这使得每次更新都会比预期产生更多的变化,工程师们认为这样把 node_modules 提交上传到 CDN 的过程,难以达到预期的效果。

构建新客户端

与其围绕 npm 客户端继续构建基础设施,不如从整体上再次回顾这些问题。伦敦办公室的 Sebastian McKenzie 提出,如果我们建立一个新客户端工具以代替 npm 客户端,从而解决我们的核心问题呢?这一构思很快得到了我们的认同,团队对于这个主意也感到非常兴奋。

在开发过程中,我们与业界的工程师们进行了交流讨论,发现他们也面临着类似的问题,也尝试过许多类似的解决方案,通常只能把这些问题逐一解决。很明显,有必要把整个 JavaScript 社区正在面临的问题集合起来,然后我们就可以开发一个主流的解决方案了。在此感谢 Exponent、 Google 与 Tilde 的工程师们的协助,我们共同建立了 Yarn 客户端,并在每一个主流 JS 框架以及 Facebook 外的使用场景中测试验证了 Yarn 的性能。今天(2016-10-11),我们很荣幸把这个工具开源分享到社区中。

介绍 Yarn

Yarn 是一个新的包管理器,用于替代现有的 npm 客户端或者其他兼容 npm仓库的包管理工具。Yarn 保留了现有工作流的特性,优点是更快、更安全、更可靠。

任何包管理器的主要功能都是安装某些软件包,软件包即用于特定功能的某段代码,通常是从一个全局的仓库安装到工程师的本地环境。每个软件包可以依赖于其他包,也可以不依赖。一个典型的项目结构的依赖树通常会包含数十个、数百个甚至上千个软件包。

这些依赖包通常是带版本号的,通过语义化版本控制(semver)安装。Semver 定义的版本号反映了每个新版本更改的类型,到底是进行了不兼容的API改动(MAJOR),还是添加了向后兼容的新特性(MINOR),还是进行了向后兼容的 bug 修复(PATCH)。然而,semver 依赖于软件包的开发者不能犯错误——如果依赖关系没有加锁,可能会引入一些破坏性更改或者产生新的 bug。

结构

在 Node 生态系统中,依赖通常安装在项目的 node_modules 文件夹中。然而,这个文件的结构和实际依赖树可能有所区别,因为重复的依赖可以合并到一起。npm 客户端把依赖安装到 node_modules 目录的过程具有不确定性。这意味着当依赖的安装顺序不同时,node_modules 目录的结构可能会发生变化。这种差异可能会导致类似“我的机子上可以运行,别的机子不行”的情况,并且通常要花费大量时间定位与解决。

Yarn 通过 lockfiles 文件以及一个确定性的、可靠的安装算法,解决了版本问题和 npm 的不确定性问题。Lockfile 文件把安装的软件包版本锁定在某个特定版本,并保证 node_modules 目录在所有机器上的安装结果都是相同的。Lockfile 还使用简洁的有序键名的格式,保证了每次的文件变化最小化,进行代码审查也更为简单。

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

  1. 处理: Yarn 通过向代码仓库发送请求,并递归查找每个依赖项,从而解决依赖关系。

  2. 抓取: 接下来,Yarn 会查找全局的缓存目录,检查所需的软件包是否已被下载。如果没有,Yarn 会抓取对应的压缩包,并放置在全局的缓存目录中,因此 Yarn 支持离线安装,同一个安装包不需要下载多次。依赖也可以通过 tarball 的压缩形式放置在源码控制系统中,以支持完整的离线安装。

  3. 生成: 最后,Yarn 从全局缓存中把需要用到的所有文件复制到本地的node_modules 目录中。

通过清晰地细分这些步骤,以及确定性的算法支持,使得 Yarn 支持并行操作,从而最大化地利用资源,并加速安装进程。在一些 Facebook 的项目上,Yarn甚至可以把安装过程降低一个数量级,从几分钟到只需几秒钟。Yarn 还使用了互斥锁,以确保多个 CLI 实例同时运行时不会互相冲突与影响。

纵观整个过程,Yarn 对于软件包安装加上了严格的限制。你可以对哪个生命周期脚本作用于哪个软件包进行控制。软件包的 checksum 也会存储在 lockfile 中,以确保每一次安装都可以得到同一个包。

特性

Yarn 除了让安装过程变得更快与更可靠,还添加了一些额外的特性,从而进一步简化依赖管理的工作流。

  • 同时兼容 npm 与 bower 工作流,并支持两种软件仓库混合使用

  • 可以限制已安装模块的协议,并提供方法输出协议信息

  • 提供一套稳定的公有 JS API,用于记录构建工具的输出信息

  • 可读、最小化、美观的 CLI 输出信息

Yarn 用于生产环境

我们已经在 Facebook 中把 Yarn 用于生产环境,并且效果非常理想。Yarn有效地管理了许多 JavaScript 项目的包依赖关系。在每次迁移时,构建都可以离线进行,因此加速了工作流程。我们基于 React Native 在不同条件下进行安装时间测试,比较了 Yarn 与 npm 的性能,具体参见这里

起步

最简单的起步方法是:

npm install -g yarnpkg
yarn

yarn CLI 代替了原有开发工作流中 npm CLI 的作用,用法可能是单纯的替代,也可能是一个新的、相似的命令:

  • npm install → yarn

    不需要带参数,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、分享构思、编写文档、互相支持,并帮助构建一个很棒的社区来进行长期维护。我们相信 Yarn 已经拥有一个良好的开局,如果有你的帮助,Yarn 的未来将会更加美好。