新手问题 npm 没有 Gemfile.lock 这样的机制,怎么想的?

mizuhashi · April 19, 2016 · Last by h_minghe replied at September 28, 2016 · 9263 hits
Topic has been selected as the excellent topic by the admin.

没有 lock 就等于没办法控制未标明的依赖的版本,我一个 package.json,可能装出不同的两个环境,用户还没办法纠错

这样就需要必须一个一个写明依赖版本,等于升级了我要手改一次 package.json 的版本号,还要自己解决依赖冲突

和 npm2 的树状依赖一样,让人完全无法理解


虽然我本意是来引战的,但是既然精了还是要说明白一点,

npm3 有类似 bundle 的机制,会帮你自动装上符合依赖需求的 gem,但是如果你没有指明依赖版本,例如写了^1.0.0,实际装上的版本可能是1.0.01.0.1等等。这就造成在一个地方装的(可用的)版本,不一定可以在另一个地方装上,除非你把整个 node_modules 存下来。bundle 是用 Gemfile.lock 记录的,但 npm3 没有这个机制。

另外,shrinkwrap 虽然可以起到 lock 的作用,但是他是在 package 内的,也就意味着非常容易发生依赖冲突,不明白的可以想想如果在 gemspec 里定死依赖版本会发生什么

#1 楼 @nightire 是不是说 package.json 其实相当于 Gemfile.lock。而实际上不存在对应 Gemfile 的文件?

#1 楼 @nightire 我确实不知道 npm install 可以管理 package.json,但是可以换个说法问,严格的说,npm 不会有依赖冲突,因为他的依赖是树形的,所以说到底这还是为何要树形依赖的问题

如果 npm3 像说的一样要搞 flat dependencies,就一定会遇到依赖冲突的问题,如果你用 shrinkwrap 把依赖放在包里,就会有无数的冲突(现在是无限大的依赖树)

gemfile.lock 不是面向包的,是面向工程的

#3 楼 @mizuhashi 不用争论,也不在原理上费尽口舌。npm3 自发布以来我每天都在用,迄今为止没有遇见过“一定会遇到依赖冲突的问题”。你遇到了的话可以给我分享一下,说不定我有机会去给 npm 贡献一下。

#2 楼 @steveltn 我的经验是这样的:你不必去 lock package.json,因为别人克隆下来重现你的环境的过程就是 npm install,那这个命令会依据 package.json 来安装所有的包。如果在你这里没有问题,那在别处也就没问题。(我不排除因为环境差异的问题,因为不是所有的包都有做很严谨的跨平台测试,也不是所有的包在不同平台下都表现一致)。当然你可以用 shrinkwrap 来声明对版本的严格依赖,npm 会根据 npm-shrinkwrap.json 来帮你处理多个版本的依赖,这个才是和 Gemfile.lock 类似的东西。

Ruby 的 Gemfile 里面是不写版本号的,所以 Gemfile.lock 是一个刚需,必须得有。但是 Node 的 package.json 是自带版本号的,而且又不需要手工维护,如果依靠 npm 自身的命令就可以自动维护 package.json 里的版本声明,那么的确没有必要去 lock

而 shrinkwrap 就相当于每一个单独的包自己锁定自己的版本依赖,如果它依赖某其他包的版本和另外的包所依赖的版本有冲突,npm 有一套机制来处理这个过程:https://docs.npmjs.com/cli/shrinkwrap

@mizuhashi 强调 Gemfile.lock 是面向工程而不是面向包的,我不确定他指的什么层面?自己做的叫工程,依赖的别人做的东西叫包?我看这无非就是一个规模大小的问题,我自己做的东西本质还是一个包,只不过可能依赖特别多,规模特别大。我做好的工程如果我愿意,我也可以 publish 到 npm 变成一个别人可以 npm install 的包。当然,如果我这么做的话,我就应该 shrinkwrap 以确保人家用了我的东西的同时如果还依赖的别的东西,那么我的东西会有对版本依赖的强制性声明。但是,如果我做完就好了,只是自己用不用发布出去,那我有 package.json 就够了呀。

#4 楼 @nightire 这有什么费不费口舌的,你不会遇到冲突只是因为你还是树形引入的依赖,shrinkwrap 会强迫依赖以树形引入,这和 flat dependencies 本来就是冲突的,npm3 新增的依赖解决也等于废掉,最后就是https://ruby-china.org/topics/28797,只能说开心就好

#6 楼 @mizuhashi 你声明了 shrinkwrap,对应的模块才需要树形依赖,这是因为整个依赖树里(可能)会有对同一模块的两个不同版本的依赖,这很好理解啊。

