野外 Rails 引擎指南:Rails 引擎的实际应用示例
已发表: 2022-03-11为什么不经常使用 Rails 引擎? 我不知道答案,但我确实认为“一切都是引擎”的概括隐藏了他们可以帮助解决的问题域。
Rails 引擎入门的优秀 Rails 指南文档引用了四个流行的 Rails 引擎实现示例:Forem、Devise、Spree 和 RefineryCMS。 这些是引擎的真实世界用例,每个用例都使用不同的方法与 Rails 应用程序集成。
检查这些 gem 的部分配置和组合方式将为高级 Ruby on Rails 开发人员提供宝贵的知识,让他们了解在野外尝试和测试了哪些模式或技术,所以到时候你可以有一些额外的选项来评估。
我确实希望您对 Engine 的工作原理有一个粗略的了解,所以如果您觉得有些东西不太合乎情理,请仔细阅读最优秀的 Rails 指南Getting Started With Engines 。
事不宜迟,让我们冒险进入 Rails 引擎示例的狂野世界!
前锋
旨在成为有史以来最好的小型论坛系统的 Rails 引擎
这颗宝石完全遵循 Rails Guide on Engines 的方向。 这是一个相当大的示例,仔细阅读它的存储库将使您了解可以将基本设置延伸到多远。
它是一个单引擎 gem,使用多种技术与主应用程序集成。
module ::Forem class Engine < Rails::Engine isolate_namespace Forem # ... config.to_prepare do Decorators.register! Engine.root, Rails.root end # ... end end
这里有趣的部分是Decorators.register!
类方法,由装饰器 gem 公开。 它封装了不会包含在 Rails 自动加载过程中的加载文件。 你可能还记得在开发模式下使用显式的require
语句会破坏自动重新加载,所以这是一个救命稻草! 使用指南中的示例来说明正在发生的事情会更清楚:
config.to_prepare do Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c| require_dependency(c) end end
Forem 配置的大部分魔力发生在Forem
的顶部主模块定义中。 该文件依赖于在初始化文件中设置的user_class
变量:
Forem.user_class = "User"
您可以使用mattr_accessor
完成此操作,但它都在 Rails 指南中,所以我不会在这里重复。 有了这个,Forem 然后用运行应用程序所需的一切来装饰用户类:
module Forem class << self def decorate_user_class! Forem.user_class.class_eval do extend Forem::Autocomplete include Forem::DefaultPermissions has_many :forem_posts, :class_name => "Forem::Post", :foreign_key => "user_id" # ... def forem_moderate_posts? Forem.moderate_first_post && !forem_approved_to_post? end alias_method :forem_needs_moderation?, :forem_moderate_posts? # ...
结果是很多! 我已经剪掉了大部分,但是留下了一个关联定义以及一个实例方法来向您展示您可以在其中找到的行的类型。
浏览整个文件可能会向您展示将应用程序的一部分移植到引擎以重用的可管理性。
Decorating 是默认引擎使用中的游戏名称。 作为 gem 的最终用户,您可以通过使用装饰器 gem README 中列出的文件路径和文件命名约定创建自己的类版本来覆盖模型、视图和控制器。 不过,这种方法需要付出一定的代价,尤其是当引擎进行主要版本升级时——保持装饰工作的维护可能很快就会失控。 我在这里没有引用 Forem,我相信他们坚定地保持紧密的核心功能,但如果你创建一个引擎并决定进行大修,请记住这一点。
让我们回顾一下:这是默认的 Rails 引擎设计模式,依赖于最终用户装饰视图、控制器和模型,以及通过初始化文件配置基本设置。 这适用于非常集中和相关的功能。
设计
Rails 的灵活身份验证解决方案
您会发现 Engine 与 Rails 应用程序非常相似,具有views
、 controllers
和models
目录。 Devise 是封装应用程序并公开方便集成点的一个很好的例子。 让我们来看看它到底是如何工作的。
如果您已经成为 Rails 开发人员超过几周,您会认出这些代码行:
class User < ActiveRecord::Base # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable end
传递给devise
方法的每个参数都代表设计引擎中的一个模块。 这些模块中共有十个继承自熟悉的ActiveSupport::Concern
。 这些通过在其范围内调用devise
方法来扩展您的User
类。
拥有这种类型的集成点非常灵活,您可以添加或删除任何这些参数来更改您需要引擎执行的功能级别。 这也意味着您不需要按照 Rails Guide on Engines 的建议,在初始化文件中硬编码您想使用的模型。 换句话说,这不是必需的:
Devise.user_model = 'User'
这种抽象还意味着您可以将其应用于同一应用程序中的多个用户模型(例如admin
和user
),而配置文件方法将使您绑定到具有身份验证的单个模型。 这不是最大的卖点,但它说明了解决问题的不同方法。
Devise 使用自己的模块扩展了ActiveRecord::Base
,该模块包括devise
方法定义:
# lib/devise/orm/active_record.rb ActiveRecord::Base.extend Devise::Models
任何从ActiveRecord::Base
继承的类现在都可以访问在Devise::Models
中定义的类方法:
#lib/devise/models.rb module Devise module Models # ... def devise(*modules) selected_modules = modules.map(&:to_sym).uniq # ... selected_modules.each do |m| mod = Devise::Models.const_get(m.to_s.classify) if mod.const_defined?("ClassMethods") class_mod = mod.const_get("ClassMethods") extend class_mod # ... end include mod end end # ... end end
(我删除了很多代码( # ...
)以突出显示重要部分。)
解释代码,对于传递给devise
方法的每个模块名称,我们是:
- 加载我们指定的位于
Devise::Models
(Devise::Models.const_get(m.to_s.classify
) 下的模块 - 使用
ClassMethods
模块扩展User
类,如果它有一个 - 包含指定的模块 (
include mod
) 以将其实例方法添加到调用devise
方法 (User
) 的类中
如果你想创建一个可以以这种方式加载的模块,你需要确保它遵循通常的ActiveSupport::Concern
接口,但在Devise:Models
下命名它,因为这是我们要检索常量的地方:
module Devise module Models module Authenticatable extend ActiveSupport::Concern included do # ... end module ClassMethods # ... end end end end
呸。
如果您以前使用过 Rails 的关注点并体验过它们提供的可重用性,那么您会欣赏这种方法的精妙之处。 简而言之,通过从ActiveRecord
模型中抽象出来,以这种方式分解功能使测试更容易,并且在扩展功能时比 Forem 使用的默认模式具有更低的开销。
此模式包括将您的功能分解为 Rails 关注点并公开配置点以在给定范围内包含或排除这些问题。 以这种方式形成的引擎对最终用户来说很方便——这是 Devise 成功和普及的一个因素。 现在你也知道该怎么做了!
狂欢
一个完整的 Ruby on Rails 开源电子商务解决方案
Spree 付出了巨大的努力来控制他们的单体应用程序,并转向使用引擎。 他们现在使用的架构设计是一个包含许多引擎 gem 的“Spree” gem。
这些引擎在您可能习惯于在单体应用程序中看到的行为中创建分区,或者在应用程序中分散:
- spree_api (RESTful API)
- spree_frontend(面向用户的组件)
- spree_backend(管理区域)
- spree_cmd(命令行工具)
- spree_core(Models & Mailers,Spree 的基本组件,它离不开它)
- spree_sample(样本数据)
包含的 gem 将这些组合在一起,使开发人员可以选择所需的功能级别。 例如,您可以只使用spree_core
引擎并围绕它包装您自己的界面。
主要的 Spree gem 需要这些引擎:
# lib/spree.rb require 'spree_core' require 'spree_api' require 'spree_backend' require 'spree_frontend'
然后每个 Engine 需要自定义其engine_name
和root
路径(后者通常指向顶级 gem)并通过挂钩到初始化过程来配置自己:
# api/lib/spree/api/engine.rb require 'rails/engine' module Spree module Api class Engine < Rails::Engine isolate_namespace Spree engine_name 'spree_api' def self.root @root ||= Pathname.new(File.expand_path('../../../../', __FILE__)) end initializer "spree.environment", :before => :load_config_initializers do |app| app.config.spree = Spree::Core::Environment.new end # ... end end end
你可能认识也可能不认识这个初始化方法:它是Railtie
的一部分,是一个钩子,让你有机会在 Rails 框架的初始化中添加或删除步骤。 Spree 严重依赖这个钩子来为其所有引擎配置其复杂的环境。

在运行时使用上面的示例,您将可以通过访问顶级Rails
常量来访问您的设置:
Rails.application.config.spree
有了上面的 Rails 引擎设计模式指南,我们可以收工了,但是 Spree 有大量令人惊叹的代码,所以让我们深入了解他们如何利用初始化在引擎和主要 Rails 应用程序之间共享配置。
Spree 有一个复杂的偏好系统,它通过在初始化过程中添加一个步骤来加载它:
# api/lib/spree/api/engine.rb initializer "spree.environment", :before => :load_config_initializers do |app| app.config.spree = Spree::Core::Environment.new end
在这里,我们将一个新的Spree::Core::Environment
实例附加到app.config.spree
。 在 rails 应用程序中,您将能够通过Rails.application.config.spree
从任何地方访问它——模型、控制器、视图。
继续往下看,我们创建的Spree::Core::Environment
类如下所示:
module Spree module Core class Environment attr_accessor :preferences def initialize @preferences = Spree::AppConfiguration.new end end end end
它将:preferences
变量设置为Spree::AppConfiguration
类的新实例,该实例又使用Preferences::Configuration
类中定义的首preference
方法来设置具有常规应用程序配置默认值的选项:
module Spree class AppConfiguration < Preferences::Configuration # Alphabetized to more easily lookup particular preferences preference :address_requires_state, :boolean, default: true # should state/state_name be required preference :admin_interface_logo, :string, default: 'logo/spree_50.png' preference :admin_products_per_page, :integer, default: 10 preference :allow_checkout_on_gateway_error, :boolean, default: false # ... end end
我不会显示Preferences::Configuration
文件,因为它需要一些解释,但本质上它是获取和设置首选项的语法糖。 (事实上,这是对其功能的过度简化,因为首选项系统将保存数据库中现有或新首选项的非默认值,对于任何具有:preference
列的ActiveRecord
类 - 但您不需要我知道。)
以下是其中一个选项:
module Spree class Calculator < Spree::Base def self.calculators Rails.application.config.spree.calculators end # ... end end
计算器控制着 Spree 中的各种事情——运输成本、促销、产品价格调整——因此拥有一种以这种方式交换它们的机制可以增加引擎的可扩展性。
您可以覆盖这些首选项的默认设置的众多方法之一是在主 Rails 应用程序的初始化程序中:
# config/initializergs/spree.rb Spree::Config do |config| config.admin_interface_logo = 'company_logo.png' end
如果您阅读过关于引擎的 RailsGuide,考虑过他们的设计模式或自己构建了一个引擎,您就会知道在初始化文件中公开一个 setter 以供他人使用是微不足道的。 所以你可能想知道,为什么要对设置和偏好系统大惊小怪? 请记住,偏好系统解决了 Spree 的域问题。 挂钩到初始化过程并获得对 Rails 框架的访问可以帮助您以可维护的方式满足您的需求。
此引擎设计模式侧重于使用 Rails 框架作为其许多移动部分之间的常量,以存储在运行时(通常)不会更改但在应用程序安装之间更改的设置。
如果您曾经尝试过为 Rails 应用程序添加白标签,那么您可能熟悉这种偏好场景,并且在每个新应用程序的漫长设置过程中感受到了复杂的数据库“设置”表的痛苦。 现在您知道有一条不同的路径可用,这太棒了 - 高五!
炼油厂CMS
Rails 的开源内容管理系统
约定优于配置有人吗? Rails 引擎有时看起来更像是一种配置练习,但 RefineryCMS 记得 Rails 的一些魔力。 这是它的lib
目录的全部内容:
# lib/refinerycms.rb require 'refinery/all' # lib/refinery/all.rb %w(core authentication dashboard images resources pages).each do |extension| require "refinerycms-#{extension}" end
哇。 如果你不能通过这一点来判断,炼油厂团队真的知道他们在做什么。 它们采用extension
的概念,本质上是另一个引擎。 与 Spree 一样,它有一个包含缝合的宝石,但只使用了两针,并汇集了一系列引擎来提供其全套功能。
引擎的用户还可以创建扩展,为博客、新闻、投资组合、推荐、查询等创建自己的 CMS 功能(这是一个很长的列表),所有这些都与核心 RefineryCMS 挂钩。
这种设计可能会因其模块化方法而引起您的注意,而 Refinery 就是这种 Rails 设计模式的一个很好的例子。 “它是如何工作的?” 我听到你问。
core
引擎映射了一些钩子供其他引擎使用:
# core/lib/refinery/engine.rb module Refinery module Engine def after_inclusion(&block) if block && block.respond_to?(:call) after_inclusion_procs << block else raise 'Anything added to be called after_inclusion must be callable (respond to #call).' end end def before_inclusion(&block) if block && block.respond_to?(:call) before_inclusion_procs << block else raise 'Anything added to be called before_inclusion must be callable (respond to #call).' end end private def after_inclusion_procs @@after_inclusion_procs ||= [] end def before_inclusion_procs @@before_inclusion_procs ||= [] end end end
如您所见, before_inclusion
和after_inclusion
只是存储稍后将运行的 proc 列表。 Refinery 包含过程使用 Refinery 的控制器和助手扩展了当前加载的 Rails 应用程序。 这是一个在行动:
# authentication/lib/refinery/authentication/engine.rb before_inclusion do [Refinery::AdminController, ::ApplicationController].each do |c| Refinery.include_once(c, Refinery::AuthenticatedSystem) end end
我确定您之前已将身份验证方法放入您的ApplicationController
和AdminController
中,这是一种编程方式。
查看 Authentication Engine 文件的其余部分将帮助我们收集其他一些关键组件:
module Refinery module Authentication class Engine < ::Rails::Engine extend Refinery::Engine isolate_namespace Refinery engine_name :refinery_authentication config.autoload_paths += %W( #{config.root}/lib ) initializer "register refinery_user plugin" do Refinery::Plugin.register do |plugin| plugin.pathname = root plugin.name = 'refinery_users' plugin.menu_match = %r{refinery/users$} plugin.url = proc { Refinery::Core::Engine.routes.url_helpers.admin_users_path } end end end config.after_initialize do Refinery.register_extension(Refinery::Authentication) end # ... end end
在引擎盖下,炼油厂扩展使用Plugin
系统。 从 Spree 代码分析中, initializer
步骤看起来很熟悉,这里只是满足register
方法的要求,要添加到core
扩展跟踪的Refinery::Plugins
列表中,而Refinery.register_extension
只是添加模块名称到存储在类访问器中的列表。
令人震惊的是: Refinery::Authentication
类实际上是对 Devise 的封装,并带有一些自定义。 所以一直都是乌龟!
扩展和插件是 Refinery 开发的概念,以支持其丰富的迷你轨道应用程序和工具生态系统 - 想想rake generate refinery:engine
。 这里的设计模式与 Spree 不同,它在 Rails 引擎周围强加了一个额外的 API 来帮助管理它们的组合。
“The Rails Way”成语是 Refinery 的核心,在他们的迷你 Rails 应用程序中越来越多,但从外部你不会知道这一点。 在应用程序组合级别设计边界与为 Rails 应用程序中使用的类和模块创建一个干净的 API 一样重要,甚至可能更重要。
包装您无法直接控制的代码是一种常见模式,它是一种预见性,可以减少代码更改时的维护时间,限制您需要进行修改以支持升级的位置数量。 将这种技术与分区功能一起应用创建了一个灵活的组合平台,这里有一个真实世界的例子就在你的眼皮底下——一定要喜欢开源!
结论
通过分析现实世界应用程序中使用的流行 gem,我们已经看到了四种设计 Rails 引擎模式的方法。 值得阅读他们的存储库,以从已经应用和迭代的丰富经验中学习。 站在巨人的肩膀上。
在本 Rails 指南中,我们重点介绍了用于集成 Rails 引擎及其最终用户的 Rails 应用程序的设计模式和技术,以便您可以将这些知识添加到 Rails 工具带中。
我希望您从查看此代码中学到的东西和我一样多,并受到启发,在 Rails Engines 符合要求时给他们一个机会。 非常感谢我们审查的 gem 的维护者和贡献者。 伟大的工作的人!