为什么需要Bundler
对于从Node.js转Ruby的人很可能会有和我一样的疑惑,为什么要有Bundler这个东西?Rubygems不够吗?
从Node.js到Ruby的包管理器
在Node的世界里,依赖管理是由npm来完成的。所有依赖信息都写在package.json里面之后,一个npm install
就能安装所有的依赖,然后直接运行程序即可。简单方便。
那当然是方便了,因为Node出来的太晚了,包管理器已经很成熟,所以才成了现在的这个样子。
RubyGems登场
我们把时间倒回去回到Ruby的早期,其包管理器RubyGems被创造出来的时候。在当时,RubyGems的使命其实很简单,就是安装依赖包(Ruby的包有个专门的名字叫做gem)。具体来说是,指定一个包名和版本号,由包管理器来安装指定版本的包以及其所有依赖的包。(当然如果没有指定版本号那当然是默认安装最新的了)。
在这个用例上,npm install xxxx
和 gem install xxxx
的功能是一样的。
只不过在行为上有一点区别。npm默认装在当前目录(在设计的时候就考虑到这个功能),而gem装在系统目录(和pip一样,早期的包管理器都是装在全局的,Ruby的每个包都带有版本号)
在gem里,它的依赖信息都在写在xxx.gemspec文件里(注意不是Gemfile,Bundler的文件)。当你gem install
的时候,gem会根据gemspec里面的依赖信息来获取对应的依赖包。
团队协作的大麻烦
有了gem命令以后,Ruby开发起来就确实方便多了。需要什么包就用gem下一个,然后查查文档就能用了。对于个人开发者来说很好用。
但是一旦有团队协作,问题就来了。现在假设你要接手开发一个Ruby项目,你手上有项目的全部源码。那么首先你要要做的第一步就是安装所有的依赖库,因为你至少要先跑起来嘛!
好,装吧。
npm install
,不对哟,这里是Ruby世界。哦,那么gem install
。对不起,你会得到一个错误告诉你必须至少指定一个gem名称!想想吧,gem是用来装单个gem的,而不能用来装一个项目的依赖。这主要是因为:
-
绝大多数Ruby项目本身不是gem,所以没必要有.gemspec文件(顾名思义这个文件是用来描述gem的元信息的)
-
由于没有.gemspec文件,也就无从记录依赖有哪些了。这导致
gem install
就算想装也不知道依赖的包是什么
当然,你可以说不是有源文件吗,里面不都记录了require了什么东西吗?理论上讲只要把里面require的东西全部装一遍不就好了吗?
可以是可以。但是真的这么做的话会发现另外一个问题:你安装的gem和作者当时用的gem很可能不是一个版本。因为使用默认的gem install
安装的总是最新的gem。那么只要任何一个依赖的gem在其版本更新中变更了现有的API,那么你复现安装的项目几乎必然是有问题的。
Bundler comes and saves the world!
说起来,解决这个问题的方法很简单,只要在最初开发的时候用一个文件记录了项目里用了什么包什么版本就好了嘛!对的,Bundler就是干这个事的。
具体来说,先创建一个Gemfile文件,在里面写上你要的gem名称和版本,然后用bundler install
命令就可以安装所有的依赖了。虽然比npm麻烦了一点,但还算过得去。
普通的项目这样就可以了。但是还有一种情况就是,如果团队开发的项目本身就是一个gem呢?这样就会同时存在.gemspec和Gemfile两个文件,一个记录gem的元信息,一个是bundler要用来安装项目依赖的文件。这样的话每添加一个gem依赖就要同时在两边加(而且不幸的是,这两个文件书写依赖的格式还不太一样,不能简单的复制粘贴)
有没有觉得哪里不对?是的,Gemfile包含的信息其实就是.gemspec的子集,换句话说Gemfile里有的东西.gemspec里也都有。Bundler也想到了这个问题,所以你只要在Gemfile里加上gemspec
这一句,bundler在安装的时候就会自动到.gemspec里面去找依赖,因此Gemfile剩下的就什么也不用写了(当然第一行指定安装源的命令还是要写的)。
版本选择
依赖全都装完了,那我们是不是可以愉快地开始用了呢?还不行,还差一步。现在出现的一个问题是,我们requrie到的gem的版本还是有可能不对的。为什么?
最直接的理由:require不能直接指定版本号!所以当你require一个包的时候,即便你装了Gemfile中指定版本的gem,如果这个gem你的系统里有好几个不同的版本(可能是以前装的),Ruby怎么知道你要依赖哪个?于是只能默认require最新的那一个了。
这当然也不是缺陷,Ruby中Kernel有个方法叫做gem。在require之前调用这个方法可以指定之后require进来的gem要用哪个版本。这个调用在本质上就是找到安装的gem包,然后把它的地址加到$LOAD_PATH里面去(注意仅仅是修改了$LOAD_PATH,包本身还没有被require进来)。下次require这个包的时候,会首先搜索$LOAD_PATH,找到这个包后就加载。这就达到了挑选gem版本的目的。
但这显然不方便啊,require一个包之前还要指定一下版本?那我改了依赖版本怎么办?
Bundler提供了一个解决方案。在require你要的包之前加一句require 'bundler/setup'
即可。
本质上说,require了bundler/setup的这个包的时候,bundler读取Gemfile文件,在你所有依赖的库上调用一下gem方法,以此来指定要用的gem的版本,把它们加到$LOAD_PATH上面去。之后你在require的时候就自然地使用了在Gemfie里指定的包了。你所要做的就是加一句require 'bundler/setup'
而已。
总结
Bundler其实是一个项目依赖管理器,而不是一个包管理器。如果那只是想下一个gem,那么Rubygems足矣。如果你是想管理项目的依赖(gem自身也是一个项目),那么在大多数情况下使用bundler会方便得多。