更多内容请访问 rubyonrails.org:

创建 Rails 插件的基础

本指南适用于希望创建 Rails 插件,以扩展或修改 Rails 应用程序行为的开发者。

阅读本指南后,您将了解

  • 什么是 Rails 插件以及何时使用它们。
  • 如何从零开始创建插件。
  • 如何扩展核心 Ruby 类。
  • 如何向 ApplicationRecord 添加方法。
  • 如何将您的插件发布到 RubyGems。

1. 什么是插件?

Rails 插件是一个打包的扩展,它为 Rails 应用程序添加功能。插件有几个用途:

  • 它们为开发者提供了一种尝试新想法而又不影响核心代码库稳定性的方式。
  • 它们支持模块化架构,允许功能独立维护、更新或发布。
  • 它们为团队提供了一个引入强大功能的途径,而无需将所有内容直接包含到框架中。

在技术层面,插件是一个旨在 Rails 应用程序内部工作的 Ruby gem。它通常使用 Railtie 挂接到 Rails 启动过程,从而允许它以结构化的方式扩展或修改框架的行为。Railtie 是扩展 Rails 最基本的集成点——它通常在您的插件需要添加配置、rake 任务或初始化代码,但不暴露任何控制器、视图或模型时使用。

引擎是一种更高级的插件类型,它行为类似于一个迷你 Rails 应用程序。它可以包含自己的路由、控制器、视图甚至资产。虽然所有引擎都是插件,但并非所有插件都是引擎。主要区别在于范围:插件通常用于较小的自定义或跨应用程序共享的行为,而引擎提供更完整功能的组件,具有自己的路由、模型和视图。

2. 生成器选项

Rails 插件作为 gem 构建。如果需要,它们可以使用 RubyGemsBundler 在不同的 Rails 应用程序之间共享。

rails plugin new 命令支持几个选项,这些选项决定了生成哪种类型的插件结构。

基本插件(默认),不带任何参数,生成一个最小的插件结构,适用于简单的扩展,如核心类方法或实用函数。

$ rails plugin new api_boost

我们将使用基本插件生成器进行本指南。有两个选项,--full--mountable,这在 Rails 引擎指南 中有介绍。

完整插件 (--full) 选项创建一个更完整的插件结构,其中包括一个 app 目录树(模型、视图、控制器)、一个 config/routes.rb 文件和一个位于 lib/api_boost/engine.rb 的引擎类。

$ rails plugin new api_boost --full

当您的插件需要自己的模型、控制器或视图但不需要命名空间隔离时,请使用 --full

可挂载引擎 (--mountable) 选项创建一个完全隔离、可挂载的引擎,它包含 --full 的所有内容,外加:

  • 命名空间隔离(所有类都带有 ApiBoost:: 前缀)
  • 隔离路由 (ApiBoost::Engine.routes.draw)
  • 资产清单文件
  • 命名空间的 ApplicationControllerApplicationHelper
  • 在 dummy 应用程序中自动挂载进行测试
$ rails plugin new api_boost --mountable

在构建可以作为独立应用程序工作的自包含功能时使用 --mountable

有关引擎的更多信息,请参阅 Rails 引擎入门指南

以下是选择正确选项的一些指导:

  • 基本插件:简单实用工具、核心类扩展或小型辅助方法
  • --full 插件:需要模型/控制器但共享主机应用程序命名空间的复杂功能
  • --mountable 引擎:自包含功能,如管理面板、博客或 API 模块

通过寻求帮助查看用法和选项

$ rails plugin new --help

3. 设置

在本指南中,假设您正在构建 API,并希望创建一个插件来添加常见的 API 功能,如请求节流、响应缓存和自动 API 文档。您将创建一个名为“ApiBoost”的插件,它可以增强任何 Rails API 应用程序。

3.1. 生成插件

使用以下命令创建一个基本插件:

$ rails plugin new api_boost

这将在名为 api_boost 的目录中创建 ApiBoost 插件。让我们检查一下生成了什么:

api_boost/
├── api_boost.gemspec
├── Gemfile
├── lib/
│   ├── api_boost/
│   │   └── version.rb
│   ├── api_boost.rb
│   └── tasks/
│       └── api_boost_tasks.rake
├── test/
│   ├── dummy/
│   │   ├── app/
│   │   ├── bin/
│   │   ├── config/
│   │   ├── db/
│   │   ├── public/
│   │   └── ... (full Rails application)
│   ├── integration/
│   └── test_helper.rb
├── MIT-LICENSE
└── README.md

