Panduan untuk Rails Engines in the Wild: Contoh Dunia Nyata dari Rails Engines in Action

Diterbitkan: 2022-03-11

Mengapa Rails Engine tidak lebih sering digunakan? Saya tidak tahu jawabannya, tetapi saya pikir generalisasi dari "Semuanya adalah Mesin" telah menyembunyikan domain masalah yang dapat mereka bantu selesaikan.

Dokumentasi Rails Guide yang luar biasa untuk memulai dengan Rails Engines merujuk pada empat contoh populer implementasi Rails Engine: Forem, Devise, Spree, dan RefineryCMS. Ini adalah kasus penggunaan dunia nyata yang fantastis untuk Engine masing-masing menggunakan pendekatan berbeda untuk berintegrasi dengan aplikasi Rails.

Setiap panduan Rails harus mencakup topik pola desain mesin Rails dan contohnya.

Memeriksa bagian bagaimana permata ini dikonfigurasi dan disusun akan memberi pengembang Ruby on Rails tingkat lanjut pengetahuan yang berharga tentang pola atau teknik apa yang dicoba dan diuji di alam liar, jadi ketika saatnya tiba, Anda dapat memiliki beberapa opsi tambahan untuk dievaluasi.

Saya berharap Anda memiliki pemahaman sepintas tentang cara kerja Mesin, jadi jika Anda merasa ada sesuatu yang tidak sesuai, silakan baca dengan teliti Panduan Rails yang paling bagus untuk Memulai Mesin .

Tanpa basa-basi lagi, mari kita menjelajah di dunia liar contoh mesin Rails!

depan

Mesin untuk Rails yang bertujuan untuk menjadi sistem forum kecil terbaik yang pernah ada

Permata ini mengikuti arahan Rails Guide on Engines ke surat itu. Ini adalah contoh yang cukup besar dan membaca dengan teliti repositorinya akan memberi Anda gambaran tentang seberapa jauh Anda dapat memperluas pengaturan dasar.

Ini adalah permata Single-Engine yang menggunakan beberapa teknik untuk berintegrasi dengan aplikasi utama.

 module ::Forem class Engine < Rails::Engine isolate_namespace Forem # ... config.to_prepare do Decorators.register! Engine.root, Rails.root end # ... end end

Bagian yang menarik di sini adalah Decorators.register! metode kelas, diekspos oleh permata Dekorator. Ini merangkum pemuatan file yang tidak akan disertakan dalam proses pemuatan otomatis Rails. Anda mungkin ingat bahwa menggunakan pernyataan require eksplisit merusak pemuatan ulang otomatis dalam mode pengembangan, jadi ini adalah penyelamat! Akan lebih jelas untuk menggunakan contoh dari Panduan untuk menggambarkan apa yang terjadi:

 config.to_prepare do Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c| require_dependency(c) end end

Sebagian besar keajaiban untuk konfigurasi Forem terjadi di definisi modul utama atas Forem . File ini bergantung pada variabel user_class yang diatur dalam file penginisialisasi:

 Forem.user_class = "User"

Anda melakukannya menggunakan mattr_accessor tetapi semuanya ada di Panduan Rails jadi saya tidak akan mengulanginya di sini. Dengan ini, Forem kemudian menghiasi kelas pengguna dengan semua yang dibutuhkan untuk menjalankan aplikasinya:

 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? # ...

Yang ternyata cukup banyak! Saya telah memotong sebagian besar tetapi meninggalkan definisi asosiasi serta metode instan untuk menunjukkan kepada Anda jenis garis yang dapat Anda temukan di sana.

Melihat sekilas seluruh file mungkin menunjukkan kepada Anda seberapa mudah bagian porting aplikasi Anda untuk digunakan kembali ke Engine.

