构建一个 Ruby Gem 第六章 版本

bunder 这个 gem 使得 ruby 的依赖管理比起几年前要容易多了。Bunder 强势集成进了 Rails,但是也可以在任何 Ruby 程序中使用。 对于我写的几个 Sinatra 程序,我通常写一个已经集成了 bundler 的自定义模板

如果你熟悉 Rails,你大概能认出下面的 Gemfile

source 'https://rubygems.org'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '4.0.2'

# Use SCSS for stylesheets
gem 'sass-rails', '~> 4.0.0'

# Use Uglifier as compressor for JavaScript assets
gem 'uglifier', '>= 1.3.0'

# Use CoffeeScript for .js.coffee assets and views
gem 'coffee-rails', '~> 4.0.0'

有一件事需要注意那就是每一个 gem 都限制了一个特别的版本。如果这个版本没有被限制,程序有可能不能正常工作。

大概你已经知道了,一旦一个 gem 被加到 Gemfile 中,它可以被通过在程序的根目录运行 bundle install 来安装,或者仅仅运行 bundle (install 是默认命令)

语义化版本

总体来说,Ruby 社区遵从 语义化版本 (后面再细说)。语义化版本给我们指导当我们打破了我们的代码的接口时。Ruby 核心团队最近宣布了 Ruby 将会在 2.1.0 之后遵从语义化版本

Rails 官方没有遵从语义化版本,但是它倾向与把大型的特性改变归并在某个版本。

所以什么是语义化版本呢?

有整部的书关于版本实践,所以我不想对你啰嗦了。语义化版本的网站 是至今最好的学习资源。然而, 宏观上看,破坏性的常见应该被限制在特定的发布版本中。

比如, 假设我们维护一个当前版本是 1.5 的 gem. 如果我们打算加入一个功能而不影响(这点很重要)gem的其他部分的工作方式, 我们应该发布一个次要的版本(在原来的基础上递增), 版本 1.6 .

假设我们在1.6版本增加了新的功能, 我们之后意识到新的代码有一个bug. 我们修复了这个bug并且随后发布了一个补丁版本, 应该是 1.6.1 .

在一年的开发后, 我们认识到gem的某个部分非常糟糕并且我们想要重构. 在重构的过程中, 我们已经决定永久的改变公共接口. 因为这些改变不会向后兼容, 我们将会发布一个新的主版本 2.0.0.

让我们看看我们如何在我们的 gem 中运用这些.

没有版本

在 Gemfile 中加入一个没有指定版本的 gem 是符合语法的:

source 'https://rubygems.org'
gem 'rails'

运行 bundle install 的结果将会根据是否特定版本已经被安装了而不同。规则就像这样:

可以很容易地看到,取决于系统可能会产生不同的结果。编写应用程序已经很难了,就不要因为指定没有版本限制的依赖性而引进更多的挑战了。

准确的版本

这个 gemfile 是一个新的 Rails 4.0.2 程序生成的。因此Gemfile的第一行是这样的。

gem 'rails', '4.0.2'

指定版本作为一个额外的参数将会使用并且只使用那个版本。这种方式的问题就是经常会出现依赖同一个 gem 的不同版本。 如果由于某些原因我们安装了两个 gem 依赖于 multi_json 这个gem,但是每一个都指定了不同的版本,我们就有可能碰到问题并且得到意外的结果。

由于很多程序都依赖于一些 gems,依赖树就会变得复杂并且难以管理,我并不是说指定一个特定的 gem 版本是不好的,因为它不是。 任何事物都有它适用的情况。但是如果你觉的要这样做,确保你有一个好的理由。否则,我建议使用下面的方法之一。

乐观版本约束

进一步来说,我们可以使用下面的语法来指定一个某个版本的 gem 是被允许使用:

gem 'sucker_punch', '>= 1.0'

如果你知道一个 gem 的某些特性或者改变,并且你想要确保用户不使用之前的版本,使用上面的语法是有效的。

我还没见过一个 Rubyist 有时间和精力来跟踪他们使用的 gem 的变化。我们作为开发者的责任是确保我们的程序可以像预期的那样工作。 当我们依赖第三方的代码时,一切都变了。人们决定停止不停的工作。他们也通常会低调的发布新的版本。

这意味着如果我们使用上面的语法在 Gemfile 中并且接着打算发布 sucker_punch 2.0 而不向后兼容,那就糟糕了。 虽然这比不指定版本要好点,但是我们能做的更好。

悲观版本约束

正如我们在语义化版本中所看到的,在版本之间有一个安全地带。我们可以假设在下一个主版本之前没有不向后兼容的问题。 然而,如果我们依赖版本 1.2 或更高,版本 2.0 会弄坏我们的程序。

使用 『悲观』版本约束:

gem 'sass-rails', '~> 4.0.0'

这约束了 sass-rails 的版本在 4.0.04.1.0 之间。正如我们在语义化版本中看到的,一个补丁的发布是用来修复 bug 的。 如果某一个bug在 4.0.0 并且我们没有看到他们的发布通告,我们可能会受到影响。上面的语法允许 sass-rails gem 被更新当版本 4.0 的补丁被发布时。

在使用这个语法时版本的小数是很重要的。比如,假设上面的写法变成:

gem 'sass-rails', '~> 4.0'

这样的写法表示我愿意接受 sass-rails 这个 gem 的任何补丁(比如 4.0.1) 或者次要版本的发布 (比如 4.1, 4.2)。

虽然语法的改变很小,但是它对在依赖时的行为造成了巨大的变化。有了这样的方式,让我们来看看它是如何影响我们的(作为 gem 的作者)。

Gemspec 依赖

在第二章中,我们加入了一些开发时的依赖到我们的 gem spec:

spec.add_development_dependency "bundler", "~> 1.3"
spec.add_development_dependency "rake"
spec.add_development_dependency "rspec"
spec.add_development_dependency "pr

注意默认的 gemspec 建议 bundler 允许 bunlder 被更新到 1.4 或者 1.5=,但不是主版本 (=2.0)。我们可以照着处理我们的 gem 的生产环境和开发环境的依赖。

因为开发时依赖是关于本地开发的体验,所以宽松的处理是没什么问题的。最糟糕的情况就是一个贡献者会在开发时碰到一些问题。如果这是个问题的话,希望他们会提一个 issus 或者最好提一个 pull request!

注意:当我们加入依赖时,花点时间去看看这个依赖和作者是否遵循了语义化版本。 但愿他们在他们的 gem 的 README 中说明了这一点(jquery.turbolinks 是一个很好的例子),否则就问问他们。 这样,你就能搞清楚可能会打破变化的版本是如何被发布的并且可以更好的加入依赖到你的系统中。 如果你不满足于你找到的信息,也许硬编码一个指定的版本或者找到一个替代的 gem 是更好的做法。

总结

当开始一个新的 gem 时,我推荐使用语义化版本。如果你没这样做,你要在 REAMDE 中说明这一点这样用户可以明确的知道。 社区越是接受语义化版本,我们的版本标准就越可靠。很多人都不会在版本号上想太多,仅仅修复一个 bug 就发布一个新版本。

语义化版本的好处不仅仅在于小的 bug 修复,还在于建立了一个可控的版本系统,这样的话你的 gem 的用户可以从中获益。这是一个很小的代价,但获得了可靠性和可预测性。 如需其他材料,RubyGems有一个关于声明依赖关系的指南很值得一读。

在下一章中,我们将看到一个 gem 是如何受益于拥有一个更改日志,和哪些信息是值得记录的。