本文概述
为什么不经常使用Rails Engine?我不知道答案, 但我确实认为”一切都是引擎”的概括掩盖了他们可以帮助解决的问题领域。
出色的Rails指南入门文档参考了Rails Engine实现的四个流行示例:Forem, Devise, Spree和RefineryCMS。这些是引擎的绝佳现实世界用例, 每个用例都使用不同的方法与Rails应用程序集成。
检查这些gem的配置和组成部分将为高级Ruby on Rails开发人员提供在野外尝试和测试哪些模式或技术的宝贵知识, 因此, 当有机会时, 你可以有一些额外的选择来进行评估。
我确实希望你对Engine的工作方式有一个粗略的熟悉, 所以如果你觉得事情还不太完整, 请仔细阅读最出色的《 Rails Guides Engines入门》。
事不宜迟, 让我们冒险进入Rails引擎示例的狂野世界!
Forem
Rails的引擎, 旨在成为有史以来最好的小型论坛系统
该宝石遵循《 Rails引擎指南》的指示。这是一个相当大的示例, 仔细阅读其存储库将使你大致了解可以扩展基本设置的程度。
它是一个单引擎的宝石, 它使用多种技术与主应用程序集成。
module ::Forem
class Engine < Rails::Engine
isolate_namespace Forem
# ...
config.to_prepare do
Decorators.register! Engine.root, Rails.root
end
# ...
end
end
这里有趣的部分是Decorators.register!类方法, 由Decorators 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?
# ...
事实证明很多!我删去了大部分内容, 但留下了关联定义以及实例方法, 以向你显示可以在其中找到的行的类型。
掠过整个文件可能会向你展示如何将应用程序的一部分移植到引擎中以易于管理。
装饰是默认引擎使用情况下游戏的名称。作为gem的最终用户, 你可以使用装饰器gem README中列出的文件路径和文件命名约定来创建自己的类版本, 从而覆盖模型, 视图和控制器。但是, 与这种方法相关的成本很高, 尤其是当引擎进行主要版本升级时-保持装饰正常工作的维护会很快失控。我在这里没有引用Forem, 我相信他们会坚定地保持紧密的核心功能, 但是如果你创建Engine并决定进行大修, 请记住这一点。
让我们回顾一下:这是默认的Rails引擎设计模式, 它依赖于最终用户修饰视图, 控制器和模型, 并通过初始化文件配置基本设置。这对于非常集中的功能和相关功能非常有效。
Devise
Rails的灵活身份验证解决方案
你会发现Engine与Rails应用程序非常相似, 具有视图, 控制器和模型目录。 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方法的每个参数代表Devise Engine中的一个模块。这些模块共有十个, 它们从熟悉的ActiveSupport :: Concern继承。这些通过在其范围内调用devise方法来扩展User类。
具有这种类型的集成点非常灵活, 你可以添加或删除这些参数中的任何一个, 以更改引擎需要执行的功能级别。这也意味着你无需按照《 Rails引擎指南》的建议, 对要在初始化程序文件中使用的模型进行硬编码。换句话说, 这不是必需的:
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类
- 包含指定的模块(包括mod), 以将其实例方法添加到调用devise方法(用户)的类中
如果你想创建一个可以通过这种方式加载的模块, 则需要确保它遵循通常的ActiveSupport :: Concern接口, 但是要在Devise:Models下对其进行命名空间, 因为这是我们检索常量的地方:
module Devise
module Models
module Authenticatable
extend ActiveSupport::Concern
included do
# ...
end
module ClassMethods
# ...
end
end
end
end
ew
如果你以前使用过Rails的关注点并经历了它们提供的可重用性, 那么你可以欣赏到这种方法的优点。简而言之, 通过从ActiveRecord模型中抽象出来, 以这种方式分解功能使测试变得更加容易, 并且在扩展功能时, 其开销比Forem使用的默认模式低。
该模式包括将功能分解为Rails Concerns, 并公开配置点以在给定范围内包括或排除这些功能。以这种方式形成的引擎对于最终用户来说很方便-这是Devise成功和受欢迎的一个因素。现在你也知道该怎么做!
Spree
Ruby on Rails的完整开源电子商务解决方案
Spree经过艰巨的努力, 在使用引擎的过程中控制了其整体应用程序。他们现在使用的架构设计是” Spree”宝石, 其中包含许多Engine宝石。
这些引擎会创建行为分区, 你可能习惯在整体应用程序中看到这些分区, 或者将其散布到各个应用程序中:
- spree_api(RESTful API)
- spree_frontend(面向用户的组件)
- spree_backend(管理区域)
- spree_cmd(命令行工具)
- spree_core(模型和邮件程序, Spree不可缺少的基本组件)
- spree_sample(样本数据)
环绕的宝石将它们缝合在一起, 使开发人员可以选择需要的功能级别。例如, 你可以仅使用spree_core引擎运行, 并在其周围包装自己的界面。
主要的Spree gem需要以下引擎:
# lib/spree.rb
require 'spree_core'
require 'spree_api'
require 'spree_backend'
require 'spree_frontend'
然后, 每个引擎都需要自定义其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
它向Spree :: AppConfiguration类的新实例公开一个:preferences变量, 该实例又使用Preferences :: Configuration类中定义的首选项方法为常规应用程序配置设置默认选项:
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, 考虑了它们的设计模式或自己构建了引擎, 则将知道在初始化程序文件中公开设置程序以供他人使用是很简单的。因此, 你可能想知道, 为什么对设置和首选项系统大惊小怪?请记住, 首选项系统为Spree解决了一个域问题。参与初始化过程并获得对Rails框架的访问权限可以帮助你以可维护的方式满足你的需求。
该引擎设计模式着重于将Rails框架用作其许多移动部件之间的常数, 以存储在运行时不会(通常)更改但在应用程序安装之间会更改的设置。
如果你曾经尝试过对Rails应用程序加白标签, 那么你可能对这种首选项场景很熟悉, 并且在每个新应用程序的漫长安装过程中, 都感到费解的数据库”设置”表之苦。现在, 你知道一条不同的道路可供选择, 那太棒了-高五!
RefineryCMS
Rails的开源内容管理系统
约定超过配置的人吗?有时, Rails Engines肯定看起来更像是在配置中的练习, 但是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
哇。如果你无法确定, 那么炼油厂团队真的知道他们在做什么。他们采用扩展的概念, 本质上是另一种引擎。像Spree一样, 它具有环绕的缝合宝石, 但仅使用两个针脚, 并且汇集了一系列引擎以提供其全部功能。
扩展程序也由Engine的用户创建, 以创建自己的CMS功能的混搭, 用于博客, 新闻, 投资组合, 推荐, 查询等(很长的列表), 所有这些功能都与核心RefineryCMS挂钩。
这种设计可能会因其模块化方法而引起你的注意, 而精炼厂就是这种Rails设计模式的一个很好的例子。 “它是如何工作的?”我听到你问。
核心引擎为其他引擎映射了一些挂钩以供使用:
# 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列表。精炼厂包含过程使用精炼厂的控制器和辅助程序扩展了当前加载的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中, 这是一种编程方式。
查看该身份验证引擎文件的其余部分将有助于我们收集其他一些关键组件:
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
在后台, 炼油厂扩展使用插件系统。从Spree代码分析中看, 初始化步骤看起来很熟悉, 这里只是满足要添加到核心扩展跟踪的Refinery :: Plugins列表中的注册方法要求, 而Refinery.register_extension只是添加了模块名称到存储在类访问器中的列表。
令人震惊的是:Refinery :: Authentication类实际上是Devise的包装, 并进行了一些自定义。所以它一直都是乌龟!
扩展程序和插件是Refinery为支持其丰富的微型轨道应用程序和工具生态系统而开发的概念-认为rake可以生成fineryry:engine。这里的设计模式与Spree不同, 它在Rails Engine周围附加了一个API, 以帮助管理其组成。
” The Rails Way”这个习语是精炼厂的核心, 在其微型Rails应用程序中越来越多地出现, 但是从外部你可能不知道。与为Rails应用程序中使用的类和模块创建干净的API相比, 在应用程序组合级别设计边界同样重要, 甚至可能更重要。
包装你无法直接控制的代码是一种常见的模式, 这是减少代码更改时的维护时间的先见之明, 从而可以减少为支持升级而需要进行修改的位置数。将此技术与分区功能结合使用可创建一个灵活的合成平台, 这是一个真实的例子, 坐在你的鼻子下面-必须爱开源!
总结
通过分析现实世界中使用的流行宝石, 我们已经看到了四种设计Rails引擎模式的方法。值得阅读它们的存储库, 以从已经应用和迭代的大量经验中学习。站在巨人的肩膀上。
在本Rails指南中, 我们重点介绍了集成Rails Engine及其最终用户的Rails应用程序的设计模式和技术, 以便你可以将这些知识添加到Rails工具带中。
我希望你能从阅读此代码中学到很多东西, 并从中得到启发, 让Rails Engine在符合条件时有机会。非常感谢我们所审查的宝石的维护者和贡献者。很棒的人!
相关:时间戳截断:Ruby on Rails ActiveRecord故事
评论前必须登录!
注册