Dekorasi adalah nama permainan dalam penggunaan Engine default. Sebagai pengguna akhir permata, Anda dapat mengganti model, tampilan, dan pengontrol dengan membuat versi kelas Anda sendiri menggunakan jalur file dan konvensi penamaan file yang ada di permata dekorator README. Ada biaya yang terkait dengan pendekatan ini, terutama ketika Engine mendapatkan peningkatan versi utama – pemeliharaan agar dekorasi Anda tetap berfungsi dapat dengan cepat menjadi tidak terkendali. Saya tidak mengutip Forem di sini, saya percaya mereka teguh dalam menjaga fungsionalitas inti yang erat, tetapi ingatlah ini jika Anda membuat Mesin dan memutuskan untuk melakukan perbaikan.

Mari kita rekap yang ini: ini adalah pola desain mesin Rails default yang mengandalkan tampilan dekorasi pengguna akhir, pengontrol dan model, bersama dengan mengonfigurasi pengaturan dasar melalui file inisialisasi. Ini berfungsi dengan baik untuk fungsionalitas yang sangat terfokus dan terkait.

Merancang

Solusi autentikasi fleksibel untuk Rails

Anda akan menemukan Engine sangat mirip dengan aplikasi Rails, dengan direktori views , controllers dan models . Rancang adalah contoh yang baik dari enkapsulasi aplikasi dan mengekspos titik integrasi yang nyaman. Mari kita lihat cara kerjanya.

Anda akan mengenali baris kode ini jika Anda telah menjadi pengembang Rails selama lebih dari beberapa minggu:

 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

Setiap parameter yang diteruskan ke metode devise mewakili modul di dalam Mesin Rancangan. Ada sepuluh modul ini secara keseluruhan yang mewarisi dari ActiveSupport::Concern yang sudah dikenal. Ini memperluas kelas User Anda dengan menerapkan metode devise dalam cakupannya.

Memiliki jenis titik integrasi ini sangat fleksibel, Anda dapat menambahkan atau menghapus salah satu parameter ini untuk mengubah tingkat fungsionalitas yang Anda perlukan untuk dijalankan oleh Engine. Ini juga berarti bahwa Anda tidak perlu membuat hardcode model mana yang ingin Anda gunakan dalam file penginisialisasi, seperti yang disarankan oleh Rails Guide on Engines. Dengan kata lain, ini tidak perlu:

 Devise.user_model = 'User'

Abstraksi ini juga berarti Anda dapat menerapkan ini ke lebih dari satu model pengguna dalam aplikasi yang sama (misalnya admin dan user ), sedangkan pendekatan file konfigurasi akan membuat Anda terikat pada satu model dengan otentikasi. Ini bukan nilai jual terbesar, tetapi menggambarkan cara yang berbeda untuk memecahkan masalah.

Rancangan memperluas ActiveRecord::Base dengan modulnya sendiri yang mencakup devise metode rancangan:

 # lib/devise/orm/active_record.rb ActiveRecord::Base.extend Devise::Models

Setiap kelas yang diwarisi dari ActiveRecord::Base sekarang akan memiliki akses ke metode kelas yang ditentukan dalam 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

