强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Ruby 入门指南 / 第 16 章:Gem 开发与管理

第 16 章:Gem 开发与管理

“Ruby 的强大,一半来自社区的 Gems。”


16.1 Gem 基础

16.1.1 Gem 是什么

Gem 是 Ruby 的包管理格式,包含代码、文档和元数据。

组件说明
.gemspecGem 的元数据文件
lib/源代码目录
bin/可执行文件
spec/test/测试代码
README.md文档
LICENSE.txt许可证
CHANGELOG.md变更日志

16.1.2 Bundler Gem 命令

# 创建新 Gem
bundle gem my_gem

# 生成的结构
my_gem/
├── .github/
├── lib/
│   ├── my_gem.rb
│   └── my_gem/
│       └── version.rb
├── spec/
│   ├── my_gem_spec.rb
│   └── spec_helper.rb
├── .gitignore
├── .rspec
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE.txt
├── my_gem.gemspec
├── Rakefile
└── README.md

16.2 Gemspec 文件

16.2.1 基本结构

# my_gem.gemspec
require_relative "lib/my_gem/version"

Gem::Specification.new do |spec|
  spec.name          = "my_gem"
  spec.version       = MyGem::VERSION
  spec.authors       = ["Your Name"]
  spec.email         = ["[email protected]"]

  spec.summary       = "A short description"
  spec.description   = "A longer description of the gem"
  spec.homepage      = "https://github.com/user/my_gem"
  spec.license       = "MIT"
  spec.required_ruby_version = ">= 3.0.0"

  spec.metadata["homepage_uri"]    = spec.homepage
  spec.metadata["source_code_uri"] = spec.homepage
  spec.metadata["changelog_uri"]   = "#{spec.homepage}/blob/main/CHANGELOG.md"

  # 文件列表
  spec.files = Dir.chdir(__dir__) do
    `git ls-files -z`.split("\x0").reject do |f|
      (File.expand_path(f) == __FILE__) ||
        f.start_with?("spec/", "test/", ".git", ".github", "Gemfile")
    end
  end

  spec.bindir        = "bin"
  spec.executables   = spec.files.grep(%r{\Abin/}) { |f| File.basename(f) }
  spec.require_paths = ["lib"]

  # 依赖
  spec.add_dependency "httparty", "~> 0.21"

  # 开发依赖
  spec.add_development_dependency "rspec", "~> 3.12"
  spec.add_development_dependency "rubocop", "~> 1.50"
end

16.2.2 版本管理

# lib/my_gem/version.rb
module MyGem
  VERSION = "0.1.0"
end

# 语义化版本:MAJOR.MINOR.PATCH
# MAJOR - 不兼容的 API 变更
# MINOR - 向后兼容的新功能
# PATCH - 向后兼容的 Bug 修复
# 预发布版本:1.0.0.beta1, 1.0.0.rc1

16.3 Gem 开发实战

16.3.1 示例:Greeting Gem

# lib/greeting.rb
require_relative "greeting/version"
require_relative "greeting/greeter"

module Greeting
  class Error < StandardError; end

  def self.greet(name, lang: :en)
    Greeter.new(lang).greet(name)
  end
end

# lib/greeting/version.rb
module Greeting
  VERSION = "1.0.0"
end

# lib/greeting/greeter.rb
module Greeting
  class Greeter
    GREETINGS = {
      en: "Hello",
      zh: "你好",
      ja: "こんにちは",
      ko: "안녕하세요",
      fr: "Bonjour"
    }.freeze

    def initialize(lang = :en)
      @lang = lang
    end

    def greet(name)
      greeting = GREETINGS.fetch(@lang) { raise Error, "Unknown language: #{@lang}" }
      "#{greeting}, #{name}!"
    end
  end
end

# spec/greeting_spec.rb
RSpec.describe Greeting do
  it "has a version number" do
    expect(Greeting::VERSION).not_to be_nil
  end

  describe ".greet" do
    it "greets in English" do
      expect(Greeting.greet("World")).to eq("Hello, World!")
    end

    it "greets in Chinese" do
      expect(Greeting.greet("世界", lang: :zh)).to eq("你好, 世界!")
    end

    it "raises error for unknown language" do
      expect { Greeting.greet("test", lang: :xx) }.to raise_error(Greeting::Error)
    end
  end
end

# spec/greeting/greeter_spec.rb
RSpec.describe Greeting::Greeter do
  subject(:greeter) { described_class.new(lang) }

  context "with English" do
    let(:lang) { :en }

    it "returns English greeting" do
      expect(greeter.greet("Alice")).to eq("Hello, Alice!")
    end
  end
end

16.3.2 可执行文件

#!/usr/bin/env ruby
# bin/greeting
require "greeting"

name = ARGV[0] || "World"
lang = (ARGV[1] || "en").to_sym

