如何评价Facebook推出的JavaScript模块管理器yarn?

facebook在其开发者博客上,code.facebook.com/posts上 公布了他们开发的新一代包管理工具yarn,项目地址:Yarn。 小玩了下,感觉和Rust的包管理工具Cargo很像。 我们知道npm有很多缺陷,那么重新设计这个yarn解决了哪些问题呢?解决的怎么样?
关注者
845
被浏览
44306

22 个回答

就在昨天(2016 年 10 月 11 日),facebook 公开了新的 javascript 包管理工具 yarn, 用来替代目前被广泛使用的 npm (nodejs 自带的包管理工具)


github 项目地址:

GitHub - yarnpkg/yarn: Fast, reliable, and secure dependency management for JavaScript.


仅仅一天的时间,github 上的这个项目已经得到了 200+ 的 watch 以及 8000+ 的 star. (还在急速增加中……人们到底是有多讨厌 npm)





yarn 有什么优点?

yarn 和 npm 做的是完全一样的事情:作为 nodejs 的包管理工具。既然是一样的事情,那么 yarn 必须有一些优点,才能说服大家去用。


根据官方网站的介绍,yarn 有以下六项特点:


> 离线模式(重要)

如果之前已经安装过一个软件包,再次安装时就不用再从网络下载了。


这一点很重要,npm 饱受诟病的一点就是,每次安装依赖,都需要从网络下载一大堆东西,而且是全部重新下载。工程多的时候比较烦人。这下子可以节约大量时间了。


> 依赖关系确定性(重要)

在每一台机器上针对同一个工程安装依赖时,生成的依赖关系顺序和版本是一致的。





之前 npm 在这里有一个处理得不好的地方。举例来说,我写的工程依赖 A, B, C 三个库,我在编写 package.json 的时候,给 A, B, C 都指定了版本号。但是 A 库可能又依赖 D, E, F 库,D 库又依赖 G, H 库。这么多关联依赖关系中,很可能某个库在指定依赖时,没有指定版本号。


于是,这就导致了一个问题。如果我在另一台机器上对同样的工程安装依赖,或者把这台机器工程下的 node_modules 目录删除来重新安装依赖。由于关联依赖中,没有指定版本号的库,发生了版本更新,就会导致再次安装的依赖,其中具体某些软件包的版本是不一致的


在这种情况下,你会发现原来能够正常运行的程序,忽然变得不能工作或一堆 BUG. 我在最近使用 react-native 编写手机应用时,就遭遇过这样的问题。只能采取一些很曲折的方式来解决。


yarn 采用的解决方式是,引入了一个 yarn.lock 文件来应对这个问题。lock 机制在很多包管理中都有用到。例如 ruby 的 rubygems 就会生成 Gemfile.lock.


yarn.lock 会记录你安装的所有大大小小的软件包的具体版本号。只要你不删除 yarn.lock 文件,再次运行 yarn install 时,会根据其中记录的版本号获取所有依赖包。你可以把 yarn.lock 提交到版本库里,这样其他同事签出代码并运行 yarn install 时,可以保证大家安装的依赖都是完全一致的。


这就特别适合大型项目的多人协作开发和部署。


> 更好的网络性能

下载软件包时,会进行更好的排序,避免“请求瀑布”,最大限度提高网络利用率。


> 多注册来源处理

所有的依赖包,不管他被不同的库间接关联引用多少次,安装这个包时,只会从一个注册来源去装,要么是 npm 要么是 bower, 防止出现混乱不一致。


> 网络弹性处理

安装依赖的过程中,不会因为某个单次网络请求的失败导致整个安装挂掉(这里又要黑一下 npm)。当请求失败时会进行自动重试。


> 扁平模式(重要)

当关联依赖中包括对某个软件包的重复引用,在实际安装时将尽量避免重复的创建。


为了说明这个问题,我们假设目前工程依赖 A, B, C 三个库,而他们对某个库 somelib 存在这样的依赖关系:


A - somelib 1.4.x
B - somelib 1.6.x
C - somelib 1.6.x

如果要安装 ABC 三个库,那么 somelib 会存在版本冲突。npm 会在实际安装时,给三个库分别下载各自依赖的 somelib 版本。假设 npm 先安装了 A, 由于 A 依赖 somelib 1.4.x 版本,那么 1.4.x 会变成主版本。

再安装 B, C 时,由于 B, C 依赖的都不是 1.4.x, 于是安装完之后,关系就变成这个样子了:


node_modules
├── A
├── somelib 1.4.x
├── B
│   └── node_modules
│       └── somelib 1.6.x
└── C
    └── node_modules
        └── somelib 1.6.x

明明 B, C 都依赖 1.6.x 版本,实际上 npm 却要把这个版本保存两次,这样明显是对磁盘空间的浪费。我们把这种情况就称为“不扁平的”。尽管 npm 也提供了诸如 flat 等指令去支持“扁平化”处理,yarn 明显试图在这方面做得更好。


总之来说,yarn 要做到的就是三点:快速,安全,可靠

LOGO 是猫咪(这也算优点?)





实际的使用体会

