Przewodnik po silnikach Rails na wolności: Przykłady silników Rails w praktyce w prawdziwym świecie
Opublikowany: 2022-03-11Dlaczego silniki Rails nie są używane częściej? Nie znam odpowiedzi, ale myślę, że uogólnienie „Wszystko jest silnikiem” ukryło domeny problemów, które mogą pomóc rozwiązać.
Znakomita dokumentacja Rails Guide do rozpoczęcia pracy z Rails Engines odwołuje się do czterech popularnych przykładów implementacji Rails Engine: Forem, Devise, Spree i RefineryCMS. Są to fantastyczne rzeczywiste przypadki użycia Silników, z których każdy używa innego podejścia do integracji z aplikacją Railsową.
Zbadanie części, w jaki sposób te klejnoty są skonfigurowane i złożone, da zaawansowanym programistom Ruby on Rails cenną wiedzę na temat wzorców lub technik wypróbowanych i przetestowanych na wolności, więc gdy nadejdzie czas, możesz mieć kilka dodatkowych opcji do oceny.
Spodziewam się, że pobieżnie zapoznasz się z działaniem silnika, więc jeśli czujesz, że coś się nie zgadza, zapoznaj się z najdoskonalszym Przewodnikiem po Railsach Pierwsze kroki z silnikami .
Bez zbędnych ceregieli wejdźmy w dziki świat przykładów silników Rails!
Forem
Silnik dla Railsów, który ma być najlepszym małym systemem forum w historii
Ten klejnot jest zgodny z kierunkiem Przewodnika po szynach w silnikach co do joty. Jest to spory przykład, a przeglądanie jego repozytorium da ci wyobrażenie, jak daleko możesz rozciągnąć podstawową konfigurację.
Jest to klejnot z jednym silnikiem, który wykorzystuje kilka technik do integracji z główną aplikacją.
module ::Forem class Engine < Rails::Engine isolate_namespace Forem # ... config.to_prepare do Decorators.register! Engine.root, Rails.root end # ... end end
Interesującą częścią jest tutaj Decorators.register!
metoda klasy, wyeksponowana przez klejnot Dekoratorów. Hermetyzuje ładowanie plików, które nie zostałyby uwzględnione w procesie automatycznego ładowania Railsów. Być może pamiętasz, że używanie wyraźnych instrukcji require
rujnuje automatyczne ponowne ładowanie w trybie deweloperskim, więc jest to ratunek! Wyraźniej będzie posłużyć się przykładem z Przewodnika, aby zilustrować to, co się dzieje:
config.to_prepare do Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c| require_dependency(c) end end
Większość magii w konfiguracji Forem dzieje się w górnej definicji modułu głównego Forem
. Ten plik opiera się na zmiennej user_class
ustawionej w pliku inicjatora:
Forem.user_class = "User"
Osiągasz to za pomocą mattr_accessor
, ale to wszystko jest w Przewodniku po Railsach, więc nie będę tego tutaj powtarzał. Mając to na miejscu, Forem następnie dekoruje klasę użytkownika wszystkim, czego potrzebuje do uruchomienia aplikacji:
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? # ...
A okazuje się, że to całkiem sporo! Wyciąłem większość, ale pozostawiłem definicję asocjacji, a także metodę instancji, aby pokazać typ linii, które można tam znaleźć.
Rzut oka na cały plik może pokazać, jak łatwe w zarządzaniu może być przeniesienie części aplikacji w celu ponownego użycia do silnika.
Dekorowanie to nazwa gry w domyślnym użyciu silnika. Jako użytkownik końcowy klejnotu możesz nadpisać model, widok i kontrolery, tworząc własne wersje klas przy użyciu ścieżki pliku i konwencji nazewnictwa plików określonych w README dekoratora klejnotu. Takie podejście wiąże się jednak z pewnymi kosztami, zwłaszcza gdy silnik zostanie ulepszony do dużej wersji – utrzymanie utrzymania sprawnych dekoracji może szybko wymknąć się spod kontroli. Nie cytuję tutaj Forem, wierzę, że są niezłomni w utrzymywaniu zwartej podstawowej funkcjonalności, ale miej to na uwadze, jeśli stworzysz silnik i zdecydujesz się na przegląd.
Podsumujmy to: jest to domyślny wzorzec projektowy silnika Rails, polegający na dekorowaniu widoków, kontrolerów i modeli przez użytkowników końcowych, wraz z konfiguracją podstawowych ustawień za pomocą plików inicjalizacyjnych. Działa to dobrze w przypadku bardzo skoncentrowanej i powiązanej funkcjonalności.
Wymyślać
Elastyczne rozwiązanie do uwierzytelniania dla Rails
Przekonasz się, że silnik jest bardzo podobny do aplikacji Rails, z katalogami views
, controllers
i models
. Devise to dobry przykład hermetyzacji aplikacji i udostępnienia wygodnego punktu integracji. Przeanalizujmy, jak dokładnie to działa.
Rozpoznasz te wiersze kodu, jeśli jesteś programistą Rails dłużej niż kilka tygodni:
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
Każdy parametr przekazany do metody devise
reprezentuje moduł w Devise Engine. W sumie jest dziesięć takich modułów, które dziedziczą po znanym ActiveSupport::Concern
. Rozszerzają one twoją klasę User
, wywołując metodę devise
w jej zakresie.
Posiadanie tego typu punktu integracji jest bardzo elastyczne, możesz dodać lub usunąć dowolny z tych parametrów, aby zmienić poziom funkcjonalności wymagany przez silnik. Oznacza to również, że nie musisz kodować na sztywno modelu, którego chcesz użyć w pliku inicjującym, jak sugeruje Rails Guide on Engines. Innymi słowy, nie jest to konieczne:
Devise.user_model = 'User'
Ta abstrakcja oznacza również, że możesz zastosować to do więcej niż jednego modelu użytkownika w tej samej aplikacji (na przykład admin
i user
), podczas gdy podejście z plikiem konfiguracyjnym pozostawiłoby Cię przywiązanym do jednego modelu z uwierzytelnianiem. Nie jest to największa zaleta, ale ilustruje inny sposób rozwiązania problemu.
Devise rozszerza ActiveRecord::Base
o własny moduł, który zawiera definicję metody devise
:
# lib/devise/orm/active_record.rb ActiveRecord::Base.extend Devise::Models
Każda klasa dziedzicząca po ActiveRecord::Base
będzie miała teraz dostęp do metod klas zdefiniowanych w 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
(Usunąłem dużo kodu ( # ...
), aby wyróżnić ważne części. )
Parafrazując kod, dla każdej nazwy modułu przekazanej do metody devise
otrzymujemy:
- ładowanie określonego przez nas modułu, który znajduje się pod
Devise::Models
(Devise::Models.const_get(m.to_s.classify
) - rozszerzenie klasy
User
o modułClassMethods
, jeśli taki posiada - dołącz określony moduł (
include mod
), aby dodać jego metody instancji do klasy wywołującej metodędevise
(User
)
Jeśli chciałbyś stworzyć moduł, który mógłby być załadowany w ten sposób, musiałbyś upewnić się, że jest zgodny ze zwykłym interfejsem ActiveSupport::Concern
, ale przestrzeń nazw znajduje się pod Devise:Models
, ponieważ tutaj szukamy stałej:
module Devise module Models module Authenticatable extend ActiveSupport::Concern included do # ... end module ClassMethods # ... end end end end
Uff.
Jeśli korzystałeś już wcześniej z Rails's Concers i doświadczyłeś możliwości ponownego wykorzystania, na jakie pozwalają, możesz docenić zalety tego podejścia. Krótko mówiąc, rozbicie funkcjonalności w ten sposób ułatwia testowanie, ponieważ jest wyabstrahowane z modelu ActiveRecord
i ma mniejszy narzut niż domyślny wzorzec używany przez Forem, jeśli chodzi o rozszerzanie funkcjonalności.
Ten wzorzec składa się z podzielenia twojej funkcjonalności na kwestie związane z Rails i ujawnienia punktu konfiguracji, aby uwzględnić lub wykluczyć je w danym zakresie. Utworzony w ten sposób silnik jest wygodny dla użytkownika końcowego – jest to czynnik przyczyniający się do sukcesu i popularności Devise. A teraz ty też wiesz, jak to zrobić!
Hulanka
Kompletne rozwiązanie e-commerce typu open source dla Ruby on Rails
Spree dokonał kolosalnego wysiłku, aby opanować swoją monolityczną aplikację, przechodząc do korzystania z silników. Projekt architektury, z którym się teraz toczą, to klejnot „Spree”, który zawiera wiele klejnotów silnika.
Silniki te tworzą partycje w zachowaniu, do którego możesz przywyknąć w monolitycznej aplikacji lub rozłożonej na aplikacje:
- spree_api (interfejs API REST)
- spree_frontend (komponenty skierowane do użytkownika)
- spree_backend (obszar administracyjny)
- spree_cmd (Narzędzia wiersza poleceń)
- spree_core (Modele i Mailery, podstawowe komponenty Spree, bez których nie może działać)
- spree_sample (Przykładowe dane)
Obejmujący klejnot łączy je razem, pozostawiając programiście wybór wymaganego poziomu funkcjonalności. Na przykład, możesz uruchomić tylko silnik spree_core
i owinąć wokół niego swój własny interfejs.
Główny klejnot Spree wymaga tych silników:
# lib/spree.rb require 'spree_core' require 'spree_api' require 'spree_backend' require 'spree_frontend'
Każdy silnik musi następnie dostosować swoją engine_name
i ścieżkę root
(ta ostatnia zwykle wskazuje na klejnot najwyższego poziomu) i skonfigurować się, podłączając się do procesu inicjalizacji:
# 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
Możesz rozpoznać tę metodę inicjalizacji lub nie: jest ona częścią Railtie
i hakiem, który daje możliwość dodawania lub usuwania kroków z inicjalizacji frameworka Rails. Spree w dużym stopniu opiera się na tym haczyku, aby skonfigurować swoje złożone środowisko dla wszystkich swoich silników.

Korzystając z powyższego przykładu w czasie wykonywania, będziesz mieć dostęp do swoich ustawień poprzez dostęp do stałej Rails
najwyższego poziomu:
Rails.application.config.spree
Dzięki powyższemu przewodnikowi po wzorcach projektowych silników Rails możemy to nazwać dniem, ale Spree ma mnóstwo niesamowitego kodu, więc zagłębmy się w to, jak wykorzystują inicjalizację do współdzielenia konfiguracji między silnikami i główną aplikacją Rails.
Spree ma złożony system preferencji, który ładuje, dodając krok do procesu inicjalizacji:
# api/lib/spree/api/engine.rb initializer "spree.environment", :before => :load_config_initializers do |app| app.config.spree = Spree::Core::Environment.new end
Tutaj dołączamy do app.config.spree
nową instancję Spree::Core::Environment
. W aplikacji rails będziesz mógł uzyskać do niej dostęp poprzez Rails.application.config.spree
z dowolnego miejsca - modele, kontrolery, widoki.
Idąc dalej, utworzona przez nas klasa Spree::Core::Environment
wygląda tak:
module Spree module Core class Environment attr_accessor :preferences def initialize @preferences = Spree::AppConfiguration.new end end end end
Udostępnia zmienną :preferences
ustawioną na nową instancję klasy Spree::AppConfiguration
, która z kolei wykorzystuje metodę preference
zdefiniowaną w klasie Preferences::Configuration
, aby ustawić opcje z wartościami domyślnymi dla ogólnej konfiguracji aplikacji:
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
Nie pokażę pliku Preferences::Configuration
, ponieważ zajmie to trochę wyjaśnienia, ale zasadniczo jest to cukier składniowy do pobierania i ustawiania preferencji. (W rzeczywistości jest to nadmierne uproszczenie jego funkcjonalności, ponieważ system preferencji zapisze w bazie danych wartości inne niż domyślne dla istniejących lub nowych preferencji, dla dowolnej klasy ActiveRecord
z kolumną :preference
- ale nie ma takiej potrzeby wiem o tym.)
Oto jedna z tych opcji w akcji:
module Spree class Calculator < Spree::Base def self.calculators Rails.application.config.spree.calculators end # ... end end
Kalkulatory kontrolują różne rzeczy w Spree – koszty wysyłki, promocje, korekty cen produktów – więc posiadanie mechanizmu ich zamiany w ten sposób zwiększa rozszerzalność Silnika.
Jednym z wielu sposobów nadpisania domyślnych ustawień tych preferencji jest użycie inicjatora w głównej aplikacji Rails:
# config/initializergs/spree.rb Spree::Config do |config| config.admin_interface_logo = 'company_logo.png' end
Jeśli zapoznałeś się z RailsGuide on Engines, rozważyłeś ich wzorce projektowe lub sam zbudowałeś silnik, wiesz, że udostępnienie settera w pliku inicjującym jest trywialne, aby ktoś mógł go użyć. Więc możesz się zastanawiać, po co tyle zamieszania z konfiguracją i systemem preferencji? Pamiętaj, że system preferencji rozwiązuje problem domeny Spree. Podłączenie się do procesu inicjalizacji i uzyskanie dostępu do frameworka Rails może pomóc w spełnieniu wymagań w sposób łatwy do utrzymania.
Ten wzorzec projektowy silnika koncentruje się na wykorzystaniu frameworka Rails jako stałej między wieloma ruchomymi częściami do przechowywania ustawień, które nie zmieniają się (ogólnie) w czasie wykonywania, ale zmieniają się między instalacjami aplikacji.
Jeśli kiedykolwiek próbowałeś nadać aplikacji Railsowej białą etykietę, być może znasz ten scenariusz preferencji i odczułeś ból zawiłych tabel „ustawień” bazy danych podczas długiego procesu konfiguracji dla każdej nowej aplikacji. Teraz wiesz, że dostępna jest inna ścieżka i to jest niesamowite – przybij piątkę!
RafineriaCMS
Otwarty system zarządzania treścią dla Rails
Konwencja nad konfiguracją ktoś? Rails Engines może czasami wyglądać bardziej jak ćwiczenie z konfiguracji, ale RefineryCMS pamięta część magii Railsów. To jest cała zawartość katalogu 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
Wow. Jeśli nie możesz tego stwierdzić, zespół rafinerii naprawdę wie, co robi. Toczą się z koncepcją extension
, które jest w istocie innym Silnikiem. Podobnie jak Spree, ma obszerny klejnot do szycia, ale używa tylko dwóch szwów i łączy kolekcję silników, aby zapewnić pełny zestaw funkcjonalności.
Rozszerzenia są również tworzone przez użytkowników silnika, aby tworzyć własne mash-upy funkcji CMS dla blogów, wiadomości, portfolio, referencji, zapytań itp. (to długa lista), wszystkie podpięte do rdzenia RefineryCMS.
Ten projekt może zwrócić twoją uwagę dzięki swojemu modułowemu podejściu, a Rafineria jest doskonałym przykładem tego wzorca projektowego Rails. "Jak to działa?" słyszę, jak pytasz.
core
silnik mapuje kilka punktów zaczepienia, z których mogą korzystać inne silniki:
# 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
Jak widać, before_inclusion
i after_inclusion
po prostu przechowują listę procesów, które będą uruchamiane później. Proces włączania Refinery rozszerza aktualnie załadowane aplikacje Railsowe o kontrolery i helpery Refinery. Oto jeden w akcji:
# authentication/lib/refinery/authentication/engine.rb before_inclusion do [Refinery::AdminController, ::ApplicationController].each do |c| Refinery.include_once(c, Refinery::AuthenticatedSystem) end end
Jestem pewien, że wcześniej umieściłeś metody uwierzytelniania w kontrolerze ApplicationController
i AdminController
, jest to programowy sposób na zrobienie tego.
Spojrzenie na resztę tego pliku mechanizmu uwierzytelniania pomoże nam zebrać kilka innych kluczowych elementów:
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
Pod maską rozszerzenia Refinery wykorzystują system Plugin
. Krok initializer
będzie wyglądał znajomo z analizy kodu Spree, tutaj po prostu spełniamy wymagania dotyczące metod register
, które mają zostać dodane do listy Refinery::Plugins
, które śledzi rozszerzenie core
, a Refinery.register_extension
po prostu dodaje nazwę modułu do listy przechowywanej w akcesorze klas.
Oto szok: klasa Refinery::Authentication
to tak naprawdę opakowanie wokół Devise, z pewnymi dostosowaniami. Więc to żółwie aż do dołu!
Rozszerzenia i wtyczki to koncepcje, które Refinery opracowała, aby wspierać bogaty ekosystem aplikacji i narzędzi do minitorów — pomyśl, że rake generate refinery:engine
. Wzorzec projektowy różni się tutaj od Spree, nakładając dodatkowe API wokół Rails Engine, aby pomóc w zarządzaniu ich kompozycją.
Idiom „The Rails Way” jest rdzeniem Refinery, coraz bardziej obecnym w ich aplikacjach mini-rails, ale z zewnątrz tego nie wiesz. Projektowanie granic na poziomie kompozycji aplikacji jest równie ważne, być może nawet bardziej, niż tworzenie czystego API dla twoich klas i modułów używanych w twoich aplikacjach Rails.
Zawijanie kodu, nad którym nie masz bezpośredniej kontroli, jest powszechnym wzorcem, jest to przewidywanie w skróceniu czasu konserwacji, gdy ten kod się zmieni, ograniczając liczbę miejsc, w których będziesz musiał wprowadzić poprawki w celu obsługi uaktualnień. Zastosowanie tej techniki wraz z funkcją partycjonowania tworzy elastyczną platformę do kompozycji, a oto przykład z prawdziwego świata, który znajduje się tuż pod twoim nosem – musisz pokochać open source!
Wniosek
Widzieliśmy cztery podejścia do projektowania wzorców silników Rails poprzez analizę popularnych klejnotów używanych w rzeczywistych aplikacjach. Warto zapoznać się z ich repozytoriami, aby uczyć się z bogatego doświadczenia już zastosowanego i powtórzonego. Stań na ramionach olbrzymów.
W tym przewodniku po Railsach skupiliśmy się na wzorcach projektowych i technikach integracji Rails Engines i aplikacji Railsowych użytkowników końcowych, tak abyś mógł dodać tę wiedzę do swojego paska narzędziowego Rails.
Mam nadzieję, że nauczyłeś się tyle samo, co ja, przeglądając ten kod i poczujesz inspirację, aby dać Rails Engines szansę, gdy będą pasować. Ogromne podziękowania dla opiekunów i współpracowników recenzowanych przez nas klejnotów. Świetni ludzie pracy!