野外 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 的維護者和貢獻者。 偉大的工作的人!