RubyGems 导航菜单
指南

一些常见的做法,可以使你的 gem 用户和其他开发人员的生活更轻松。

一致的命名

计算机科学中只有两件难事:缓存失效和命名事物。 -Phil Karlton

文件名

对 gem 文件在 libbin 中的命名方式保持一致。来自 创建自己的 gem 指南的 hola gem 就是一个很好的例子。

% tree
.
├── Rakefile
├── bin
│   └── hola
├── hola.gemspec
├── lib
│   ├── hola
│   │   └── translator.rb
│   └── hola.rb
└── test
    └── test_hola.rb

可执行文件和 lib 中的主要文件具有相同的名称。开发人员可以轻松地跳入并调用 require 'hola' 而不出现任何问题。

命名你的 gem

为你的 gem 命名很重要。在你为你的 gem 选择名称之前,请在 RubyGems.orgGitHub 上快速搜索一下,看看是否有人已经使用了这个名称。每个发布的 gem 必须拥有一个唯一的名称。当你找到一个你喜欢的名称后,请务必阅读我们的 命名建议

语义版本控制

版本控制策略仅仅是一套简单的规则,用于管理版本号的分配。它可以非常简单(例如,版本号是一个从 1 开始的单一数字,每次发布新版本时递增),也可以非常奇怪(Knuth 的 TeX 项目的版本号为:3、3.1、3.14、3.141、3.1415;每个后续版本都会在 PI 中添加一位数字)。

RubyGems 团队敦促 gem 开发者遵循 语义化版本控制 标准来管理其 gem 的版本。RubyGems 库本身并不强制执行严格的版本控制策略,但使用“非理性”的策略只会不利于社区中使用你的 gem 的人。

假设你有一个名为“stack”的 gem,它包含一个具有 pushpop 功能的 Stack 类。如果你使用语义化版本控制,你的 CHANGELOG 可能看起来像这样

  • 版本 0.1.0:发布了初始的 Stack 类。
  • 版本 0.2.0:切换到链表实现,因为它更酷。
  • 版本 0.3.0:添加了 depth 方法。
  • 版本 1.0.0:添加了 top,并使 pop 返回 nilpop 以前返回旧的顶部元素)。
  • 版本 1.1.0push 现在返回被推入的值(以前返回 nil)。
  • 版本 1.1.1:修复了链表实现中的一个 bug。
  • 版本 1.1.2:修复了上一个修复中引入的一个 bug。

语义化版本控制归结为

  • 补丁 0.0.x 级别的更改用于实现级别的细节更改,例如小的 bug 修复
  • 次要 0.x.0 级别的更改用于任何向后兼容的 API 更改,例如新功能/特性
  • 主要 x.0.0 级别的更改用于向后不兼容的 API 更改,例如如果用户更新,这些更改将破坏现有用户的代码

声明依赖项

Gems 与其他 Gems 协同工作。以下是一些确保它们彼此友好的提示。

运行时与开发

RubyGems 提供两种主要的依赖项“类型”:运行时和开发。运行时依赖项是您的 Gem 运行所需的内容(例如,rails 需要 activesupport)。

开发依赖项在有人想要修改您的 Gem 时很有用。当您指定开发依赖项时,另一个开发人员可以运行 gem install --dev your_gem,RubyGems 将获取两组依赖项(运行时和开发)。典型的开发依赖项包括测试框架和构建系统。

在您的 gemspec 中设置依赖项很容易。只需使用 add_runtime_dependencyadd_development_dependency

Gem::Specification.new do |s|
  s.name = "hola"
  s.version = "2.0.0"
  s.add_runtime_dependency "daemons",
    ["= 1.1.0"]
  s.add_development_dependency "bourne",
    [">= 0"]

不要在您的 Gem 中使用 gem

您可能在周围看到过一些这样的代码,以确保您使用的是特定版本的 Gem

gem "extlib", ">= 1.0.8"
require "extlib"