lib 目录包含您的插件源代码

  • lib/api_boost.rb 是您的插件的主要入口点
  • lib/api_boost/ 包含您的插件功能的模块和类
  • lib/tasks/ 包含您的插件提供的任何 Rake 任务

test/dummy 目录包含一个完整的 Rails 应用程序,用于测试您的插件。这个 dummy 应用程序:

  • 通过 Gemfile 自动加载您的插件
  • 提供一个 Rails 环境来测试您的插件集成
  • 根据需要包含生成器、模型、控制器和视图进行测试
  • 可以通过 rails consolerails server 进行交互式使用

Gemspec 文件 (api_boost.gemspec) 定义了您的 gem 的元数据、依赖项和打包时要包含的文件。

3.2. 设置插件

导航到包含插件的目录,并编辑 api_boost.gemspec 以替换任何具有 TODO 值的行:

spec.homepage    = "http://example.com"
spec.summary     = "Enhance your API endpoints"
spec.description = "Adds common API functionality like request throttling, response caching, and automatic API documentation."

...

spec.metadata["source_code_uri"] = "http://example.com"
spec.metadata["changelog_uri"] = "http://example.com"

然后运行 bundle install 命令。

之后,通过导航到 test/dummy 目录并运行以下命令来设置您的测试数据库:

$ cd test/dummy
$ bin/rails db:create

这个 dummy 应用程序就像任何 Rails 应用程序一样工作——您可以生成模型、运行迁移、启动服务器或打开控制台来测试插件的功能。

数据库创建后,返回到插件的根目录 (cd ../..)。

现在您可以使用 bin/test 命令运行测试,您应该会看到:

$ bin/test
...
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

这将告诉您所有内容都已正确生成,并且您可以开始添加功能了。

4. 扩展核心类

本节将解释如何向 Integer 添加一个方法,该方法将在您的 Rails 应用程序中的任何地方可用。

在继续之前,重要的是要理解扩展核心类(如 String、Array、Hash 等)应该谨慎使用,如果可能的话,尽量避免。核心类扩展可能脆弱、危险,而且通常是不必要的。

它们可能:
- 当多个 gem 使用相同的方法名扩展同一个类时,导致命名冲突
- 当 Ruby 或 Rails 更新改变核心类行为时,意外地中断
- 使调试变得困难,因为不清楚方法来自哪里
- 在您的插件和其他代码之间创建耦合问题

更好的替代方案:
- 创建实用模块或辅助类
- 使用组合而非猴子补丁
- 在您自己的类上实现功能作为实例方法

有关核心类扩展为何存在问题的更多详细信息,请参阅 反对猴子补丁的案例

话虽如此,理解核心类扩展的工作原理是很有价值的。下面的例子演示了这种技术,但它们应该谨慎使用。

在此示例中,您将向 Integer 添加一个名为 requests_per_hour 的方法。

lib/api_boost.rb 中,添加 require "api_boost/core_ext"

# api_boost/lib/api_boost.rb

require "api_boost/version"
require "api_boost/railtie"
require "api_boost/core_ext"

module ApiBoost
  # Your code goes here...
end

创建 core_ext.rb 文件,并向 Integer 添加一个方法来定义一个 RateLimit,该方法可以定义 10.requests_per_hour,类似于 10.hours 返回一个 Time。

# api_boost/lib/api_boost/core_ext.rb

ApiBoost::RateLimit = Data.define(:requests, :per)

class Integer
  def requests_per_hour
    ApiBoost::RateLimit.new(self, :hour)
  end
end

要查看实际效果,请切换到 test/dummy 目录,启动 bin/rails console,并测试 API 响应格式:

$ cd test/dummy
$ bin/rails console
irb> 10.requests_per_hour
=> #<data ApiBoost::RateLimit requests=10, per=:hour>

dummy 应用程序会自动加载您的插件,因此您添加的任何扩展都可以立即用于测试。

5. 向 Active Record 添加一个 "acts_as" 方法

插件中一个常见的模式是向模型添加一个名为 acts_as_something 的方法。在这种情况下,您希望编写一个名为 acts_as_api_resource 的方法,该方法向您的 Active Record 模型添加特定于 API 的功能。