( Saya telah menghapus banyak kode ( # ... ) untuk menyorot bagian-bagian penting. )

Menguraikan kode, untuk setiap nama modul yang diteruskan ke metode devise kita adalah:

  • memuat modul yang kami tentukan yang hidup di bawah Devise::Models ( Devise::Models.const_get(m.to_s.classify )
  • memperluas kelas User dengan modul ClassMethods jika ada
  • sertakan modul yang ditentukan ( include mod ) untuk menambahkan metode turunannya ke kelas yang memanggil metode devise ( User )

Jika Anda ingin membuat modul yang dapat dimuat dengan cara ini, Anda harus memastikannya mengikuti antarmuka ActiveSupport::Concern yang biasa, tetapi beri nama di bawah Devise:Models karena di sinilah kami mencari untuk mengambil konstanta:

 module Devise module Models module Authenticatable extend ActiveSupport::Concern included do # ... end module ClassMethods # ... end end end end

Fiuh.

Jika Anda telah menggunakan Rails' Concerns sebelumnya dan mengalami kegunaan kembali yang mereka mampu, maka Anda dapat menghargai kebaikan dari pendekatan ini. Singkatnya, memecah fungsionalitas dengan cara ini membuat pengujian lebih mudah dengan diabstraksikan dari model ActiveRecord , dan memiliki overhead yang lebih rendah daripada pola default yang digunakan oleh Forem dalam hal memperluas fungsionalitas.

Pola ini terdiri dari memecah fungsionalitas Anda menjadi Rails Concerns dan mengekspos titik konfigurasi untuk menyertakan atau mengecualikan ini dalam cakupan yang diberikan. Mesin yang dibentuk dengan cara ini nyaman bagi pengguna akhir – faktor yang berkontribusi terhadap kesuksesan dan popularitas Perangkat. Dan sekarang Anda tahu bagaimana melakukannya juga!

Kesenangan

Solusi e-commerce open source lengkap untuk Ruby on Rails

Spree melakukan upaya kolosal untuk mengendalikan aplikasi monolitik mereka dengan beralih menggunakan Engines. Desain arsitektur yang sekarang mereka gunakan adalah permata "Spree" yang berisi banyak permata Engine.

Engine ini membuat partisi dalam perilaku yang mungkin biasa Anda lihat dalam aplikasi monolitik atau tersebar di seluruh aplikasi:

  • foya_api (API RESTful)
  • spree_frontend (Komponen yang menghadap pengguna)
  • foya_backend (Area admin)
  • foya_cmd (Alat baris perintah)
  • spree_core (Model & Mailer, komponen dasar Spree yang tidak dapat dijalankan tanpanya)
  • spree_sample (Contoh data)

Permata yang tercakup menyatukan ini bersama-sama meninggalkan pengembang dengan pilihan di tingkat fungsionalitas yang dibutuhkan. Misalnya, Anda dapat menjalankan hanya dengan spree_core Engine dan membungkus antarmuka Anda sendiri di sekitarnya.

Permata Spree utama membutuhkan mesin ini:

 # lib/spree.rb require 'spree_core' require 'spree_api' require 'spree_backend' require 'spree_frontend'

Setiap Engine kemudian perlu menyesuaikan engine_name dan jalur root (yang terakhir biasanya menunjuk ke permata tingkat atas) dan mengonfigurasi diri mereka sendiri dengan menghubungkan ke proses inisialisasi:

 # 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

Anda mungkin atau mungkin tidak mengenali metode penginisialisasi ini: ini adalah bagian dari Railtie dan merupakan pengait yang memberi Anda kesempatan untuk menambah atau menghapus langkah-langkah dari inisialisasi kerangka kerja Rails. Spree sangat bergantung pada kait ini untuk mengonfigurasi lingkungan kompleksnya untuk semua mesinnya.

Menggunakan contoh di atas saat runtime, Anda akan memiliki akses ke pengaturan Anda dengan mengakses konstanta Rails tingkat atas:

 Rails.application.config.spree

Dengan panduan pola desain engine Rails di atas, kita dapat menyebutnya sehari, tetapi Spree memiliki banyak kode yang luar biasa, jadi mari selami bagaimana mereka menggunakan inisialisasi untuk berbagi konfigurasi antara Engine dan Aplikasi Rails utama.

Spree memiliki sistem preferensi kompleks yang dimuat dengan menambahkan langkah ke dalam proses inisialisasi:

 # api/lib/spree/api/engine.rb initializer "spree.environment", :before => :load_config_initializers do |app| app.config.spree = Spree::Core::Environment.new end

Di sini, kami melampirkan ke app.config.spree contoh Spree::Core::Environment baru. Dalam aplikasi Rails Anda akan dapat mengakses ini melalui Rails.application.config.spree dari mana saja - model, pengontrol, tampilan.

Pindah ke bawah, kelas Spree::Core::Environment yang kami buat terlihat seperti ini:

 module Spree module Core class Environment attr_accessor :preferences def initialize @preferences = Spree::AppConfiguration.new end end end end

Ini memperlihatkan variabel :preferences yang disetel ke instance baru dari kelas Spree::AppConfiguration , yang pada gilirannya menggunakan metode preference yang ditentukan dalam kelas Preferences::Configuration untuk mengatur opsi dengan default untuk konfigurasi aplikasi umum:

 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

Saya tidak akan menampilkan file Preferences::Configuration karena akan membutuhkan sedikit penjelasan tetapi pada dasarnya ini adalah gula sintaksis untuk mendapatkan dan mengatur preferensi. (Sebenarnya, ini adalah penyederhanaan yang berlebihan dari fungsinya, karena sistem preferensi akan menyimpan nilai selain default untuk preferensi yang ada atau baru dalam database, untuk kelas ActiveRecord apa pun dengan kolom :preference - tetapi Anda tidak perlu melakukannya tahu bahwa.)

Inilah salah satu opsi yang sedang beraksi:

 module Spree class Calculator < Spree::Base def self.calculators Rails.application.config.spree.calculators end # ... end end

Kalkulator mengontrol segala macam hal di Spree – biaya pengiriman, promo, penyesuaian harga produk – sehingga memiliki mekanisme untuk menukarnya dengan cara ini meningkatkan ekstensibilitas Engine.

Salah satu dari banyak cara Anda dapat mengganti pengaturan default untuk preferensi ini adalah dalam penginisialisasi di Aplikasi Rails utama:

 # config/initializergs/spree.rb Spree::Config do |config| config.admin_interface_logo = 'company_logo.png' end

Jika Anda telah membaca RailsGuide on Engines, mempertimbangkan pola desainnya atau membuat Engine sendiri, Anda akan tahu bahwa mengekspos penyetel dalam file penginisialisasi untuk digunakan seseorang adalah hal yang sepele. Jadi Anda mungkin bertanya-tanya, mengapa harus repot dengan pengaturan dan sistem preferensi? Ingat, sistem preferensi memecahkan masalah domain untuk Spree. Menghubungkan ke dalam proses inisialisasi dan mendapatkan akses ke kerangka kerja Rails dapat membantu Anda memenuhi kebutuhan Anda dengan cara yang dapat dipertahankan.

Pola desain mesin ini berfokus pada penggunaan kerangka Rails sebagai konstanta di antara banyak bagiannya yang bergerak untuk menyimpan pengaturan yang tidak (umumnya) berubah saat runtime, tetapi berubah di antara penginstalan aplikasi.

Jika Anda pernah mencoba memberi label putih pada aplikasi Rails, Anda mungkin akrab dengan skenario preferensi ini, dan pernah merasakan sakitnya tabel "pengaturan" database yang berbelit-belit dalam proses penyiapan yang panjang untuk setiap aplikasi baru. Sekarang Anda tahu jalur yang berbeda tersedia dan itu luar biasa - tos!

KilangCMS

Sistem manajemen konten sumber terbuka untuk Rails

Konvensi atas konfigurasi siapa pun? Rails Engines pasti bisa tampak lebih seperti latihan dalam konfigurasi di kali, tetapi RefineryCMS mengingat beberapa keajaiban Rails itu. Ini adalah seluruh isi direktori lib -nya:

 # 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. Jika Anda tidak tahu dari sini, tim Refinery benar-benar tahu apa yang mereka lakukan. Mereka menggulung dengan konsep extension yang pada dasarnya adalah Engine lain. Seperti Spree, ia memiliki permata jahitan yang mencakup, tetapi hanya menggunakan dua jahitan, dan menyatukan koleksi Mesin untuk memberikan fungsionalitas lengkapnya.

Ekstensi juga dibuat oleh pengguna Engine, untuk membuat perpaduan fitur CMS mereka sendiri untuk blogging, berita, portofolio, testimonial, pertanyaan, dll. (daftarnya panjang), semuanya terhubung ke inti RefineryCMS.

Desain ini mungkin menarik perhatian Anda karena pendekatan modularnya, dan Refinery adalah contoh yang bagus dari pola desain Rails ini. "Bagaimana cara kerjanya?" Saya mendengar Anda bertanya.

Mesin core memetakan beberapa kait untuk digunakan mesin lain:

 # 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

Seperti yang Anda lihat, before_inclusion dan after_inclusion hanya menyimpan daftar procs yang akan dijalankan nanti. Proses penyertaan Refinery memperluas aplikasi Rails yang saat ini dimuat dengan pengontrol dan helper Refinery. Inilah salah satu aksinya:

 # authentication/lib/refinery/authentication/engine.rb before_inclusion do [Refinery::AdminController, ::ApplicationController].each do |c| Refinery.include_once(c, Refinery::AuthenticatedSystem) end end

Saya yakin Anda telah memasukkan metode otentikasi ke dalam ApplicationController dan AdminController Anda sebelumnya, ini adalah cara terprogram untuk melakukannya.

Melihat sisa file Mesin Otentikasi itu akan membantu kami mengumpulkan beberapa komponen utama lainnya:

 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

Di bawah tenda, ekstensi Refinery menggunakan sistem Plugin . Langkah initializer akan terlihat familier dari analisis kode Spree, di sini hanya memenuhi persyaratan metode register untuk ditambahkan ke daftar Refinery::Plugins yang dilacak oleh ekstensi core , dan Refinery.register_extension hanya menambahkan nama modul ke daftar yang disimpan di pengakses kelas.

Inilah yang mengejutkan: kelas Refinery::Authentication benar-benar membungkus Devise, dengan beberapa penyesuaian. Jadi itu kura-kura sepanjang jalan!

Ekstensi dan plugin adalah konsep yang telah dikembangkan oleh Refinery untuk mendukung ekosistem kaya aplikasi rel mini dan perkakas - think rake generate refinery:engine . Pola desain di sini berbeda dari Spree dengan menerapkan API tambahan di sekitar Rails Engine untuk membantu mengelola komposisinya.

Idiom "The Rails Way" adalah inti dari Refinery, semakin hadir di aplikasi mini-rails mereka, tetapi dari luar Anda tidak akan tahu itu. Mendesain batasan pada tingkat komposisi aplikasi sama pentingnya, mungkin lebih penting, daripada membuat API bersih untuk Kelas dan Modul yang digunakan dalam Aplikasi Rails Anda.

Membungkus kode yang tidak Anda kendalikan langsung adalah pola umum, ini adalah pandangan ke depan dalam mengurangi waktu pemeliharaan ketika kode itu berubah, membatasi jumlah tempat yang Anda perlukan untuk membuat amandemen untuk mendukung peningkatan. Menerapkan teknik ini di samping fungsionalitas partisi menciptakan platform yang fleksibel untuk komposisi, dan inilah contoh dunia nyata yang ada tepat di bawah hidung Anda - harus menyukai open source!

Kesimpulan


Kami telah melihat empat pendekatan untuk merancang pola mesin Rails dengan menganalisis permata populer yang digunakan dalam aplikasi dunia nyata. Layak untuk membaca melalui repositori mereka untuk belajar dari banyak pengalaman yang telah diterapkan dan diulang. Berdiri di atas bahu raksasa.

Dalam panduan Rails ini, kami telah berfokus pada pola dan teknik desain untuk mengintegrasikan Rails Engines dan aplikasi Rails pengguna akhir mereka, sehingga Anda dapat menambahkan pengetahuan ini ke sabuk alat Rails Anda.

Saya harap Anda telah belajar sebanyak saya dari meninjau kode ini dan merasa terinspirasi untuk memberi Rails Engines kesempatan ketika mereka sesuai dengan tagihan. Terima kasih banyak kepada pengelola dan kontributor permata yang kami ulas. Orang-orang kerja yang hebat!

Terkait: Pemotongan stempel waktu: Kisah ActiveRecord Ruby on Rails