我把自己手头的几个用到 npm 安装 js 依赖的 rails 工程还有静态 web 工程下的 node_modules 子目录删除,然后用 yarn install 重新安装依赖。


实际体验是速度要比 npm 快上不少,基本上可以令人满意。原来 npm install 需要 8 - 10 分钟的一个工程,改用 yarn install 后,只需 72 秒完成。


而且更令人欣喜的是:如果某个 js 库的某个版本在这个系统里被安装过一次,那么另一个工程再次需要安装这个库时,就完全不用再次下载。会直接从当前系统里获取这个库。大大节约了网络传输量和下载安装时间。


可以做一个简单的测试,对某个工程执行过 yarn install 后,删除 node_modules 目录,再次 yarn install. 会看到完全不用进行网络下载,几秒内就能再次生成 node_modules 目录。


这其中 lock 机制起到了很大的作用。对于经常要同时编写维护很多依赖 nodejs 的工程师而言,这是一个非常好的消息。


建议大家马上开始尝试使用 yarn.


--------------------


2016.10.13 补充:


如果觉得安装速度慢,安装源和原来 npm 是一样的,可以通用,修改方法如下:

yarn config get registry
# -> https://registry.yarnpkg.com

可以改成 taobao 的源:

yarn config set registry 'https://registry.npm.taobao.org'
# -> yarn config v0.15.0
# -> success Set "registry" to "https://registry.npm.taobao.org".
# -> Done in 0.04s.

然后 yarn install 的速度就快多了


另外,目前的小瑕疵还是不少,例如我刚刚遇到的问题,某个工程的 package.json 里面是这样写的:

"eslint-plugin-markdown": "*",

yarn install 的时候报错:

error Couldn't find any versions for eslint-plugin-markdown that matches *.

改成

"eslint-plugin-markdown": "latest",

之后,安装可以通过。


此外,这样的写法(直接引用本工程路径)目前也是不支持的:
"eslint-plugin-material-ui": "./packages/eslint-plugin-material-ui",

类似的小问题估计在近期会被各种人发现(提 issue 呗),最后如何取舍,是否都能修复,看团队的反应。目前还是可以看好。

@贺师俊 只是提到了他的加载慢,我觉得更重要的一点是可靠性以及因为保证可靠性所导致的开发慢。

This worked well enough for engineers, but broke down in our continuous integration environments, which need to be sandboxed and cut off from the internet for security and reliability reasons.

大家都知道 npm 在 fetch 的时候会自动执行一些脚本,默认情况下我们都认为这是安全的脚本。但试想一下假如一个知名的 npm publisher 的账号被窃取,被人恶意利用。那么这种情况无异于 cdn 投毒,甚至比 cdn 投毒更厉害。

如何看待 Azer Koçulu 删除了自己的所有 npm 库? - JavaScript 事件已经证明现有的npm机制是有漏洞的。

所以 facebook 的人员想到了手动管理版本,并且一个个检查 node_modules 下的包以保证可靠性。

The next solution we implemented was to check all of node_modules into the repository. While this worked, it made some simple operations quite difficult. For example, updating a minor version ofbabel generated an 800,000-line commit that was difficult to land and triggered lint rules for invalid utf8 byte sequences, windows line endings, non png-crushed images, and more. Merging changes tonode_modules would often take engineers an entire day. Our source control team also pointed out that our checked-in node_modules folder was responsible for a tremendous amount of metadata. The React Native package.json currently lists just 68 dependencies, but after running npm install the node_modules directory contains 121,358 files.
这里有两个数字,800000 和 121358。升级一下babel的小版本就有超过800000的提交,而 React Native 的 package.json 只包含68个依赖但是却有121358个文件。所以每次比对都要花费工程师一整天的时间,然后事实上这些比对大多数时候都是没有意义的,只是例行工作而已。

这里的原因是因为 package.json 只控制了依赖模块的大版本,但是依赖模块里面玩往往又依赖着子模块,如果这些依赖模块的 package.json 没有写死版本号,那么可能在不同的时间执行相同的 npm install 会得到两个不完全一样的 node_modules 。这个绝对不能忍!

为了解决这个所以他们选择了锁定版本,就是文中提到的 shrinkwrap

We also had to work around issues with npm's shrinkwrap feature, which we used to lock down dependency versions. Shrinkwrap files aren't generated by default and will fall out of sync if engineers forget to generate them, so we wrote a tool to verify that the contents of the shrinkwrap file matches what's in node_modules. These files are huge JSON blobs with unsorted keys, though, so changes to them would generate massive, difficult-to-review commits. To mitigate this, we needed to add an additional script to sort all the entries.

但就像文中写的 “We attempted to build solutions around these issues, but they often raised new issues themselves.”,解决了一个问题又出现了另外一个问题!所以他们想到了自己去开发客户端。

所以这就是 Yarn 的出现。


我觉得设计 Yarn 的人非常的务实,所有的点都围绕着工程上的问题去解决的,而且完全兼容现有的 npm registry。这有什么好挑剔的!但是对于大部分的基础用户,特别是国内的用户,我的建议还是使用cnpm,毕竟他的大部分设计功能并不是针对普通用户设计,特别是国内有cnpm这么优秀的工具的情况下。
为什么?