Rails + Merb
A situação prometeu mudar quando aconteceu a lendária decisão de mesclar tecnologias do Merb dentro do Rails, e uma das coisas interessantes que o Merb tinha era o conceito de Slices, justamente pequenos trechos MVC que você poderia plugar à sua aplicação.
Um ano depois disso, o Rails 3 Beta 3 está aqui, se preparando para o lançamento oficial, e ele trouxe de fato muitas mudanças interessantes, incluindo uma grande refatoração no seu processo de boot.
Quando criamos uma nova aplicação de Rails 3, superficialmente não parece que mudou muita coisa. A primeira mudança que vemos é o arquivo “config/application.rb”. Antigamente, toda a configuração se concentrava no bom e velho “config/environment.rb”, mas agora ele tem só isto:
1 2 3 4 5 |
# Load the rails application require File.expand_path('../application', __FILE__) # Initialize the rails application MyApp::Application.initialize! |
Esse “MyApp” é uma classe que representa sua aplicação e ela tem o mesmo nome da sua aplicação. Quando você usa o comando “rails [my_app]” para criar uma nova aplicação, o generator usa esse nome para criar essa classe. Note que a primeira linha carrega o arquivo “config/application.rb”, que é onde essa classe está declarada. Um trecho é este:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
require File.expand_path('../boot', __FILE__) require 'rails/all' # If you have a Gemfile, require the gems listed there, including any gems # you've limited to :test, :development, or :production. Bundler.require(:default, Rails.env) if defined?(Bundler) module MyApp class Application < Rails::Application ... end end |
Este arquivo, por sua vez, carrega o também velho conhecido “config/boot.rb” que inicializa a aplicação. Em seguida ele declara a dependência aos frameworks do Rails. Note que ele requer o “rails/all”. No código fonte do pacote Railties, o arquivo “lib/rails/all.rb” tem este conteúdo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
require "rails" %w( active_record action_controller action_mailer active_resource rails/test_unit ).each do |framework| begin require "#{framework}/railtie" rescue LoadError end end |
Você pode mudar o ‘require’ e requerir menos frameworks, por exemplo, se não estiver interessado no Action Mailer, ou no Active Resource. Voltando ao “application.rb”, em seguida ele checa se o Bundler está carregado e então requer as dependências declaradas no arquivo “Gemfile”. Leia a documentação do Bundler para entender sobre esse arquivo, vou explorar esse assunto em outro artigo, mas por enquanto basta entender que antigamente declarávamos as gems no arquivo “environment.rb” usando linhas como “config.gem ’paperclip”, agora declaramos no arquivo “Gemfile”.
Note também que ele carrega o arquivo “railtie.rb” de cada um dos frameworks como Active Record. É porque esta é a forma que cada um deles é configurável e consegue se integrar ao processo de boot do Rails e ao mesmo tempo serem desacoplados uns dos outros.
Finalmente, temos a declaração da classe “MyApp::Application”. Uma aplicação Rails agora não é mais global, ela é delimitada por um namespace adequado, que por sua vez dá acesso às suas APIs internas de inicialização, configuração e tudo mais. De fato, no mesmo processo, podemos carregar múltiplas aplicações Rails isoladas.
Olhando no código fonte do pacote Railties novamente, vemos que o “lib/rails/application.rb” é declarado desta forma:
1 2 3 4 5 |
module Rails class Application < Engine ... end end |
Ou seja, toda aplicação Rails é derivada de um “Engine”. Vejamos agora sua declaração em “lib/rails/engine.rb”.
1 2 3 4 5 6 7 8 9 10 11 |
module Rails class Engine < Railtie autoload :Configurable, "rails/railtie/configurable" autoload :Configuration, "rails/railtie/configuration" include Initializable ABSTRACT_RAILTIES = %w(Rails::Railtie Rails::Plugin Rails::Engine Rails::Application) ... end end |
Uma Engine, por sua vez, deriva da classe-pai “Railtie”. Ela é uma classe bem simples, declarada em “lib/rails/railtie.rb” que define o núcleo de uma aplicação Rails. Segundo a documentação, você implementa um Railtie em um plugin, por exemplo, se precisar criar initializadores, configurar partes do framework Rails, criar chaves “config.*” de configuração de ambientes, se assinar nos novos notificadores ActiveSupport::Notifications (mais um assunto para outro artigo), adicionar tarefas de Rake no Rails.
Como podem ver no trecho listado acima, ele carrega os módulos que o tornam configurável e faz mixin do módulo que o torna inicializável.
Finalmente, temos uma última classe, o Rails::Plugin, que é definida em “lib/rails/plugin.rb”.
1 2 3 4 5 |
module Rails class Plugin < Engine ... end end |
Como podemos ver um Rails::Application e Rails::Plugin ambos herdam de Rails::Engine que, por sua vez, herda de Rails::Railtie. Essa é a hierarquia da nova estrutura do Rails. Um Rails::Plugin é um Rails::Engine que se carrega mais tarde no processo de inicialização e por isso não tem as mesmas oportunidades de customização de uma Engine. E novamente segundo a documentação, diferente de um Railtie ou um Engine, você não deve criar uma classe que herda de Rails::Plugin pois todo pacote dentro de “vendor/plugins” é um plugin e ele automaticamente vai procurar por um arquivo “init.rb” dentro dela para começar.
Expliquei tudo isso para dizer que, no Rails 3, um Engine não só melhorou como agora ele é a espinha dorsal de todo o resto do framework. Lembrando também que o Rails::Application é o responsável por carregar Rails Metal (end-points mais leves que o ActionController::Base) e também outros middleware Rack. O Rails::Application tem um método “call”, justamente o mínimo necessário para constituir uma aplicação Rack. O Application faz a costura com o ActionDispatch para cuidar de toda a complexidade dos roteamentos entre applications, engines, rack middlewares e assim por diante.
Introdução a Engines
Isso tudo dito, ainda estou experimentando e não cheguei a uma solução ótima, mas me parece que uma forma de começar um novo Rails Engine é basicamente criando uma aplicação Rails normal. Por exemplo, digamos que eu ache que uma funcionalidade de Blog é importante para ser reusado em meus projetos e por isso quero uma Engine de Blogs. Começo assim:
1 2 |
rails blog cd blog |
Existe muita coisa que não vou precisar:
1 2 3 4 5 6 7 8 |
rm -Rf log rm -Rf tmp rm -Rf doc rm -Rf public rm -Rf vendor rm -Rf db rm -Rf config/environments/ rm config/boot.rb |
Os outros diretórios funcionam normalmente, o “app” para nossos arquivos MVC, o “config” para configurar o Engine e as Rotas, o “lib” para ter código customizado e também o “lib/tasks” para tarefas Rake. Tudo isso deve funcionar. Depois vou apagar também o “script”, mas por agora ele será útil.
Agora, podemos fazer assim:
1 |
./script/rails g scaffold Blog::Post title:string post:text |
O bom e velho model de Post. Notem que estou usando um namespace, “Blog”, isso é importante para evitar conflitos, especialmente com nomes muito comuns como “Post”. Se existir um outro model “Post” na aplicação onde você pretende integrar esta Engine, terá problemas.
Um detalhe é que o Generator atual não lida muito bem com namespaces. Significa que precisaremos mexer nos arquivos gerados. Em particular, no controller a ação “index” do Blog::PostsController estará assim:
1 2 3 4 5 6 |
unloadable # acrescente isto def index @blog_posts = Blog::Post.all ... end |
Outra coisa interessante é declarar “unloadable” no corpo da classe controller. Isso permite que em modo de desenvolvimento você consiga recarregá-lo.
Substitua “@blog_posts” por “@posts”, isso deve estar errado no template. Falando em templates, as rotas nomeadas nas views também estão todas erradas, por exemplo, no “app/views/blog/posts/index.html.erb” teremos este trecho:
1 2 3 4 5 6 7 |
<tr> <td><%= post.title %></td> <td><%= post.post %></td> <td><%= link_to 'Show', post %></td> <td><%= link_to 'Edit', edit_post_path(post) %></td> <td><%= link_to 'Destroy', post, :confirm => 'Are you sure?', :method => :delete %></td> </tr> |
Mas deveria ser assim:
1 2 3 4 5 6 7 |
<tr> <td><%= post.title %></td> <td><%= post.post %></td> <td><%= link_to 'Show', [:blog, post] %></td> <td><%= link_to 'Edit', edit_blog_post_path(post) %></td> <td><%= link_to 'Destroy', [:blog, post], :confirm => 'Are you sure?', :method => :delete %></td> </tr> |
Estou assumindo que todos aqui sabem lidar com rotas nomeadas, especialmente com rotas aninhadas e rotas com namespace como é o caso acima. Se não, leiam o Guia Oficial sobre o assunto para entender melhor.
Precisamos mudar o arquivo de rotas também. No nosso caso basta modificar o “config/routes.rb” para ter o seguinte:
1 2 3 4 5 |
Rails.application.routes.draw do |map| namespace :blog do resources :posts end end |
Veja que estamos usando o “Rails.application” em vez de ir direto na classe da aplicação, porque a idéia é esta Engine se plugar à aplicação principal. Desta forma as rotas da Engine estarão disponíveis no conjunto de rotas principais da aplicação.
Agora, precisamos configurar a Engine propriamente dita, para isso precisamos modificar o arquivo “config/application.rb” para ter algo como:
1 2 3 4 5 |
module Blog class Engine < Rails::Engine ... end end |
Se fosse uma aplicação Rails normal, estaria herdando de “Rails::Application”. No corpo da classe você pode fazer as mesmas configurações que faria no inicializador de uma aplicação, todas aquelas chaves tipo “config.load_paths” ou “config.time_zone” e assim por diante. Veja os comentários do arquivo “application.rb” original pois ele já vem bem documentado para começar.
Não sei se é necessário, mas para garantir modifiquei o “config/environment.rb” para ter apenas:
1 |
require File.expand_path('../application', __FILE__) |
Pronto. Isso deve ser o suficiente como esqueleto básico. Se você colocar esse projeto “blog” dentro de “vendor/plugins” da sua outra aplicação, ela já estará automaticamente integrada. Se executar “rake routes” verá as rotas do seu projeto e da Engine. Lembrando que outros diretórios padrão como “config/initializers” também funcionam como você esperaria então dá para organizar bem a engine.
Mas ainda há mais algumas coisas que precisam ser feitas. Por exemplo:
- É melhor empacotar esta Engine como uma Gem. Nesse caso você deve configurar um arquivo “.gemspec” na raíz da sua Engine para gerar uma “.gem”. Uma opção é usar o Jeweler para criar e manter a Gemspec.
- Migrations não são automaticamente executadas da Engine. Este é um dos poucos casos onde você vai precisar manualmente criar ou um Generator ou uma tarefa Rake para copiar o arquivo de Migration para o diretório “db/migrate” do projeto principal. Não é algo difícil de fazer e este tutorial pode ser um bom começo.
- Além de Migrations, se você precisar de CSS, Javascripts ou outros arquivos que normalmente colocaria no diretório “public”, também precisam ser copiados ao diretório “public” do projeto-pai. Novamente, as soluções são usando Generators ou tarefas Rake.
Novamente, esta é só uma introdução. Ainda quero escrever outro artigo mostrando exemplos mais completos. Se tiverem sugestões de técnicas e boas práticas, elas são bem vindas, não deixem de comentar no artigo.