Por que os Rails Engines não são usados com mais frequência? Não sei a resposta, mas acho que a generalização de 'Tudo é um motor' escondeu os domínios do problema que eles podem ajudar a resolver.
O soberbo Documentação do Rails Guide para começar a usar o Rails Engines, consulte quatro exemplos populares de implementações do Rails Engine: Forem, Devise, Spree e RefineryCMS. Esses são casos de uso do mundo real fantásticos para Engines, cada um usando uma abordagem diferente para integração com um aplicativo Rails.
Examinar partes de como essas joias são configuradas e compostas fornecerá Desenvolvedores de Ruby on Rails conhecimento valioso de quais padrões ou técnicas são experimentados e testados na natureza, então, quando chegar a hora, você terá algumas opções extras para avaliar.
Eu espero que você tenha uma familiaridade superficial de como um Engine funciona, então se você sentir que algo não está somando muito, por favor leia o mais excelente Guia Rails Introdução aos motores .
Sem mais delongas, vamos nos aventurar no mundo selvagem dos exemplos de engine Rails!
o retorno de chamada não é um nó de função js
Um motor para Rails que pretende ser o melhor pequeno sistema de fórum de todos os tempos
Esta joia segue a direção do Guia Rails sobre Motores ao pé da letra. É um exemplo considerável e examinar seu repositório lhe dará uma ideia de até onde você pode esticar a configuração básica.
É uma gem de mecanismo único que usa algumas técnicas para se integrar ao aplicativo principal.
module ::Forem class Engine A parte interessante aqui é o Decorators.register!
método de classe, exposto pela gema Decorators. Ele encapsula o carregamento de arquivos que não seriam incluídos no processo de carregamento automático do Rails. Você deve se lembrar de que usar require
explícito | declarações estragam o recarregamento automático no modo de desenvolvimento, portanto, este é um salva-vidas! Será mais claro usar o exemplo do Guia para ilustrar o que está acontecendo:
config.to_prepare do Dir.glob(Rails.root + 'app/decorators/**/*_decorator*.rb').each do |c| require_dependency(c) end end
A maior parte da mágica para a configuração do Forem acontece na definição do módulo principal superior de Forem
. Este arquivo depende de um user_class
variável sendo definida em um arquivo inicializador:
Forem.user_class = 'User'
Você consegue isso usando mattr_accessor
mas está tudo no Guia do Rails, então não vou repetir isso aqui. Com isso no lugar, Forem então decora a classe de usuário com tudo o que precisa para executar seu aplicativo:
module Forem class <'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? # ...
O que acaba sendo bastante! Eu cortei a maioria, mas deixei em uma definição de associação, bem como um método de instância para mostrar o tipo de linhas que você pode encontrar lá.
Vislumbrar todo o arquivo pode mostrar como pode ser gerenciável portar parte de seu aplicativo para reutilização em um mecanismo.
Decorar é o nome do jogo no uso padrão do mecanismo. Como um usuário final da gema, você pode sobrescrever o modelo, visualização e controladores criando suas próprias versões das classes usando o caminho do arquivo e as convenções de nomenclatura de arquivo estabelecidas no README da gema decoradora. Porém, há um custo associado a essa abordagem, especialmente quando o Engine recebe uma atualização de versão principal - a manutenção de manter suas decorações funcionando pode rapidamente sair do controle. Não estou citando Forem aqui, acredito que eles são firmes em manter uma funcionalidade básica bem unida, mas tenha isso em mente se você criar um Motor e decidir fazer uma revisão.
Vamos recapitular: este é o padrão de design do motor Rails padrão que depende dos usuários finais que decoram visualizações, controladores e modelos, juntamente com a configuração de configurações básicas por meio de arquivos de inicialização. Isso funciona bem para uma funcionalidade muito focada e relacionada.
Lema
Você descobrirá que um Engine é muito semelhante a uma aplicação Rails, com views
, controllers
e models
diretórios. Devise é um bom exemplo de encapsular um aplicativo e expor um ponto de integração conveniente. Vamos ver como isso funciona exatamente.
Você reconhecerá essas linhas de código se tiver sido um desenvolvedor Rails por mais de algumas semanas:
como obter os números dos cartões de crédito das pessoas online
class User Cada parâmetro passado para o devise
método representa um módulo dentro do Devise Engine. Existem dez desses módulos no total que herdam do familiar ActiveSupport::Concern
. Eles estendem seu User
classe invocando devise
método dentro de seu escopo.
Ter este tipo de ponto de integração é muito flexível, você pode adicionar ou remover qualquer um desses parâmetros para alterar o nível de funcionalidade que você precisa que o Engine execute. Isso também significa que você não precisa codificar qual model você gostaria de usar em um arquivo inicializador, como sugerido pelo Rails Guide on Engines. Em outras palavras, isso não é necessário:
Devise.user_model = 'User'
Essa abstração também significa que você pode aplicar isso a mais de um modelo de usuário dentro do mesmo aplicativo (admin
e user
por exemplo), enquanto a abordagem do arquivo de configuração deixaria você amarrado a um único modelo com autenticação. Este não é o maior argumento de venda, mas ilustra uma maneira diferente de resolver um problema.
O Devise estende ActiveRecord::Base
com seu próprio módulo que inclui o devise
definição do método:
# lib/devise/orm/active_record.rb ActiveRecord::Base.extend Devise::Models
Qualquer classe que herde de ActiveRecord::Base
agora terá acesso aos métodos de classe definidos em 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
(Eu removi muitos códigos (# ...
) para destacar as partes importantes.)
Parafraseando o código, para cada nome de módulo passado para o devise
método que somos:
- carregando o módulo que especificamos que reside em
Devise::Models
(Devise::Models.const_get(m.to_s.classify
) - estendendo o
User
classe com ClassMethods
módulo se tiver um - incluem o módulo especificado (
include mod
) para adicionar seus métodos de instância à classe que chama o devise
método (User
)
Se você quiser criar um módulo que possa ser carregado dessa forma, você precisará certificar-se de que segue o ActiveSupport::Concern
usual | interface, mas o namespace sob o Devise:Models
pois é aqui que procuramos recuperar a constante:
module Devise module Models module Authenticatable extend ActiveSupport::Concern included do # ... end module ClassMethods # ... end end end end
Ufa.
Se você já usou o Rails ’Concerns antes e experimentou a reutilização que eles oferecem, então você pode apreciar as sutilezas desta abordagem. Resumindo, dividir a funcionalidade dessa maneira torna o teste mais fácil, sendo abstraído de um ActiveRecord
modelo e tem uma sobrecarga menor do que o padrão usado pelo Forem quando se trata de estender a funcionalidade.
Este padrão consiste em dividir sua funcionalidade em Rails Concerns e expor um ponto de configuração para incluir ou excluir dentro de um determinado escopo. Um motor formado dessa maneira é conveniente para o usuário final - um fator que contribui para o sucesso e a popularidade do Devise. E agora você também sabe fazer!
Farra
Uma solução de e-commerce de código aberto completa para Ruby on Rails
tutorial do angular 5 para iniciantes passo a passo
Spree fez um esforço colossal para colocar seu aplicativo monolítico sob controle com uma mudança para o uso de motores. O projeto de arquitetura com o qual eles estão trabalhando agora é uma joia “Spree” que contém muitas joias do motor.
Esses motores criam partições no comportamento que você pode estar acostumado a ver em um aplicativo monolítico ou espalhado entre os aplicativos:
- spree_api (RESTful API)
- spree_frontend (componentes voltados para o usuário)
- spree_backend (área administrativa)
- spree_cmd (ferramentas de linha de comando)
- spree_core (Modelos e Mailers, os componentes básicos do Spree sem os quais ele não pode ser executado)
- spree_sample (dados de amostra)
A gema abrangente os une, deixando o desenvolvedor com uma escolha no nível de funcionalidade a ser exigido. Por exemplo, você pode executar apenas com spree_core
Engine e envolva sua própria interface em torno dele.
A principal joia do Spree requer estes motores:
# lib/spree.rb require 'spree_core' require 'spree_api' require 'spree_backend' require 'spree_frontend'
Cada Engine precisa então personalizar seu engine_name
e root
caminho (o último geralmente apontando para a gema de nível superior) e se configurarem conectando-se ao processo de inicialização:
# api/lib/spree/api/engine.rb require 'rails/engine' module Spree module Api class Engine :load_config_initializers do |app| app.config.spree = Spree::Core::Environment.new end # ... end end end
Você pode ou não reconhecer este método inicializador: ele é parte de Railtie
e é um gancho que dá a oportunidade de adicionar ou remover etapas da inicialização do framework Rails. Spree depende muito desse gancho para configurar seu ambiente complexo para todos os seus motores.
Usando o exemplo acima em tempo de execução, você terá acesso às suas configurações acessando o nível superior Rails
constante:
Rails.application.config.spree
Com este guia de padrão de design do motor Rails acima, poderíamos encerrar o dia, mas Spree tem uma tonelada de código incrível, então vamos mergulhar em como eles utilizam a inicialização para compartilhar a configuração entre os motores e o aplicativo Rails principal.
O Spree possui um sistema de preferências complexo que carrega adicionando uma etapa ao processo de inicialização:
# api/lib/spree/api/engine.rb initializer 'spree.environment', :before => :load_config_initializers do |app| app.config.spree = Spree::Core::Environment.new end
Aqui, estamos anexando a app.config.spree
um novo Spree::Core::Environment
instância. Dentro da aplicação rails você poderá acessar isto via Rails.application.config.spree
de qualquer lugar - modelos, controladores, visualizações.
Continuando para baixo, o Spree::Core::Environment
classe que criamos tem a seguinte aparência:
module Spree module Core class Environment attr_accessor :preferences def initialize @preferences = Spree::AppConfiguration.new end end end end
Ele expõe um :preferences
variável definida para uma nova instância de Spree::AppConfiguration
classe, que por sua vez usa um preference
método definido no Preferences::Configuration
classe para definir opções com padrões para a configuração geral do aplicativo:
module Spree class AppConfiguration Não vou mostrar o Preferences::Configuration
porque vai levar um pouco de explicação, mas essencialmente é um açúcar sintático para obter e definir preferências. (Na verdade, esta é uma simplificação exagerada de sua funcionalidade, pois o sistema de preferências salvará valores diferentes do padrão para preferências existentes ou novas no banco de dados, para qualquer ActiveRecord
classe com uma coluna :preference
mas você não precisa saber disso.)
Aqui está uma dessas opções em ação:
module Spree class Calculator Calculadoras controlam todos os tipos de coisas no Spree - custos de remessa, promoções, ajustes de preços de produtos - portanto, ter um mecanismo para trocá-los dessa maneira aumenta a extensibilidade do motor.
Uma das muitas maneiras de sobrescrever as configurações padrão para essas preferências é dentro de um inicializador no aplicativo Rails principal:
# config/initializergs/spree.rb Spree::Config do |config| config.admin_interface_logo = 'company_logo.png' end
Se você leu o RailsGuide em motores , considerando seus padrões de projeto ou construindo você mesmo um Engine, você saberá que é trivial expor um setter em um arquivo inicializador para alguém usar. Então você deve estar se perguntando, por que tanto barulho com o sistema de configuração e preferência? Lembre-se de que o sistema de preferência resolve um problema de domínio para Spree. Conectar-se ao processo de inicialização e obter acesso ao framework Rails pode ajudá-lo a atender seus requisitos de forma sustentável.
Este padrão de design de mecanismo foca no uso do framework Rails como a constante entre suas muitas partes móveis para armazenar configurações que (geralmente) não mudam no tempo de execução, mas mudam entre as instalações de aplicativos.
Se você já tentou etiqueta branca um aplicativo Rails, você pode estar familiarizado com este cenário de preferências e ter sentido a dor das complicadas tabelas de “configurações” do banco de dados em um longo processo de configuração para cada novo aplicativo. Agora você sabe que um caminho diferente está disponível e isso é incrível - high five!
RefineryCMS
Um sistema de gerenciamento de conteúdo de código aberto para Rails
c ++ da maneira mais difícil
Convenção sobre configuração de alguém? Os Rails Engines podem definitivamente parecer mais um exercício de configuração às vezes, mas o RefineryCMS lembra um pouco da magia do Rails. Este é todo o conteúdo de lib
diretório:
# lib/refinerycms.rb require 'refinery/all' # lib/refinery/all.rb %w(core authentication dashboard images resources pages).each do |extension| require 'refinerycms-#{extension}' end
Uau. Se você não pode dizer com isso, a equipe da Refinaria realmente sabe o que está fazendo. Eles rolam com o conceito de um extension
que é, em essência, outro motor. Como o Spree, ele tem uma gema de costura abrangente, mas usa apenas dois pontos e reúne uma coleção de motores para oferecer seu conjunto completo de funcionalidades.
Extensões também são criadas por usuários do Engine, para criar seu próprio mash-up de recursos CMS para blogs, notícias, portfólio, depoimentos, investigações, etc. (é uma longa lista), todos ligados ao RefineryCMS central.
Este projeto pode chamar sua atenção por sua abordagem modular, e Refinery é um ótimo exemplo desse padrão de projeto Rails. 'Como funciona?' Eu ouço você perguntar.
O core
motor mapeia alguns ganchos para os outros motores usarem:
# 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
Como você pode ver o before_inclusion
e after_inclusion
apenas armazene uma lista de procs que serão executados mais tarde. O processo de inclusão da Refinaria estende os aplicativos Rails carregados atualmente com controladores e ajudantes da Refinaria. Aqui está um em ação:
# authentication/lib/refinery/authentication/engine.rb before_inclusion do [Refinery::AdminController, ::ApplicationController].each do |c| Refinery.include_once(c, Refinery::AuthenticatedSystem) end end
Tenho certeza de que você colocou métodos de autenticação em seu ApplicationController
e AdminController
antes, essa é uma maneira programática de fazer isso.
Qual das alternativas a seguir é verdadeira para um teste de unidade?
Analisar o restante do arquivo do mecanismo de autenticação nos ajudará a reunir alguns outros componentes importantes:
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
Sob o capô, as extensões da Refinaria usam um Plugin
sistema. O initializer
etapa parecerá familiar a partir da análise de código do Spree, aqui estamos apenas encontrando o register
requisitos de métodos a serem adicionados à lista de Refinery::Plugins
que o core
extensão mantém registro de, e Refinery.register_extension
apenas adiciona o nome do módulo a uma lista armazenada em um acessador de classe.
Aqui está um choque: o Refinery::Authentication
classe é realmente um invólucro em torno do Devise, com alguma personalização. Então, são tartarugas até o fim!
As extensões e plug-ins são conceitos que a Refinery desenvolveu para suportar seu rico ecossistema de aplicativos e ferramentas de mini-rails - pense rake generate refinery:engine
. O padrão de design aqui difere do Spree, impondo uma API adicional em torno do Rails Engine para auxiliar no gerenciamento de sua composição.
O idioma “The Rails Way” está no cerne da Refinaria, cada vez mais presente em seus aplicativos mini-rails, mas de fora você não saberia disso. Projetar limites no nível de composição do aplicativo é tão importante, possivelmente mais, do que criar uma API limpa para suas classes e módulos usados em seus aplicativos Rails.
Encapsular o código sobre o qual você não tem controle direto é um padrão comum, é uma previsão na redução do tempo de manutenção para quando o código for alterado, limitando o número de locais em que você precisará fazer alterações para oferecer suporte a atualizações. Aplicar essa técnica junto com a funcionalidade de particionamento cria uma plataforma flexível para composição, e aqui está um exemplo do mundo real bem debaixo do seu nariz - tenho que amar o código aberto!
Conclusão
Vimos quatro abordagens para projetar padrões de engine Rails analisando joias populares sendo usadas em aplicações do mundo real. Vale a pena ler seus repositórios para aprender com uma vasta experiência já aplicada e iterada. Fique de pé sobre os ombros dos gigantes.
Neste guia Rails, nos concentramos nos padrões de design e técnicas para integrar os Rails Engines e os aplicativos Rails de seus usuários finais, para que você possa adicionar o conhecimento deles ao seu Correia de ferramentas de trilhos .
Espero que você tenha aprendido tanto quanto eu ao revisar este código e se sinta inspirado a dar uma chance aos Rails Engines quando eles forem adequados. Um grande obrigado aos mantenedores e contribuintes das joias que revisamos. Bom trabalho pessoal!
Relacionado: Truncamento de carimbo de data / hora: um conto de Ruby on Rails ActiveRecord