其他没有强制依赖的模块还是 flat 安装的,谁说 shrinkwrap 会让你的整个工程全都变成树形依赖?

你举的例子再正常不过了,大工程依赖的东西多自然安装的文件就会多,占的空间就会大,这有什么好奇怪的?

我把我正在做的工程 dump 一下:

$ ls node_modules | wc -l
918
$ du -h node_modules
293M    node_modules

包的数量没差多少,占据的空间有那么极端吗?

最后,展平依赖树除了减少空间占用以外,还有一个原因是为了解决 windows 的深层嵌套目录结构的问题,这也是刚需,怎么就废掉了?

#6 楼 @nightire 近乎废掉啊,你全用 shrinkwrap 还能保持引入包的版本基本一致我只能说我服,然而真正想展平依赖的话就应该像 bundle 一样,删掉 shrinkwrap,加入 lock

npm3 想 resolve dependencies,却又不让你追踪 resolve 的结果,这是不可理解的

当然树形依赖是个见仁见痔的问题,要是觉得这玩意很棒大可全部上 shrinkwrap,我也没法说什么。。

#8 楼 @mizuhashi 我已经没法完全弄明白你的逻辑了,可能是和我不够了解 bundle 有关吧。我只能说自有 npm3 以来,我也做过几个大规模的项目了,就好像上面那个 dump 的例子一样。第一,从未遇到过依赖冲突问题;第二,绝大部分是 flat 的,不能展平的依赖自然就是自身 shrinkwrap 的了,然而这并没有给我带来什么问题。

因此你说你不可理解,但是现实中的痛点你又没办法 show 出来,而我又是天天接触 npm 的算是一个佐证……于是我也不知道说什么了,也许就是一个理论上的“不可理解”吧。

#10 楼 @darkbaby123 嗯,你这么一说我也明白了一些东西,因为我对 Bundler 不是很了解,所以我一直疑惑依赖包的依赖包如果冲突,你完全展平怎么解决?

怪不得那么多人吐槽 npm 的 tree+flat 模式(不只是这里),让你这么一分析就更清楚了,多谢。

在 Ruby 中,一旦 require 一个模块,那这个模块就存在于全局命名空间中。因此 Ruby 的世界里几乎不可能在一个项目里使用两个包的不同版本(比如 A 1.0.0 和 A 2.0.0),因为它们的很多模块名都是相同的。在这种情况下,一个 Ruby 项目的所有包,不管是直接还是间接依赖,都只允许出现一次,并且还不能有模块重名(这也是为什么每个包都会有命名空间封装一下)。这点不管用不用 Bundler 都一样。只是 Bundler 加上了锁定每个包版本的功能。至于这是不是 Ruby 世界最好的处理方式就见仁见智了。 我觉得锁定版本已经是一个很好的方式了。

-

而 JavaScript 的模块设计都是局部可用的。你在模块 a 中导入另一个模块 b,那 b 就只在 a 中有效,b 也不会被注册到全局命名空间中。这让每个模块有自己独立的依赖成为可能,只要它们从不同的地方去 require 就行了。基于此 npm 的逻辑就是每个包都应该有它自己的依赖,这可以让使用者只关心项目的直接依赖,忽略间接依赖。在达到这个目标的前提下,能共用一些包就共用,不能用就算了。

我觉得重点是这里

我们总是喜欢把一个地方总结出来的规律硬生生的套用在别的地方。

#10 楼 @darkbaby123 所以说有 resolve 这个过程就应该提供 resolve 的记录啊,要么你像 npm2 不要 resolve,不然谁知道你 resolve 出来的是什么东西?表面上是把锅推给开发者,实际上是把锅推给用户。我不是来争 tree/flat 孰优孰劣的,是你既然 resolve 了,就必须保证你 resolve 的结果是可重现的

#13 楼 @pynix 惊了,硬生生套用怎么看出来的?

学习了

shrinkwrap 在 node 社区很少有人用。不过 node 就这么用下来了。

underscore 有一次 minor version 发布了一个不兼容 api 更改(懒得搜具体 issue 了),被人骂不遵循 semver。不过好像也没有什么后续了。。。

说实话,我还是喜欢 node 的方式,不同的模块分项目管理。而像 Ruby Python 这样全局管理的,不是很喜欢,没法折腾的玩了

@mizuhashi 我不知道有没有明白你的意思。如果你说的“resolve 结果可重现“是说”每个人 npm install 后的结果都是一样的“ 。那 npm 没法做到。甚至你把自己项目的 node_modules 删了再 npm install 都不一定完全一样。npm v3 里是安装顺序决定了那个包是平铺还是嵌套的。如果要完全可重现,除非锁定每个包的版本号并单独记录,就像 Bundler 的 Gemfile.lock 那样。

