没有 lock 就等于没办法控制未标明的依赖的版本,我一个 package.json,可能装出不同的两个环境,用户还没办法纠错
这样就需要必须一个一个写明依赖版本,等于升级了我要手改一次 package.json 的版本号,还要自己解决依赖冲突
和 npm2 的树状依赖一样,让人完全无法理解
虽然我本意是来引战的,但是既然精了还是要说明白一点,
npm3 有类似 bundle 的机制,会帮你自动装上符合依赖需求的 gem,但是如果你没有指明依赖版本,例如写了^1.0.0
,实际装上的版本可能是1.0.0
,1.0.1
等等。这就造成在一个地方装的(可用的)版本,不一定可以在另一个地方装上,除非你把整个 node_modules 存下来。bundle 是用 Gemfile.lock 记录的,但 npm3 没有这个机制。
另外,shrinkwrap 虽然可以起到 lock 的作用,但是他是在 package 内的,也就意味着非常容易发生依赖冲突,不明白的可以想想如果在 gemspec 里定死依赖版本会发生什么
首先,没人会去手工编写 package.json
,通常你只需要 npm install PACKAGE_NAME
,npm 会自动安装注册表里该模块的最新版本,并自动更新 package.json
。更新按默认规则会把版本写成 ^x.x.x
的 semver 表达式,当然你可以修改默认的前缀 ^
,比如说换成 ~
之类的。
如果你要安装特定版本,那就 npm install PACKAGE_NAME@version
即可,如果你要升级就把 install
替换成 update
。npm 会自动更新 package.json
。其他的用户 clone 下来之后只需要 npm install
就会按照 package.json
里的规则安装好所有依赖包。所以一个 package.json
装出俩不同的环境这是不对的。
如果某一个模块的依赖和另外的模块产生版本冲突,严格来说不是你(作为使用者的责任),而是该模块的作者没有做好这方面的工作。作为模块的作者,如果要特别限定依赖模块的版本,那就应该在发布前执行 npm shrinkwrap
,这样会产生一个 npm-shrinkwrap.json
文件,其作用和 Gemfile.lock
是类似的。你安装的模块如果有做这件事情,那么 npm 会帮助你处理好冲突的问题。另外 npm outdated
会检查所有依赖包是否有新版本可用。
作为目前世界上最大的软件包生态环境之一,这些问题 npm 不可能不考虑到,诚然它也走过不少弯路,可是喷它之前是不是也该至少读一遍使用说明书呀?
补充一点:原生的 npm 只提供了包管理的最基本功能,有些时候的确也会觉得不够方便。不够 npm 自身也是 node 的一个模块,因此有不少人开发了针对它的增强包。对于管理更新/依赖这件事情,我推荐两个补充的东西:
npm-check: 这个东西可以自动检查 package.json
然后告诉你哪些装了哪些没装,哪些装了但是源码里没用到,哪些可以升级,以及升级是 patch 还是 minor 或者是 major。此外它还提供了一个交互式的傻瓜自动升级命令,可以帮你列出所有待处理的模块,你自己勾选要处理哪些,然后一键搞定所有事情。
GreenKeeper: 这个东西很有意思,它只能和 Github 集成(不排除未来支援更多平台,因为它还很新),其作用是自动帮你监视模块的更新情况,如果有更新,它就把你的 repo clone 下来,然后帮你升级好,接着 PR。你要做的就是 Merge Requests 就好了,全过程都可以直接在 Github 上完成,省心又省力。
想看到效果直接:https://github.com/very-geek/univera,昨晚 greenkeeper 给提交了一个 PR 我还没处理,先留着给看效果。CI 的 failure 请直接无视,因为我还没 push tests……
#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 的深层嵌套目录结构的问题,这也是刚需,怎么就废掉了?
#8 楼 @mizuhashi 我已经没法完全弄明白你的逻辑了,可能是和我不够了解 bundle 有关吧。我只能说自有 npm3 以来,我也做过几个大规模的项目了,就好像上面那个 dump 的例子一样。第一,从未遇到过依赖冲突问题;第二,绝大部分是 flat 的,不能展平的依赖自然就是自身 shrinkwrap 的了,然而这并没有给我带来什么问题。
因此你说你不可理解,但是现实中的痛点你又没办法 show 出来,而我又是天天接触 npm 的算是一个佐证……于是我也不知道说什么了,也许就是一个理论上的“不可理解”吧。
作为一个专业 Ruby 开发者和半吊子 JavaScript 开发者说几句。
第一次用 npm 的时候我也被惊到了,不是因为太好用,而是因为庞大的 node_modules 目录和层层嵌套的包。当时很多包都依赖一些基础库,比如 underscore,然后我自己的项目也需要 underscore,于是很无语的看到 node_modules 变成这个样子:
underscore
`--- dependencies
A
`--- underscore
`--- dependencies
这种情况 underscore 显然被重复安装了。而且如果碰到一个依赖众多的基础库,那它下面的依赖也都重复了…… 我当时就想你个二货怎么不学 Bundler 一样展平依赖呢?
直到后来更加了解 JS 的一些模块加载,我才突然想通了。其实这样做的一大好处是“你不用管你安装的包还有哪些依赖”。
我想很多 Rubyist 都碰到过这样一种事情:你的项目本身和安装的一个 gem A 都依赖 gem B。Gemfile 大概像这样:
gem 'A' # has dependency B
gem 'B'
有天 B 升级了并提供了一些新特性,你想在项目中使用新版的 B。这时你就得确保 A 也能够使用新版的 B,否则就不能升级,Bundler 会提示你 A 依赖的 B 跟你的项目依赖的 B 版本有冲突。你只能使用 A 能够允许的最高版本的 B ,然后也许你就被迫升级 A 去了。如果恰好 A 那时已经没人维护了。你就有更多的事情做了,要么 fork 一个 A 自己维护,要么把你需要的功能抽出来然后 kill your dependency。
其实很多时候,我们需要的不是 锁定项目的每个直接或间接依赖的包的版本 ,而是 保证我们的项目的直接依赖的包接口和行为都是稳定的 。就我的理解,前者是方法,后者是目标。如果做到了前者,后者肯定能做到,但反过来是不成立的。达到目标的方法不止一种。
Bundler 和 npm 其实都是为了后者服务的。但 Bundler 选择的方式是前者(锁定所有包的版本)。这除了处理问题的哲学不同,也跟 Ruby 和 JavaScript 的运行方式有一定关系。
在 Ruby 中,一旦 require
一个模块,那这个模块就存在于全局命名空间中。因此 Ruby 的世界里几乎不可能在一个项目里使用两个包的不同版本(比如 A 1.0.0 和 A 2.0.0),因为它们的很多模块名都是相同的。在这种情况下,一个 Ruby 项目的所有包,不管是直接还是间接依赖,都只允许出现一次,并且还不能有模块重名(这也是为什么每个包都会有命名空间封装一下)。这点不管用不用 Bundler 都一样。只是 Bundler 加上了锁定每个包版本的功能。至于这是不是 Ruby 世界最好的处理方式就见仁见智了。我觉得锁定版本已经是一个很好的方式了。
而 JavaScript 的模块设计都是局部可用的。你在模块 a 中导入另一个模块 b,那 b 就只在 a 中有效,b 也不会被注册到全局命名空间中。这让每个模块有自己独立的依赖成为可能,只要它们从不同的地方去 require 就行了。基于此 npm 的逻辑就是每个包都应该有它自己的依赖,这可以让使用者只关心项目的直接依赖,忽略间接依赖。在达到这个目标的前提下,能共用一些包就共用,不能用就算了。
了解这一点后再来看 npm v2 的处理方式就可以理解了。这货根本没关心包共用和省空间的问题,安装时就是简单粗暴的不断嵌套。npm v3 弄成安装时尽量平铺,需要一个包不同版本就嵌套的方式,我觉得除了 windows 目录层数限制的问题(who care),另一个好处就是可以某种程度上避免空间浪费(重复安装同一个版本的包)。但不管是 v2 还是 v3,他们的核心哲学都是没有变的。
要我说 Bundler 和 npm,它们没有孰优孰劣,只是在各自的平台下做着自己认为对的事 。但我觉得身为开发者是不能被某个平台的设计方式局限的。拿一个的经验当标尺去评价另一个,就像 Windows 用户吐槽 Mac(反过来也是)一样,比较没道理。
最后说一个不锁定版本带来的担忧。我开始也担忧不锁定版本会不会导致我装的包跟别人不一样?其实仔细想想, 我担心的是不同版本的包改变了行为,导致一些不可预知的错误 。这点 npm 用 semver 解决的。semver 规定所有破坏性更新都必须改 major version。而默认加入 package.json 里的包都是锁定 major version 的。如果你依赖的包都是守法公民,那应该是不会出问题的。如果你发现出问题了,那就跟 @nightire 说的一样,当 contributor 的机会来了。
最最后说一句,因为间接依赖的被锁住导致整个系统难以升级,也属于 dependency hell 的一种。我对此是深以为然。这个说法貌似是在 Wikipedia 上查到的,如果不对欢迎指正。
#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 的结果是可重现的
shrinkwrap 在 node 社区很少有人用。不过 node 就这么用下来了。
underscore 有一次 minor version 发布了一个不兼容 api 更改(懒得搜具体 issue 了),被人骂不遵循 semver。不过好像也没有什么后续了。。。
@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
这里,之前想用 qiniu 的 ruby sdk,但是居然因为 mime 的依赖问题无法使用,https://github.com/qiniu/ruby-sdk/pull/135
遇到这种情况,那就真的要么不用,要么 folk 自己搞了
#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
@tangmonk @coolzilj 大部分这种问题都可以通过搜索引擎加合适的关键字解决。随手搜到的:https://lincolnloop.com/blog/speeding-npm-installs/
总之,你得相信自己不是第一个碰到问题的人。
恩。。这只是 cache 而已,不能彻底解决问题。
我试了下最新版的 cnpm, instsall ember-cli 速度为 29s,黑科技
原来的 cnpm 要装 10-20 分钟 (用 taobao 源)
应该是 cnpm 做过一些优化了
做大型项目的时候,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 字段都不会有很大影响。