Руководство по движкам Rails в дикой природе: реальные примеры движков Rails в действии
Опубликовано: 2022-03-11Почему Rails Engines не используются чаще? Я не знаю ответа, но я думаю, что обобщение «все есть двигатель» скрывает проблемные области, которые они могут помочь решить.
Превосходная документация Rails Guide для начала работы с Rails Engines содержит ссылки на четыре популярных примера реализации Rails Engine: Forem, Devise, Spree и RefineryCMS. Это фантастические примеры использования движков в реальном мире, каждый из которых использует свой подход к интеграции с приложением Rails.
Изучение того, как эти драгоценные камни настроены и составлены, даст продвинутым разработчикам Ruby on Rails ценные знания о том, какие шаблоны или методы опробованы и протестированы в реальных условиях, поэтому, когда придет время, у вас будет несколько дополнительных вариантов для оценки.
Я ожидаю, что вы будете иметь поверхностное представление о том, как работает Engine, поэтому, если вы чувствуете, что что-то не совсем складывается, пожалуйста, прочтите превосходнейшее руководство по Rails Getting Started With Engines .
Без дальнейших церемоний, давайте отправимся в дикий мир примеров движка Rails!
Форма
Движок для 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. Он инкапсулирует загрузку файлов, которые не будут включены в процесс автозагрузки 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? # ...
Что оказывается довольно много! Я вырезал большую часть, но оставил определение ассоциации, а также метод экземпляра, чтобы показать вам тип строк, которые вы можете там найти.
Беглый просмотр всего файла может показать вам, насколько управляемым может быть перенос части вашего приложения для повторного использования в Engine.
Украшение - это название игры в использовании движка по умолчанию. Как конечный пользователь драгоценного камня вы можете переопределить модель, представление и контроллеры, создав свои собственные версии классов, используя пути к файлам и соглашения об именах файлов, изложенные в 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
, представляет собой модуль в Devise Engine. Всего есть десять таких модулей, которые унаследованы от знакомого ActiveSupport::Concern
. Они расширяют ваш класс User
, вызывая метод devise
в его области.
Наличие этого типа точки интеграции очень гибко, вы можете добавить или удалить любой из этих параметров, чтобы изменить уровень функциональности, который вам требуется для выполнения Engine. Это также означает, что вам не нужно жестко указывать, какую модель вы хотели бы использовать в файле инициализатора, как это предлагается в 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
) - расширение класса
User
с помощью модуляClassMethods
, если он есть - включить указанный модуль (
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 Concerns и испытали на себе возможности повторного использования, которые они предоставляют, то вы можете оценить тонкости этого подхода. Короче говоря, разделение функциональности таким образом упрощает тестирование, абстрагируясь от модели ActiveRecord
, и имеет меньшие накладные расходы, чем шаблон по умолчанию, используемый Forem, когда дело доходит до расширения функциональности.
Этот шаблон состоит из разбиения вашей функциональности на Rails Concerns и раскрытия точки конфигурации для включения или исключения их в заданной области. Сформированный таким образом движок удобен для конечного пользователя, что способствует успеху и популярности Devise. И теперь вы тоже знаете, как это сделать!
Веселье
Полное решение для электронной коммерции с открытым исходным кодом для Ruby on Rails.
Компания Spree приложила колоссальные усилия, чтобы взять под контроль свое монолитное приложение, перейдя на использование движков. Архитектурный дизайн, с которым они сейчас работают, представляет собой драгоценный камень «Веселье», который содержит множество драгоценных камней Engine.
Эти механизмы создают разделы в поведении, которое вы, возможно, привыкли видеть в монолитном приложении или разбросаны по приложениям:
- spree_api (RESTful API)
- spree_frontend (компоненты, ориентированные на пользователя)
- spree_backend (область администратора)
- spree_cmd (инструменты командной строки)
- spree_core (Модели и почтовые программы, основные компоненты Spree, без которых он не может работать)
- spree_sample (пример данных)
Охватывающий драгоценный камень сшивает их вместе, оставляя разработчику возможность выбора требуемого уровня функциональности. Например, вы можете работать только с spree_core
и обернуть вокруг него свой собственный интерфейс.
Для основной жемчужины Spree требуются следующие двигатели:
# lib/spree.rb require 'spree_core' require 'spree_api' require 'spree_backend' require 'spree_frontend'
Затем каждый движок должен настроить свое engine_name
и root
путь (последний обычно указывает на гем верхнего уровня) и настроить себя, подключившись к процессу инициализации:
# 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
Здесь мы присоединяем к app.config.spree
новый экземпляр Spree::Core::Environment
. В приложении 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
, который, в свою очередь, использует метод preference
, определенный в классе 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
, потому что это потребует некоторого объяснения, но по сути это синтаксический сахар для получения и установки предпочтений. (По правде говоря, это чрезмерное упрощение его функциональности, так как система предпочтений будет сохранять значения, отличные от значений по умолчанию, для существующих или новых предпочтений в базе данных, для любого класса ActiveRecord
со столбцом :preference
— но вам не нужно знай это.)
Вот один из таких вариантов в действии:
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, вы, возможно, знакомы с этим сценарием настроек и чувствовали боль от запутанных таблиц «настроек» базы данных в течение длительного процесса настройки для каждого нового приложения. Теперь вы знаете, что доступен другой путь, и это здорово — дай пять!
НПЗCMS
Система управления контентом с открытым исходным кодом для 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
Вот это да. Если вы не можете сказать по этому, команда Refinery действительно знает, что они делают. Они придерживаются концепции extension
, которое по сути является другим движком. Как и в Spree, он имеет обширную жемчужину сшивания, но использует только два стежка и объединяет коллекцию движков для обеспечения полного набора функций.
Расширения также создаются пользователями Engine для создания собственного сочетания функций 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
просто хранят список процессов, которые будут запущены позже. Процесс включения Refinery расширяет загруженные в данный момент приложения Rails контроллерами и помощниками Refinery. Вот один в действии:
# 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
Под капотом расширения Refinery используется система Plugin
. Шаг initializer
будет выглядеть знакомым из анализа кода Spree, здесь просто выполняются требования к методам register
, которые должны быть добавлены в список Refinery::Plugins
, которые отслеживает core
расширение, а Refinery.register_extension
просто добавляет имя модуля. в список, хранящийся в методе доступа к классу.
Вот шок: класс Refinery::Authentication
на самом деле представляет собой оболочку Devise с некоторыми настройками. Так что это черепахи на всем пути вниз!
Расширения и плагины — это концепты, которые Refinery разработала для поддержки своей богатой экосистемы приложений и инструментов mini- rake generate refinery:engine
. Шаблон проектирования здесь отличается от Spree тем, что накладывает дополнительный API на Rails Engine, чтобы помочь в управлении их композицией.
Идиома «The Rails Way» лежит в основе Refinery, она все больше присутствует в их мини-рельсовых приложениях, но со стороны вы этого не заметите. Разработка границ на уровне композиции приложения так же важна, а может быть, даже важнее, чем создание чистого API для ваших классов и модулей, используемых в ваших приложениях Rails.
Обертка кода, над которым у вас нет прямого контроля, является распространенным шаблоном, это дальновидность в сокращении времени обслуживания при изменении этого кода, ограничении количества мест, которые вам понадобятся для внесения изменений для поддержки обновлений. Применение этой техники вместе с функциональностью разбиения на разделы создает гибкую платформу для композиции, и вот пример из реального мира, который находится прямо у вас под носом — вы должны любить открытый исходный код!
Заключение
Мы рассмотрели четыре подхода к проектированию паттернов движка Rails, анализируя популярные гемы, используемые в реальных приложениях. Стоит прочитать их репозитории, чтобы извлечь уроки из богатого опыта, уже примененного и повторенного. Встаньте на плечи гигантов.
В этом руководстве по Rails мы сосредоточились на шаблонах проектирования и методах интеграции Rails Engines и приложений Rails их конечных пользователей, чтобы вы могли добавить эти знания в свой набор инструментов Rails.
Я надеюсь, что вы узнали столько же, сколько и я, изучив этот код, и почувствовали вдохновение дать шанс Rails Engines, когда они будут соответствовать всем требованиям. Огромное спасибо сопровождающим и участникам драгоценных камней, которые мы рассмотрели. Отличная работа!