puts Greeting.greet(name, lang: lang)
chmod +x bin/greeting
./bin/greeting Alice en     # => Hello, Alice!
./bin/greeting 世界 zh      # => 你好, 世界!

16.4 测试和质量

16.4.1 Rake 任务

# Rakefile
require "bundler/gem_tasks"
require "rspec/core/rake_task"
require "rubocop/rake_task"

RSpec::Core::RakeTask.new(:spec)
RuboCop::RakeTask.new

task default: [:rubocop, :spec]

desc "Run tests with coverage"
task :coverage do
  ENV["COVERAGE"] = "true"
  Rake::Task[:spec].invoke
end

desc "Generate documentation"
task :docs do
  sh "rdoc lib/ --title 'MyGem Documentation'"
end

16.4.2 RuboCop 配置

# .rubocop.yml
AllCops:
  TargetRubyVersion: 3.0
  NewCops: enable
  Exclude:
    - 'vendor/**/*'
    - 'tmp/**/*'

Style/Documentation:
  Enabled: false

Metrics/MethodLength:
  Max: 20

Metrics/AbcSize:
  Max: 25

Layout/LineLength:
  Max: 120

16.5 发布 Gem

16.5.1 准备发布

# 1. 确保测试通过
bundle exec rspec

# 2. 检查 gemspec
gem build my_gem.gemspec

# 3. 本地安装测试
gem install ./my_gem-1.0.0.gem

# 4. 检查所有文件
gem specification my_gem-1.0.0.gem

16.5.2 发布到 RubyGems

# 1. 注册 RubyGems 账号
# https://rubygems.org/sign_up

# 2. 登录
gem signin

# 3. 发布
gem push my_gem-1.0.0.gem

# 4. 发布新版本
# 修改 VERSION,重新 build 和 push

# 5. 删除版本(谨慎)
gem yank my_gem -v 1.0.0

16.5.3 发布清单

  • 版本号已更新
  • CHANGELOG 已更新
  • README 完整准确
  • 测试全部通过
  • RuboCop 无警告
  • LICENSE 文件存在
  • gemspec 描述准确
  • 推送到 GitHub
  • 运行 gem build 无错误
  • 运行 gem push

16.6 Bundler 高级用法

16.6.1 Gemfile 高级特性

source "https://rubygems.org"

ruby "3.3.0"

# 条件平台
platforms :ruby do
  gem "pg"
end

platforms :jruby do
  gem "activerecord-jdbcpostgresql-adapter"
end

# Git 依赖
gem "rails", github: "rails/rails", branch: "main"

# 路径依赖(本地开发)
gem "my_local_gem", path: "../my_local_gem"

# 安装钩子
gem "nokogiri", "1.15.4" do |v|
  puts "Installing nokogiri #{v}"
end

# gemspec
gemspec

# 环境分组
group :development do
  gem "rubocop"
end

group :test do
  gem "rspec"
  gem "capybara"
end

group :development, :test do
  gem "pry"
end

# 条件加载
gem "puma" if ENV["USE_PUMA"]

16.6.2 Bundle 配置

# 查看配置
bundle config list

# 设置配置
bundle config set --local without production
bundle config set --global jobs 4

# 镜像源
bundle config set mirror.https://rubygems.org https://gems.ruby-china.com

# 禁用功能
bundle config set disable_checksum_validation true

# 查看配置文件
cat .bundle/config      # 项目配置
cat ~/.bundle/config    # 全局配置

16.7 私有 Gem 源

16.7.1 Gemfury / Gemstash

# Gemfile 中添加私有源
source "https://rubygems.org"
source "https://gems.fury.io/my-org/" do
  gem "private_gem"
end

# 或使用 Gemstash 自建源
source "https://gemstash.mycompany.com/"

16.7.2 私有 Git 仓库

# 使用 GitHub 私有仓库
gem "private_gem", git: "https://github.com/my-org/private_gem.git"

# 使用 SSH
gem "private_gem", git: "[email protected]:my-org/private_gem.git"

# 使用特定标签或提交
gem "private_gem", git: "https://github.com/my-org/private_gem.git", tag: "v1.0.0"

16.8 动手练习

  1. 创建一个工具 Gem
bundle gem json_formatter
# 实现 JSON 格式化命令行工具
  1. 发布一个 Gem
# 创建、测试、发布你的第一个 Gem 到 RubyGems
  1. 配置私有源
# 使用 Gemstash 搭建私有 Gem 源

16.9 本章小结

要点说明
GemspecGem 的元数据定义文件
Bundler依赖管理工具
语义化版本MAJOR.MINOR.PATCH
RubyGemsGem 发布平台
测试RSpec + RuboCop 确保质量
私有源Gemstash / Gemfury 托管私有 Gem

📖 扩展阅读


上一章← 第 15 章:测试驱动开发 下一章第 17 章:Rails 入门 →