我的主要意思是:应该关注直接依赖的包有没有破坏项目的稳定性。而不需要关心 npm install 后的 node_modules 结构是不是一样的,也不用关心安装的包到底是平铺还是嵌套的。在我看来 npm + semver 可以做到这一点,所以我甚至不去折腾 shrinkwrap。举个例子,我不关心 A 的版本是 1.1.0 还是 1.9.3,只要它的行为没有改变就行。锁定版本号当然可以保证 A 的行为始终是一致的,但这不是唯一的方法。

如果某个包有 bug,从 1.0.0 升级到 1.0.1,但是 package.json 里面写的是 '^ 1.0.0',那么就可能有的人安装旧版遇到这个 bug,有的人安装新版没遇到。

My node_modules are in git again https://medium.com/@bestander_nz/my-node-modules-are-in-git-again-4fb18f5671a#.icv7eu7wg

  1. node module 是另一种思维,但一样需要 lock down
  2. shrinkwrap 是 npm 的 lock down
  3. shrinkwrap 没有 bundle 完善,而且社区接受度还不高。
Unknow user #23 April 21, 2016

真的是各有优劣……

Unknow user #24 April 21, 2016

这里,之前想用 qiniu 的 ruby sdk,但是居然因为 mime 的依赖问题无法使用,https://github.com/qiniu/ruby-sdk/pull/135

遇到这种情况,那就真的要么不用,要么 folk 自己搞了

Unknow user #25 April 21, 2016

至今没改……

很多弊端

#20 楼 @darkbaby123 You can set none prefix semver on global or local .npmrc, then every npm install will lock the exact version and no need to lock down versions manually...only if you really care about precise versions. Some new generation project indeed follow this practice like lux, they will review your pr very carefully in order to prevent trivial differences.

最蛋疼的是每次 npm install some-package 都要重新下载。。 而不是和 gem 一样安装 ~/.rvm/gems/ruby-2.1.5/gems

#28 楼 @tangmonk 可能你需要的是 npm install -g。。。

#28 楼 @tangmonk 同感,新项目 install 都好长好长时间,稍微大点的项目依赖也是够受的

@tangmonk @coolzilj 大部分这种问题都可以通过搜索引擎加合适的关键字解决。随手搜到的:https://lincolnloop.com/blog/speeding-npm-installs/

总之,你得相信自己不是第一个碰到问题的人。

#31 楼 @darkbaby123

恩。。这只是 cache 而已,不能彻底解决问题。

我试了下最新版的 cnpm, instsall ember-cli 速度为 29s,黑科技

原来的 cnpm 要装 10-20 分钟 (用 taobao 源)

应该是 cnpm 做过一些优化了

NPM 就是垃圾,依赖地狱

做大型项目的时候,shrinkwrap 是必须的。虽然 npm 的包建议使用 semver 机制,但是你很难保证每个包的开发者都能很好的遵循这个机制。在不锁定的情况下,等于把项目的稳定性交由外部社区管理,这个是多大的风险?被坑过的人都明白这个痛。

shrinkwrap 是根据你当前安装的包的版本来设定的,也就是可能你设置的版本是 ^1.0.0, 但通过 npm install 安装的是 1.0.3,最终实际被写入 shrinkwrap 的也是 1.0.3, 另外有一个 from 字段会是 >=1.0.0 <2.0.0。所以一般生成 shrinkwrap 是你在确定当前的程序能够正常运行以后。如果安装的依赖包版本有冲突,npm 还是会保持树形结构,安装改组件依赖的特定版本;生成的 shrinkwrap 中也列出这个嵌套依赖的特定版本。

另外使用 shrinkwrap 的时候注意生成的 resolved 地址可能是 localhost,这个是因为 npm install 的时候包可能是从你本地缓存中安装的。这样会导致别人拿到你的 shirkwrap 文件以后无法正确安装。所以一种方法是安装插件前用 npm clean 清理下缓存。另一种我经常用的是写一个脚本删除 shrinkwrap 文件中所有的 resolved 字段,这个只要网络正常,有没有 resolved 字段都不会有很大影响。

shrinkwrap 解决版本问题,但是树状结构的依赖使得有些工具不能很好工作,比如 grunt-hord.

Rei in Yarn: A new package manager for JavaScript mention this topic. 12 Oct 01:43
You need to Sign in before reply, if you don't have an account, please Sign up first.