假设您正在构建一个 API,并且希望跟踪资源(例如 Product)通过该 API 最后一次访问的时间。您可能希望使用该时间戳来:

  • 节流请求
  • 在您的管理面板中显示“上次活动”时间
  • 优先处理过时的记录进行同步

您无需在每个模型中编写此逻辑,而是可以使用共享插件。acts_as_api_resource 方法将此功能添加到任何模型,允许您通过更新时间戳字段来跟踪 API 活动。

首先,设置您的文件,使您拥有:

# api_boost/lib/api_boost.rb

require "api_boost/version"
require "api_boost/railtie"
require "api_boost/core_ext"
require "api_boost/acts_as_api_resource"

module ApiBoost
  # Your code goes here...
end
# api_boost/lib/api_boost/acts_as_api_resource.rb

module ApiBoost
  module ActsAsApiResource
    extend ActiveSupport::Concern

    class_methods do
      def acts_as_api_resource(api_timestamp_field: :last_requested_at)
        # Create a class-level setting that stores which field to use for the API timestamp.
        cattr_accessor :api_timestamp_field, default: api_timestamp_field.to_s
      end
    end
  end
end

上面的代码使用 ActiveSupport::Concern 简化了包含同时具有类方法和实例方法的模块。class_methods 块中的方法在模块被包含时成为类方法。有关更多详细信息,请参阅 ActiveSupport::Concern API 文档

5.1. 添加一个类方法

默认情况下,此插件要求您的模型具有名为 last_requested_at 的列。但是,由于该列名可能已被用于其他用途,因此该插件允许您自定义它。您可以通过传递不同的列名和 api_timestamp_field: 选项来覆盖默认值。在内部,此值存储在一个名为 api_timestamp_field 的类级别设置中,插件在更新时间戳时使用该设置。

例如,如果您想使用 last_api_call 而不是 last_requested_at 作为列名,您可以执行以下操作:

首先,在您的“dummy”Rails 应用程序中生成一些模型来测试此功能。从 test/dummy 目录运行以下命令:

$ cd test/dummy
$ bin/rails generate model Product last_requested_at:datetime last_api_call:datetime
$ bin/rails db:migrate

现在更新 Product 模型,使其表现得像一个 API 资源:

# test/dummy/app/models/product.rb

class Product < ApplicationRecord
  acts_as_api_resource api_timestamp_field: :last_api_call
end

要使插件可用于所有模型,请将模块包含在 ApplicationRecord 中(我们稍后将讨论如何自动执行此操作):

# test/dummy/app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
  include ApiBoost::ActsAsApiResource

  self.abstract_class = true
end

现在您可以在 Rails 控制台中测试此功能:

irb> Product.api_timestamp_field
=> "last_api_call"

5.2. 添加一个实例方法

此插件向任何调用 acts_as_api_resource 的 Active Record 模型添加一个名为 track_api_request 的实例方法。此方法将配置的时间戳字段的值设置为当前时间(或如果提供则为自定义时间),允许您跟踪 API 请求的发出时间。

要添加此行为,请更新 acts_as_api_resource.rb

# api_boost/lib/api_boost/acts_as_api_resource.rb

module ApiBoost
  module ActsAsApiResource
    extend ActiveSupport::Concern

    class_methods do
      def acts_as_api_resource(options = {})
        cattr_accessor :api_timestamp_field,
                       default: (options[:api_timestamp_field] || :last_requested_at).to_s
      end
    end

    def track_api_request(timestamp = Time.current)
      write_attribute(self.class.api_timestamp_field, timestamp)
    end
  end
end

上面使用 write_attribute 在模型中写入字段只是插件如何与模型交互的一个示例,并非总是正确的用法。例如,您可能更喜欢使用 send,它调用 setter 方法:

send("#{self.class.api_timestamp_field}=", timestamp)

现在您可以在 Rails 控制台中测试该功能:

irb> product = Product.new
irb> product.track_api_request
irb> product.last_api_call
=> 2025-06-01 10:31:15 UTC

6. 高级集成:使用 Railties

我们目前构建的插件对于基本功能来说已经足够了。但是,如果插件需要与 Rails 框架更深层次地集成,您将需要使用 Railtie