对于使用 Gems 的应用程序来说,这样做是合理的(尽管它们也可以使用像 Bundler 这样的工具)。Gems 本身不应该这样做。相反,它们应该在 gemspec 中使用依赖项,以便 RubyGems 可以处理加载依赖项,而不是用户。

悲观版本约束

如果您的 Gem 按照其版本方案正确遵循 语义版本控制,那么其他 Ruby 开发人员在选择版本约束以锁定其应用程序中的 Gem 时可以利用这一点。

假设 Gem 存在以下版本

  • 版本 2.1.0 — 基线
  • 版本 2.2.0 — 引入了一些新的(向后兼容)功能。
  • 版本 2.2.1 — 删除了一些错误
  • 版本 2.2.2 — 简化了您的代码
  • 版本 2.3.0 — 更多新功能(但仍然向后兼容)。
  • 版本 3.0.0 — 重做了界面。为版本 2.x 编写的代码可能无法工作。

您想使用一个 Gem,并且您已经确定版本 2.2.0 与您的软件兼容,但版本 2.1.0 没有您需要的功能。在您的 Gem(或 Bundler 的 Gemfile)中添加依赖项可能看起来像这样

# gemspec
spec.add_runtime_dependency 'library',
  '>= 2.2.0'

# bundler
gem 'library', '>= 2.2.0'

这是一个“乐观”的版本约束。它表示所有大于或等于 2.2.0 的版本都与您的软件兼容。

但是,您可能知道 3.0 引入了重大更改,不再兼容。指定此方法是“悲观”的。这明确排除了可能破坏您的代码的版本

# gemspec
spec.add_runtime_dependency 'library',
  ['>= 2.2.0', '< 3.0']

# bundler
gem 'library', '>= 2.2.0', '< 3.0'

RubyGems 为此提供了一个快捷方式,通常称为 twiddle-wakka

# gemspec
spec.add_runtime_dependency 'library',
  '~> 2.2'

# bundler
gem 'library', '~> 2.2'

请注意,我们删除了版本号中的 PATCH 级别。如果我们写成 ~> 2.2.0,它将等同于 ['>= 2.2.0', '< 2.3.0']

如果您想允许使用更新的向后兼容版本,但需要特定的错误修复,您可以使用复合需求。

# gemspec
spec.add_runtime_dependency 'library', '~> 2.2', '>= 2.2.1'

# bundler
gem 'library', '~> 2.2', '>= 2.2.1'

这里需要注意的是,要意识到其他人使用您的 gem,因此,如果可能的话,请使用 ~> 而不是 >= 来保护自己免受未来版本中潜在的错误/故障的影响。

如果您在应用程序中处理大量 gem 依赖项,我们建议您查看 BundlerIsolate,它们在管理许多 gem 的复杂版本清单方面做得很好。

同样重要的是要知道,如果您只指定一个主版本,例如

# gemspec
spec.add_runtime_dependency 'library', '~> 2'

它只会使用 2.x 系列中的最新版本 - 也就是 2.3.0 - 而不是 3.0.0。这种行为可能会让一些人感到惊讶,但自动允许任何超过版本 2 的主版本的行为更令人惊讶。

您还可以使用 != 排除特定版本。假设版本 2.2.1 存在一个致命的错误,或者一个意外破坏了向后兼容性的更改,从而破坏了您的 gem。您可以按如下方式将其排除

# gemspec
spec.add_runtime_dependency 'library', '~> 2', '!= 2.2.1'

您可以通过将它们作为 add_runtime_dependency 的附加参数添加来追加其他版本 - 毕竟,它的最后一个参数只是一个数组。

预发布依赖项

使用稳定需求时,Bundler 会“优先”使用稳定 gem,只有在必要时才会使用预发布 gem。

但是,如果您想使用预发布 gem,那么您可以使用非数字字符声明预发布需求

# gemspec
spec.add_runtime_dependency 'library', '>= 2.0.0.a', '< 2.0.0'

当给出预发布需求时,Bundler 会尊重该 gem 的实际语义版本优先级。

