>* 原文链接 : [Yarn: A new package manager for JavaScript](https://code.facebook.com/posts/1840075619545360)
* 原文作者 : [SEBASTIAN MCKENZIE](https://www.facebook.com/sebmck)，[CHRISTOPH POJER](https://www.facebook.com/cpojer)，[JAMES KYLE](https://www.facebook.com/thejameskyle)
* 译文出自 : [掘金翻译计划](https://github.com/xitu/gold-miner)
* 译者 : [达仔](https://github.com/Zhangjd)
* 校对者: [根号三](https://github.com/sqrthree) 


在 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](https://babeljs.io/) 更新一个次要版本号时，会产生多达 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](https://docs.npmjs.com/cli/shrinkwrap) 功能寻求优化方案，这个工具是用来锁定依赖版本号的。但 `Shrinkwrap` 功能的文件默认不会生成，如果开发者忘记了生成这一步骤，文件就不会被同步更新，因此我们编写了一个工具，以确定  `Shrinkwrap` 的文件内容和 `node_modules` 目录中的文件相符。这些文件由大量的 JSON 块组成，并且键名是无序的，因此每次更改通常会导致 `Shrinkwrap` 文件的内容大幅变化，难以进行代码审查。为减缓这一问题，我们还需要借助一个额外的脚本，对所有条目进行排序。

最后，通过 `npm` 升级单个依赖包时，基于 [语义化版本号](http://semver.org/) 规则，`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](https://bower.io/) 工作流，并支持两种软件仓库混合使用
*   可以限制已安装模块的协议，并提供方法输出协议信息
*   提供一套稳定的公有 JS API，用于记录构建工具的输出信息
*   可读、最小化、美观的 CLI 输出信息

### Yarn 用于生产环境

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

![](http://ww4.sinaimg.cn/large/5ef54d60jw1f8p8iy84s9g20nj05ldjm.gif)

## 起步

最简单的起步方法是：

    npm install -g yarnpkg
    yarn

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

*   `npm install` → `yarn`

    不需要带参数，`yarn` 命令会读取 `package.json` 文件，然后从 npm 仓库中抓取软件包，并放置到 `node_modules` 目录中。等价于运行 `npm install`。
*   `npm install --save <name>` → `yarn add <name>`

    我们避免了 `npm install <name>` 命令中安装“不可见的依赖”的行为，并分离出一个新命令。运行 `yarn add <name>` 等价于运行 `npm install --save <name>`。

### 未来

目前已经有许多成员一起参与到 `Yarn` 的构建中，以解决我们的共同问题，我们也希望 `Yarn` 未来能真正成为一个大众化的社区项目。`Yarn` 目前已经 [在 GitHub 开源](https://github.com/yarnpkg/yarn) ，我们也已经准备好向 Node 社区进行推广：使用 `Yarn`、分享构思、编写文档、互相支持，并帮助构建一个很棒的社区来进行长期维护。我们相信 `Yarn` 已经拥有一个良好的开局，如果有你的帮助，`Yarn` 的未来将会更加美好。