当您的插件需要以下情况时,需要 Railtie:

  • 添加可通过 Rails.application.config 访问的配置选项
  • 无需手动设置即可自动将模块包含在 Rails 类中
  • 向主机应用程序提供 Rake 任务
  • 设置在 Rails 启动期间运行的初始化器
  • 向应用程序堆栈添加中间件
  • 配置 Rails 生成器
  • 订阅 ActiveSupport::Notifications

对于像我们这样只扩展核心类或添加模块的简单插件,Railtie 不是必需的。

6.1. 配置选项

假设您希望使 to_throttled_response 方法中的默认速率限制可配置。首先,创建一个 Railtie:

# api_boost/lib/api_boost/railtie.rb

module ApiBoost
  class Railtie < Rails::Railtie
    config.api_boost = ActiveSupport::OrderedOptions.new
    config.api_boost.default_rate_limit = 60.requests_per_hour

    initializer "api_boost.configure" do |app|
      ApiBoost.configuration = app.config.api_boost
    end
  end
end

向您的插件添加一个配置模块:

# api_boost/lib/api_boost/configuration.rb

module ApiBoost
  mattr_accessor :configuration, default: nil

  def self.configure
    yield(configuration) if block_given?
  end
end

更新您的核心扩展以使用配置:

# api_boost/lib/api_boost/core_ext.rb

module ApiBoost
  module ActsAsApiResource
    def to_throttled_json(rate_limit = ApiBoost.configuration.default_rate_limit)
      limit_window = 1.send(rate_limit.per).ago..
      num_of_requests = self.class.where(self.class.api_timestamp_field => limit_window).count
      if num_of_requests > rate_limit.requests
        { error: "Rate limit reached" }.to_json
      else
        to_json
      end
    end
  end
end

在您的主插件文件中引用新文件:

# api_boost/lib/api_boost.rb

require "api_boost/version"
require "api_boost/configuration"
require "api_boost/railtie"
require "api_boost/core_ext"
require "api_boost/acts_as_api_resource"

module ApiBoost
  # Your code goes here...
end

现在,使用您的插件的应用程序可以对其进行配置:

# config/application.rb
config.api_boost.default_rate_limit = "100 requests per hour"

6.2. 自动模块包含

您可以使用 Railtie 自动将 ActsAsApiResource 包含在用户的 ApplicationRecord 中,而不是要求用户手动包含它:

# api_boost/lib/api_boost/railtie.rb

module ApiBoost
  class Railtie < Rails::Railtie
    config.api_boost = ActiveSupport::OrderedOptions.new
    config.api_boost.default_rate_limit = 60.requests_per_hour

    initializer "api_boost.configure" do |app|
      ApiBoost.configuration = app.config.api_boost
    end

    initializer "api_boost.active_record" do
      ActiveSupport.on_load(:active_record) do
        include ApiBoost::ActsAsApiResource
      end
    end
  end
end

ActiveSupport.on_load 钩子确保您的模块在 Rails 初始化期间的正确时间(在 ActiveRecord 完全加载之后)包含。

6.3. Rake 任务

要向使用您的插件的应用程序提供 Rake 任务:

# api_boost/lib/api_boost/railtie.rb

module ApiBoost
  class Railtie < Rails::Railtie
    # ... existing configuration ...

    rake_tasks do
      load "tasks/api_boost_tasks.rake"
    end
  end
end

创建 Rake 任务文件:

# api_boost/lib/tasks/api_boost_tasks.rake

namespace :api_boost do
  desc "Show API usage statistics"
  task stats: :environment do
    puts "API Boost Statistics:"
    puts "Models using acts_as_api_resource: #{api_resource_models.count}"
  end

  def api_resource_models
    ApplicationRecord.descendants.select do |model|
      model.include?(ApiBoost::ActsAsApiResource)
    end
  end
end

现在使用您的插件的应用程序将可以访问 rails api_boost:stats

6.4. 测试 Railtie

您可以在 dummy 应用程序中测试您的 Railtie 是否正常工作:

# api_boost/test/railtie_test.rb

require "test_helper"

class RailtieTest < ActiveSupport::TestCase
  def test_configuration_is_available
    assert_not_nil ApiBoost.configuration
    assert_equal 60.requests_per_hour, ApiBoost.configuration.default_rate_limit
  end

  def test_acts_as_api_resource_is_automatically_included
    assert Class.new(ApplicationRecord).include?(ApiBoost::ActsAsApiResource)
  end

  def test_rake_tasks_are_loaded
    Rails.application.load_tasks
    assert Rake::Task.task_defined?("api_boost:stats")
  end