要求 RubyGems

总结:不要。

这行代码...

require 'rubygems'

...在您的 gem 代码中应该是不必要的,因为当 gem 被加载时,RubyGems 已经加载了。在您的代码中没有 require 'rubygems' 意味着 gem 可以很容易地使用,而无需运行 RubyGems 客户端。

有关更多信息,请查看 Ryan Tomayko 关于该主题的原始文章。

加载代码

RubyGems 的核心功能是帮助你管理 Ruby 的 $LOAD_PATH,这是 require 语句加载新代码的方式。你可以采取一些措施来确保代码以正确的方式加载。

尊重全局加载路径

打包 gem 文件时,你需要注意 lib 目录中的内容。你安装的每个 gem 都会将它的 lib 目录追加到你的 $LOAD_PATH 中。这意味着 lib 目录顶层的任何文件都可能被加载。

例如,假设我们有一个名为 foo 的 gem,其结构如下:

.
└── lib
    ├── foo
    │   └── cgi.rb
    ├── erb.rb
    ├── foo.rb
    └── set.rb

这看起来可能无害,因为你的自定义 erbset 文件位于你的 gem 中。但是,这并不无害,任何需要这个 gem 的人将无法使用 Ruby 标准库提供的 ERBSet 类。

解决这个问题的最佳方法是将文件放在 lib 下的另一个目录中。通常的做法是保持一致,将它们放在与你的 gem 名字相同的文件夹中,例如 lib/foo/cgi.rb

相对加载文件

gem 不应该使用 __FILE__ 来加载 gem 中的其他 Ruby 文件。像这样的代码在 gem 中非常常见

require File.join(
          File.dirname(__FILE__),
          "foo", "bar")

或者

require File.expand_path(File.join(
          File.dirname(__FILE__),
          "foo", "bar"))

解决方法很简单,只需相对于加载路径加载文件即可

require 'foo/bar'

或者使用 require_relative

require_relative 'foo/bar'

创建自己的 gem 指南中,有一个很好的示例展示了这种行为在实践中的应用,包括一个可用的测试套件。该 gem 的代码也托管在 GitHub 上。

修改加载路径

gem 不应该修改 $LOAD_PATH 变量。RubyGems 会为你管理它。像这样的代码是不必要的

lp = File.expand_path(File.dirname(__FILE__))
unless $LOAD_PATH.include?(lp)
  $LOAD_PATH.unshift(lp)
end

或者

__DIR__ = File.dirname(__FILE__)

$LOAD_PATH.unshift __DIR__ unless
  $LOAD_PATH.include?(__DIR__) ||
  $LOAD_PATH.include?(File.expand_path(__DIR__))

当 RubyGems 激活一个 gem 时,它会将你的包的 lib 文件夹添加到 $LOAD_PATH 中,以便其他库或应用程序可以正常加载它。你可以安全地假设你可以在你的 lib 文件夹中加载任何文件。

预发布 gems

许多宝石开发者在发布大型宝石之前,都准备好了宝石的测试版或“边缘”版本。RubyGems 支持“预发布”版本的概念,这些版本可以是测试版、alpha 版或任何尚未准备好作为正式版本发布的版本。

利用这一点很容易。您只需要在宝石版本中添加一个或多个字母即可。例如,以下是一个预发布 gemspec 的 version 字段示例:

Gem::Specification.new do |s|
  s.name = "hola"
  s.version = "1.0.0.pre"

其他预发布版本号可能包括 2.0.0.rc11.5.0.beta.3。它只需要包含一个字母,您就完成了。然后可以使用 --pre 标志安装这些宝石,如下所示:

% gem list factory_bot -r --pre

*** REMOTE GEMS ***

factory_bot (2.0.0.beta2, 2.0.0.beta1)
factory_bot_rails (1.1.beta1)

% gem install factory_bot --pre
Successfully installed factory_bot-2.0.0.beta2
1 gem installed

致谢

本指南的内容参考了多个来源。