#277 Mountable Engines
- Download:
- source codeProject Files in Zip (56.6 KB)
- mp4Full Size H.264 Video (19.6 MB)
- m4vSmaller H.264 Video (12.7 MB)
- webmFull Size VP8 Video (18.7 MB)
- ogvFull Size Theora Video (23.3 MB)
Le Rails 3.1 HackFest a eu lieu le week-end dernier et, grâce au dur travail des participants, la cinquième release candidate de Rails 3.1 est maintenant disponible. Cette release contient d'importantes corrections pour les mountable engines. Les mountable engines nous permettent de monter une application Rails à l'intérieur d'une autre. Nous allons voir cela dans cet épisode.
Vous vous souvenez peut être du plugin Notification Exception, couvert dans l'épisode 104 [ regarder ]. Il peut être ajouté à une application et stocke en base chaque exception levée par l'application. Il fournit également une interface utilisateur permettant de consulter les exceptions. Dans cet épisode, nous allons recréer ce plugin sous forme de mountable engine.
Mise en route
Avant de commencer à écrire notre engine, nous devons nous assurez que nous utilisons Rails 3.1 Release Candidate 5 ou une version plus récente. Nous pouvons installer la dernière version en date grâce à
$ gem install rails --pre
Une fois cela fait, nous pouvons commencer à générer notre mountable engine. Nous n'avons pas besoin de le créer dans une application existante, créer un engine revient à créer un nouvelle application Rails avec la commande rails new
. La seule différence est que nous lançons rails plugin new
à la place. Comme notre application s'occupe d'exceptions, nous l'appellerons uhoh
. Nous devons passer l'option --mountable
pour en faire un mountable engine.
$ rails plugin new uhoh --mountable
La structure du dossier de notre engine ressemble beaucoup à celle d'une application Rails classique et c'en est pratiquement une. Elle est simplement conçue pour être montée à l'intérieur d'une autre. Il existe cependant quelques différences. Il existe un certain nombre de dossier sous namespace dans l'application ; par exemple, le fichier application_controller
est dans le dossier /app/controllers/uhoh
. Le principe est le même pour les fichiers des dossiers assets
, helpers
et views
. Cela permet de garder le code de l'engine séparé de celui de l'application dans laquelle il sera monté. La présence du dossier assets
signifie qu'il n'est plus besoin de copier les ressources media dans le dossier public
de l'application lorsque l'engine est monté. Grâce à l'asset pipeline, tout est géré pour nous.
Les assets (ressources media) sont également encapsulés dans un namespace, nous devons donc passer par ce dossier lorsque nous créons un lien vers ceux-ci. Nous pouvons également voir cela dans le dossier layouts
. Il semble y avoir un bug en RC5, nous voyons deux fichiers application.html.erb
. Nous pouvons supprimer celui situé en dehors du dossier uhoh
. Si nous regardons l'autre, nous allons voir que les références aux assets que l'on y trouve contiennent le dossier uhoh
. Chaque fois que nous créons un liens vers ces assets, nous devons inclure ce namespace.
<!DOCTYPE html> <html> <head> <title>Uhoh</title> <%= stylesheet_link_tag "uhoh/application" %> <%= javascript_include_tag "uhoh/application" %> <%= csrf_meta_tags %> </head> <body> <%= yield %> </body> </html>
Nous allons ensuite regarder l'un des fichiers clés d'un engine, engine.rb
dans le dossier /lib/uhoh
.
module Uhoh class Engine < Rails::Engine isolate_namespace Uhoh end end
C'est une classe héritant de Rails::Engine
et c'est l'endroit où la configuration est centralisée. Il existe déjà un appel à isolate_namespace
dans la classe et cela signifie que l'engine sera traité comme une unité isolée et nous n'avons donc pas à nous inquiéter pour l'application dans laquelle il sera monté.
La dernière partie de l'engine que nous allons voir est située dans le dossier /test/dummy
. On y trouve une application Rails permettant de montrer comment marche notre engine lorsqu'il est monté. Le dossier config
de l'application contient un fichier routes.rb
. Ce dernier contient un appel à mount
auquel est passée la classe principale de l'engine qui sera attaché à l'URL.
Rails.application.routes.draw do mount Uhoh::Engine => "/uhoh" end
C'est ce qu'un utilisateur devra faire lorsqu'il installera votre engine dans une application, le monter sur une URL de leur choix. C'est une application Rack et, par conséquent, si une requête arrive sur /uhoh elle sera passée à notre class Engine
. Une bonne pratique est d'ajouter cette ligne aux instructions d'installation dans le fichier README de l'engine. Cela permettra aux utilisateurs de savoir comment le monter dans le fichier routes de leur application.
Bien que cette application d'exemple soit située dans le dossier /test
, elle est également utile pour tester à la main. Si nous lançons rails s
depuis le dossier de notre engine, l'application d'exemple va se lancer. Si nous visitons http://localhost:3000/uhoh/
, nous serons menés à notre engine puisque c'est l'URL à laquelle il est monté. Nous allons voir une erreur lors de la visite de cette page. C'est tout à fait normal puisque nous n'avons pas encore écrit le contrôleur correspondant.
Nous allons donc créer un contrôleur failures
dans l'engine. Nous pouvons utiliser les générateurs de Rails pour le faire, tout comme nous le ferions dans une application Rails classique. Bien que nous soyons dans un engine, nous n'avons pas besoin de spécifier le namespace, tout cela est fait pour nous.
$ rails g controller failures index
Cela génère les mêmes fichiers que pour un contrôleur classique, à la différence que tout est dans le bon dossier de namespace. Nous allons ensuite modifier les routes de l'engine pour fournir une route root
pointant vers l'action index de notre contrôleur. Cela se fait dans le fichier /config/routes.rb
de l'engine.
Uhoh::Engine.routes.draw do root :to => "failures#index" end
Lorsque nous visitons la page http://localhost:3000/uhoh/
, nous allons maintenant voir la vue de l'action.
Sur cette page, nous voulons afficher une liste des exceptions levées. Nous allons avoir besoin d'un modèle pour stocker cette information et nous allons donc créer un simple modèle Failure
avec un champ message.
$ rails g model failure message:text
Maintenant que nous avons créé le modèle, comment lançons nous la migration ? Dans l'engine lui-même, nous pouvons lancer rake db:migrate
comme d'habitude et tout fonctionnera comme prévu. Cela ne marchera pas lorsque quelqu'un tentera de monter l'engine dans une application car rake
ne prendra pas les migrations de celui-ci. Nous devons dire aux utilisateurs d'utiliser rake uhoh:install:migrations
. Cela va copier les migrations de l'engine dans l'application afin qu'ils puissent faire appel à rake db:migrate
normalement. Inclure cette information dans les instructions d'installation de l'engine semble une bonne idée.
La console Rails fonctionne également comme attendu et nous allons maintenant l'utiliser pour créer une Failure
d'exemple.
Uhoh::Failure.create!(:message => "hello world!")
Notez que nous devons inclure le namespace chaque fois que nous référençons une classe. Maintenant que nous avons une entrée Failure
, nous allons l'afficher dans l'action index de FailuresController
.
module Uhoh class FailuresController < ApplicationController def index @failures = Failure.all end end end
Dans la console, au contraire, nous n'avons pas besoin de spécifier le namespace car nous somme déjà dans le module Uhoh
. Dans la vue, nous allons écrire un peu de code pour parcourir les exceptions et les afficher sous forme de liste.
<h1>Failures</h1> <ul> <% for failure in @failures %> <li><%= failure.message %></li> <% end %> </ul>
Lorsque nous rechargeons la page, nous allons voir la Failure
que nous avons ajoutée.
Noter les exceptions
Maintenant que nous avons une méthode pour enregistrer une Failure
, nous allons devoir en générer une chaque fois que l'application dans laquelle est monté l'engine lève une exception. Pour tester cela, nous devons simuler une exception dans l'application de démonstration. Nous allons donc nous placer dans le dossier de cette dernière et générer un contrôleur nommé simulate
avec une action failure
.
$ rails g controller simulate failure
Dans l'action, nous allons lever une exception.
class SimulateController < ApplicationController def failure raise "Simulating an exception" end end
Si nous visitons cette action dans le navigateur, nous allons voir l'exception, comme attendu.
Nous allons devoir changer notre engine de façon à ce qu'il note les exceptions et crée une nouvelle Failure
lorsqu'elles sont levées. La solution que nous allons utiliser n'est pas la plus efficace mais elle est simple et fonctionnera dans notre cas. Nous allons commencer par créer un initializer dans notre engine. Il n'y a pas de dossier initializers
dans le dossier config de l'engine mais nous pouvons en créer un et les initializers placés dedans fonctionneront. Dans ce dossier, nous allons créer un fichier nommé exception_handler.rb
.
ActiveSupport::Notifications.subscribe "process_action.action_controller" do |name, start, finish, id, payload| if payload[:exception] name, message = *payload[:exception] Uhoh::Failure.create!(:message => message) end end
Dans ce fichier, nous souscrivons à une notification (les notifications sont vues dans l'épisode 249 [regarder, lire]) qui nous indique lorsqu'une action est traitée. Lorsque c'est le cas, nous pouvons vérifier si le payload
contient une exception. Si oui, nous savons donc qu'un exception a été levée et nous pouvons stocker son message dans une nouvelle Failure
.
Nous devons redémarrer le serveur avant de tester. Nous pouvons ensuite visiter http://localhost:3000/simulate/failure
de nouveau pour lever une exception. Une fois l'exception vue, nous pouvons ensuite vérifier qu'elle est listée en allant sur http://localhost:3000/uhoh
.
Gérer les URLs dans l'engine
Tout helper d'URL que nous utilisons dans l'engine va générer des URLs pour cet engine. Par exemple, si nous ajoutons un lien vers l'URL root depuis page index
des failures
, ce lien pointera vers l'URL root de cet engine et non vers celle de l'application dans laquelle il est monté.
<p><%= link_to "Failures", root_url %></p>
Ce lien pointe vers http://localhost:3000/uhoh
qui est l'URL root de l'engine. C'est la même page que celle contenant le lien car l'URL root est définie dans les routes pour pointer sur l'action index
de FailuresController
. Nous pouvons créer des liens vers l'application elle-même en appelant le helper d'URL sur main_app
comme ceci :
<p><%= link_to "Failures", root_url %></p> <p><%= link_to "Simulate Failure", main_app.simulate_failure_path %></p>
Cela nous donne un lien vers la page de simulation d'exception de l'application, http://localhost:3000/simulate/failure
.
Que se passe-t-il si nous voulons faire cela dans l'autre sens et avoir un lien vers l'engine dans l'application ? La première chose à faire est de changer la ligne dans le fichier de routes de l'application pour donner un nom à la route qui monte l'engine. Cela se fait avec l'option :as
.
Rails.application.routes.draw do get "simulate/failure" mount Uhoh::Engine => "/uhoh", :as => "uhoh_engine" end
Nous pouvons ensuite accéder aux helpers d'URLs de l'engine en les appelant comme des méthodes de uhoh_engine
. Pour démontrer ceci, nous allons changer temporairement notre action failure
de façon à ce que, au lieu de lever une exception, elle redirige vers l'URL root de l'engine.
class SimulateController < ApplicationController def failure redirect_to uhoh_engine.root_url end end
Si nous visitons http://localhost:3000/simulate/failure
, nous allons être redirigés vers http://localhost:3000/uhoh
puisque nous utilisons le helper de l'engine pour rediriger vers l'une des ses URLs. C'est une autre fonctionnalité qu'il peut être bon de mentionner dans le README de votre engine.
La fonctionnalité de notre engine est presque complète mais la page listant les failures
est plutôt terne. Nous allons donc l'égayer un peu en ajoutant quelques assets. Tout d'abord, nous allons ajouter une image. Nous en avons déjà trouvé une et l'avons placée dans le dossier /app/assets/images/uhoh
. Pour l'inclure dans la page, nous pouvons utiliser image_tag
comme pour n'importe quelle autre image.
<%= image_tag "uhoh/alert.png" %> <h1>Failures</h1> <ul> <% for failure in @failures %> <li><%= failure.message %></li> <% end %> </ul> <p><%= link_to "Failures", root_url %></p> <p><%= link_to "Simulate Failure", main_app.simulate_failure_path %></p>
Nous allons également inclure un peu de CSS. SASS et CoffeeScript ne sont pas disponibles par défaut dans les engines, bien qu'ils puissent être ajoutés comme dépendances. Si nous ajoutons quelques CSS au fichier failures.css
, il sera inclus automatiquement.
html, body { background-color: #DDD; font-family: Verdana; } body { padding: 20px 200px; } img { display: block; margin: 0 auto; } a { color: #000; } ul { list-style: none; margin: 0; padding: 0; } li { background-color: #FFF; margin-bottom: 10px; padding: 5px 10px; }
Ceci est également valable pour JavaScript. Tout code placé dans le fichier failures.js
sera automatiquement inclus.
$(function() { $("li").click(function() { $(this).slideUp(); }); });
Lorsque nous rechargeons la page, elle sera bien mieux et nous pourrons masquer les exceptions en cliquant dessus. Cela nous montre que le JavaScript a été inclus.
C'est tout pour cet épisode. Les mountable engines sont une très bonne fonctionnalité de Rails 3.1 et valent vraiment la peine d'y jeter un œil.