end

Railties 提供了一种干净的方式来将您的插件与 Rails 的初始化过程集成。有关完整的 Rails 初始化生命周期的更多详细信息,请参阅 Rails 初始化过程指南

7. 测试您的插件

添加测试是一个好习惯。Rails 插件生成器为您创建了一个测试框架。让我们为我们刚刚构建的功能添加测试。

7.1. 测试核心扩展

为您的核心扩展创建测试文件:

# api_boost/test/core_ext_test.rb

require "test_helper"

class CoreExtTest < ActiveSupport::TestCase
  def test_to_throttled_response_adds_rate_limit_header
    response_data = "Hello API"
    expected = { data: "Hello API", rate_limit: 60.requests_per_hour }
    assert_equal expected, response_data.to_throttled_response
  end

  def test_to_throttled_response_with_custom_limit
    response_data = "User data"
    expected = { data: "User data", rate_limit: "100 requests per hour" }
    assert_equal expected, response_data.to_throttled_response("100 requests per hour")
  end
end

7.2. 测试 Acts As 方法

为您的 ActsAs 功能创建测试文件:

# api_boost/test/acts_as_api_resource_test.rb

require "test_helper"

class ActsAsApiResourceTest < ActiveSupport::TestCase
  def test_a_users_api_timestamp_field_should_be_last_requested_at
    assert_equal "last_requested_at", User.api_timestamp_field
  end

  def test_a_products_api_timestamp_field_should_be_last_api_call
    assert_equal "last_api_call", Product.api_timestamp_field
  end

  def test_users_track_api_request_should_populate_last_requested_at
    user = User.new
    freeze_time = Time.current
    Time.stub(:current, freeze_time) do
      user.track_api_request
      assert_equal freeze_time.to_s, user.last_requested_at.to_s
    end
  end

  def test_products_track_api_request_should_populate_last_api_call
    product = Product.new
    freeze_time = Time.current
    Time.stub(:current, freeze_time) do
      product.track_api_request
      assert_equal freeze_time.to_s, product.last_api_call.to_s
    end
  end
end

运行您的测试以确保一切正常:

$ bin/test
...
6 runs, 6 assertions, 0 failures, 0 errors, 0 skips

8. 生成器

通过在插件的 lib/generators 目录中创建生成器,即可将其包含在您的 gem 中。有关生成器创建的更多信息,请参阅 生成器指南

9. 发布您的 Gem

当前正在开发的 Gem 插件可以轻松地从任何 Git 仓库共享。要与他人共享 ApiBoost gem,只需将代码提交到 Git 仓库(如 GitHub),并在相关应用程序的 Gemfile 中添加一行:

gem "api_boost", git: "https://github.com/YOUR_GITHUB_HANDLE/api_boost.git"

运行 bundle install 后,您的 gem 功能将可用于应用程序。

当 gem 准备好作为正式版本发布时,可以将其发布到 RubyGems

或者,您可以利用 Bundler 的 Rake 任务。您可以通过以下方式查看完整列表:

$ bundle exec rake -T

$ bundle exec rake build
# Build api_boost-0.1.0.gem into the pkg directory

$ bundle exec rake install
# Build and install api_boost-0.1.0.gem into system gems

$ bundle exec rake release
# Create tag v0.1.0 and build and push api_boost-0.1.0.gem to Rubygems

有关将 gem 发布到 RubyGems 的更多信息,请参阅:发布您的 gem

10. RDoc 文档

当您的插件稳定后,您可以为其编写文档。第一步是更新 README.md 文件,其中包含有关如何使用插件的详细信息。要包含的一些关键内容是:

  • 您的名字
  • 如何安装
  • 如何将功能添加到应用程序(几个常见用例的示例)
  • 可能帮助用户并节省时间的警告、注意事项或提示

当您的 README.md 文件完善后,请仔细检查并向所有开发者将使用的方法添加 RDoc 注释。通常还会向不属于公共 API 的代码部分添加 # :nodoc: 注释。

当您的注释准备就绪后,导航到您的插件目录并运行:

$ bundle exec rake rdoc


回到顶部