张大侠

我们为什么要从NPM切换到Yarn

张大侠 · 2017-01-05翻译 · 1260阅读 原文链接

今年10月,Facebook发布了Yarn一个新的JavaScript包管理CLI客户端工具,意图与NPM相竞争。起初,包括我自己在内很多人对此均持怀疑态度,这确实也很合理。同一生态下的多依赖管理解决方案,事情很容易就会变得复杂。那在这种让人复杂地让人感到痛苦的情况下,任何增加复杂性(的行为)都是极其糟糕的。

Yarn发布初始版本之后,我体验了Yarn大约15分钟,得出的结论是:Yarn是为了解决非常大型机构面临的问题。而NPM则能满足我们所有的需求,所以为什么我要投入时间和精力去学习一个根本不会给我带来什么价值的东西呢?

问题

Yarn起始于我们前端一次失败的构建。在长达几个月里,我们间歇性地遭遇构建失败,并且发现是由于使用NPM安装命令不正确安装的模块,或者一些模块作者破坏性的修改(而并非遵循类似于版本控制要求的修改)所引起的。因为NPM允许指定范围内的包版本安装,那么就会造成补丁或者版本修复就会自动应用到项目中的副作用,这可能就会造成同一个“package.json”(NPM配置文件)在不同的机器内根据相同的dependencies却安装了不同版本的库。而这根本就不是我们想要的。

对于我们来说,这些问题是间歇发生的,并且临时采取的解决方案特别多:重新构建、希望NPM安装正确的模块、或者将出问题的包降级等。任何一名JS开发者都知道,追踪由于NPM安装引起的错误极其困难,因为NPM的错误提示信息根本没什么用,再加上大多数的JS模块一般都依赖若干其他的JS模块,这反过来就会追踪到更多的模块上,如此递归。

深入研究

某一天,由于transpilation(ES6转换为ES5的过程)出错引发构建失败。我检查代码版本并问了团队其他成员是否有人不小心提交了一个破坏性修改到Webpack配置文件上,或者显式(主动)升级了依赖版本。很不幸,(都没能找到原因)。

我尝试重新构建了一下,还是同样的报错信息。每次构建时,典型的构建过程就是会下载全新的依赖包。无奈之下,我参考着三天前备份在电脑里的node_modules目录下的依赖在本地构建成功了。然后,我彻底删除了node_modules目录再重新运行npm install,他会根据全新安装的依赖组件来重新构建。最开始还是会报之前同样的错误。我继续删除了node_modules目录再重新安装,相同的报错又出现了。我让我同时Michael将他本地的node_modules目录打包发给我(他的包是五天前的)。我参考这些组件重新构建最后成功了。所以我很确定错误是由于依赖模块引起的,并且打算弄明白到底是哪一个依赖模块。

30分钟过后,经过Google和查阅Github issues,我将引起问题的模块定位到babel转换重新生成器插件上。(针对这个模块)几个月以来都没有提交过修改,并且网上针对这个问题为什么出现也没有令人满意的解释。但是这个模块的几个不同版本6.5.2, 6.9.0, 6.20.0作为其他包的依赖被安装过。6.9.0版本的依赖自动升级为6.20.0版本。这对于6.9.0版本允许升级来说是正确的,然而6.5.2版本也允许升级但却并没有自动升级。所有这些版本都应该升级到6.20.0版本。

当你在node中通过require()方法引入一个包的时候,相应的依赖也会随后被缓存起来(引入)。当下次一个文件引入相同的包时,缓存起来的依赖就会用上。但这里存在一个问题:当6.5.2版本的包第一次引入的时候,相应地依赖被缓存到了本地。之后,当本地缓存的依赖被不同的模块需要的时候,就会优先去缓存版本中去获取相应地依赖,而不是根据要求的那样去引入。那所有的模块都应当使用的是6.20.0版本,但是因为6.5.2版本的缓存可用,那么就会优先使用6.5.2版本的缓存版本依赖,而6.5.2版本的依赖是用于构建已经过期的项目的。

我通过强制NPM安装最新的依赖版本6.20.0来解决这个错误,但是我明白当我重新安装依赖时,这个问题很有可能会重现,因此我并不满意这个解决方案。

毫无疑问,我删除了node_modules目录,并重新安装,依赖就回到了不匹配的状态。

解决方案

我的同事Boris,推荐我使用Yarn代替NPM来安装依赖。开始我并不相信这样就会解决所有问题,但是我也没有了其他方案了,就姑且试一试吧,毕竟也没有原因证明Yarn并没有什么特别的。

当使用Yarn安装依赖后,构建成功了!我很惊讶!我检查了node_modules目录中安装的是哪一个版本的依赖,结果是仅安装了6.20.0版本。对比NPM,Yarn是根据Commit版本号层级来安装依赖,而NPM仅仅依据的是package.json中的说明。对于怎么安装不同版本的依赖,或者安装最少的不同版本上,Yarn显得更加智能,升级后的依赖版本通常是可以被升级的。

如果一个包有多个版本,而且要求版本已经安装了,NPM就会使用已经安装的版本,而并非指定的版本,这样就会造成NPM偶然跳过安装升级版本。这种版本号的不匹配,无论多小,都是NPM一部分不可接受的错误。事实证明,这种矛盾越来越让人担忧。如果没有好好梳理以花长时间挖掘依赖树,很难发现是哪里出现了问题。

在这个过程中两样东西很明确了:Yarn比NPM要快很多很多。Yarn也不会报出很多警告以及你并不关心的其他输出占满你的终端。

好奇心驱使下,我好好研究了下Yarn是怎样用更少的时间完成同样的任务。

对比来看,NPM发送请求来下载包的时候,会一次全部执行完,并且每个包都是边下载边安装。这意味着,如果你项目中有15个依赖包,就会一个接着一个地下载和安装,无论任一一个包的日志都可能输出到控制台。

然而,Yarn处理这个过程更为精细化了,当Yarn发起下载包请求的时候,会并行执行。如果你项目中有15个依赖,这些依赖会在同一时间全部下载完。当所有依赖全部下载之后,Yarn会安装要求安装的包(并不是所有的模块都需要安装),并且显示在安装该包过程中的任意可能结果。

以下展示的情景就是为什么Yarn更令人满意的原因了:

你正在使用NPM安装包。包会在同一时间下载和安装,中途某个时候,一个包抛出了一个错误,但是NPM会继续下载和安装包。因为NPM会把所有的日志输出到终端,有关错误包的错误信息就会在一大堆NPM打印的警告中丢失掉,并且你甚至永远不会注意到实际发生的错误。

为了避免这个问题,当下载和安装过程完成后,Yarn把错误消息放在了前面。

结论

Yarn提供了处理过程中更多的可见性,而NPM则倾向于将其模糊掉。当使用Yarn做了实验并且发现所提供的好处之后,我们决定做此转变(就像这个案例这么容易)。Yarn是多年来使用NPM管理JS依赖实践后的产品,并且致力于解决JS开发者遇到的诸多问题,而这些问题往往将其归因于“JavaScript工作方式”(而作为一个常见问题存在着而很难理解)。

如果在使用NPM过程中有任何不爽,强烈鼓励你选择Yarn!谢谢阅读。

相关文章