#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)
Gracias al trabajo de los participantes en el Rails 3.1 Hackfest ya se encuentra disponible la quinta versión candidata de Rails 3.1, que incorpora correcciones importantes en el montaje de engines que permiten montar una aplicación Rails dentro de otra, lo que estudiaremos en este episodio.
Recordemos el plugin de Notificación de Excepciones que vimos en el Episodio 104 y que se podía añadir a una aplicación para almacenar en la base de datos todas las excepciones que ocurriesen. También proporcionaba una interfaz de usuario que permitía mostrar las excepciones. En este episodio reescribiremos dicho plugin como un engine.
Empezando
Antes de empezar a escribir nuestro engine tenemos que asegurarnos de estar con la versión candidata 5 de Rails 3.1, podemos hacerlo con
$ gem install rails --pre
Una vez que tengamos instalada la versión adecuada de Rails podemos empezar a generar nuestro engine. No tenemos por qué crearlo dentro de una aplicación Rails: de hecho, la creación de un engine es muy parecida a la creación de una aplicación Rails normal, incluyendo la generación del código porque la única diferencia es que ejecutaremos rails plugin new
en lugar de rails new
. Como nuestro engine gestiona excepciones, lo llamaremos uhoh
. Tenemos que pasar la opción --mountable
para que sea un engine que se pueda montar sobre otras aplicaciones.
$ rails plugin new uhoh --mountable
La estructura de directorios de la aplicación tiene todo el aspecto de una aplicación Rails normal y básicamente lo es, sólo que está diseñada para ser montada sobre otra aplicación por lo que hay algunas diferencias. Hay varios directorios con su propio espacio de nombres, por ejemplo el fichero application_controller
vive dentro de /app/controllers/uhoh
y lo mismo sucede con los ficheros debajo de assets
, helpers
y views
Esto hace que el código del engine quede limpiamente separado del resto de la aplicación dentro de la que vivirá. Como ya existe un directorio en assets
esto quiere decir que ya no tenemos que preocuparnos de copiar los recursos estáticos al directorio /public
cuando montamos el engine dentro de la aplicación.
Los recursos estáticos también tienen su propio espacio de nombres así que cuando los enlacemos tendremos que hacerlo a través de dicho directorio. Podemos ver esto también en la carpeta de layouts, aunque parece que todavía queda algún error en la RC5 porque tenemos dos application.html.erb
. Podemos borrar con tranquilidad el que está fuera de la carpeta uhoh
. Si abrimos el otro veremos que las referencias a los recursos llevan todas el directorio extra uhoh
. Siempre que enlacemos a imágenes u otros recursos tenemos que hacerlo a través de dicho espacio de nombres.
<!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>
A continuación veremos uno de los ficheros clave llamado engine.rb
que se encuentra en la carpeta /lib/uhoh
.
module Uhoh class Engine < Rails::Engine isolate_namespace Uhoh end end
Se trata de una clase que hereda de Rails::Engine
y es donde ocurre toda la configuración personalizada. Ya existe una llamada a isolate_namespace
en esta clase, lo que significa que el engine será considerado como su propia unidad aislada y no tendrá que preocuparse de la aplicación en la que se encuentre montado.
La parte final del engine es el directorio /test
. Bajo /test/dummy
hay una aplicación Rails que está ahí para que veamos cómo funciona nuestro engine cuando se monta sobre otra aplicación. El directorio config/
de dicha aplicación contiene un fichero routes.rb
, con la llamada a mount
en la que se pasa la clase principal del engine asignándola a una ruta.
Rails.application.routes.draw do mount Uhoh::Engine => "/uhoh" end
Esto es exactamente lo que cualquiera que quiera instalar el engine en una aplicación tendrá que hacer, montarla en la ruta que escoja. Se trata de una aplicación Rack por lo que si una petición llega a /uhoh
será nuestra clase Engine
quien la reciba. No es mala idea añadir esta línea a las instrucciones de instlación en el fichero README de la clase para que los usuarios sepan cómo montarla en el fichero de rutas de su aplicación.
La aplicación de pruebas sirve también para probar manualmente el engine, aunque se encuentre en el directorio test
. Podemos ejecutar la aplicación de prueba ejecutando rails s
en el directorio de nuestro engine. Y si visitamos http://localhost:3000/uhoh/
iremos al engine porque hemos cargado la URL en la que se encuentra montado. Sin embargo aparecerá un error al cargar dicha página porque no hemos escrito todavía ningún controlador.
A continuación crearemos un controlador failures
. Podemos usar los generadores de Rails igual que en una aplicación normal. Aunque estamos en un engine no tenemos por qué explicitar el espacio de nombres ya que ésto se gestiona automáticamente.
$ rails g controller failures index
Esta orden genera los mismos archivos que con un controlador normal, excepto que todo se coloca automáticamente en el directorio correspondiente al espacio de nombres adecuado. A continuación modificaremos las rutas del engine para tener una ruta root
a la acción index
del controlador. Esto se hace en el fichero /config/routes
del engine.
Uhoh::Engine.routes.draw do root :to => "failures#index" end
Si visitamos http://localhost:3000/uhoh/
veremos la vista de la acción.
En esta página queremos mostrar un listado con las excepciones que han ocurrido. Necesitamos un modelo para guardar esta información, por lo que crearemos el modelo Failure
que sólo tendrá un campo para el mensaje de la excepción.
$ rails g model failure message:text
Con este modelo ya creado, ¿cómo ejecutamos las migraciones? Dentro del propio engine podemos ejecutar normalmente rake db:migrate
y todo funcionará como sería de esperar. Pero cuando el engine se encuentre montado sobre otra aplicación esto no funcionará porque la tarea rake
no cogerá las migraciones que se encuentren dentro de los engines. Tendremos que especificar a los usuarios de nuestro engine que deben ejecutar la tarea rake uhoh:install:migrations
, que copiará las migraciones del engine junto con las de la aplicación de forma que se ejecuten cuando se lanza rake db:migrate
como es habitual. Es buena idea incluir esta información en las instrucciones de instalación.
La consola de Rails también funciona como cabría esperar, por lo que la utilizaremos para crear un Failure
de ejemplo.
Uhoh::Failure.create!(:message => "hello world!")
Obsérvese que tenemos que incluir el espacio de nombre siempre que hagamos referencia a una de las clases. Ahora que tenemos un registro Failure
lo mostraremos en la acción index
de nuestro failuresController
.
module Uhoh class FailuresController < ApplicationController def index @failures = Failure.all end end end
Al contrario de cuando estamos en la consola no tenemos que especificar un espacio de nombres, porque ya estamos dentro del módulo Uhoh
. En la vista escribiremos el código necesario para iterar sobre todos los errores y mostrarlos en una lista.
<h1>Failures</h1> <ul> <% for failure in @failures %> <li><%= failure.message %></li> <% end %> </ul>
Cuando recarguemos la página veremos el mensaje Failure
que acabamos de añadir.
Captura de excepciones
Ya tenemos una manera de guardar los errores, ahora sólo tenemos que usarla cada vez que ocurra una excepción en la aplicación sobre la que hayamos montado nuestro engine. Para probar esto tenemos que poder simular una excepción en la aplicación de prueba, para lo que nos moveremos al directorio de la aplicación de prueba y generaremos un controlador llamado simulate
con una acción failure
.
$ rails g controller simulate failure
En la acción elevaremos una excepción.
class SimulateController < ApplicationController def failure raise "Simulating an exception" end end
Si visitamos con el navegador dicha acción veremos, como era de esperar, la excepción.
Tenemos que cambiar el engine de forma que esté pendiente de dicha excepción y cree en ese momento un nuevo registro Failure
. La solución que proponemos no es especialmente eficiente pero al menos es sencilla y servirá como ejemplo. Empezaremos creando un inicializador en el engine, aunque no exista un directorio initializers
en el directorio config
de nuestro engine lo podemos crear. En este directorio pondremos un fichero llamado 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
En este fichero nos suscribimos a una notificación (vimos las notificaciones en el episodio 249 [verlo, leerlo]). Dicha notificación nos indica que se ha terminado de procesar una acción. En este momento es cuando podemos comprobar si el atributo payload
contiene una excepción en cuyo caso sabemos que se ha elevado una excepción y podemos guardar el mensaje en su correspondiente Failure
.
Para probarlo tenemos que reiniciar el servidor. Podemos visitar http://localhost:3000/simulate/failure
para que se lance la excepción. Si luego vamos a http://localhost:3000/uhoh
encontraremos que dicha excepción ha quedado registrada.
Gestión de URLs en los engines.
Los helpers de URL que se utilicen dentro de un engine generarán URLs para dicho engine. Por ejemplo si se añade un enlace a la URL raíz desde nuestra vista index
, el enlace apuntará a la URL del engine, no a la de la aplicación en la que se ha montado.
<p><%= link_to "Failures", root_url %></p>
El enlace apunta a http://localhost:3000/uhoh
que es la URL raíz del engine. En este caso se trata de la misma página porque la URL raíz se define en las rutas del engine como la acción index
de FailuresController
. Podemos crear enlaces a la aplicación propiamente dicha utilizando el prefijo main_app
de la siguiente manera:
<p><%= link_to "Failures", root_url %></p> <p><%= link_to "Simulate Failure", main_app.simulate_failure_path %></p>
Con esto tenemos un enlace a la página de simulación de excepciones, que se encuentra en http://localhost:3000/simulate/failure
.
¿Y si lo que queremos es justo lo contrario, tener un enlace al engine desde la aplicación principal? Lo primero que tenemos que hacer es cambiar la línea en el fichero de rutas donde montamos el engine y darle un nombre utilizando la opción :as
.
Rails.application.routes.draw do get "simulate/failure" mount Uhoh::Engine => "/uhoh", :as => "uhoh_engine" end
Tras esto podemos acceder a los helpers de URL del engine como métodos de uhoh_engine
. Para demostrar esto vamos a cambiar temporalmente nuestra acción que genera excepciones para que en lugar de elevar la excepción redirija a la URL raíz del engine.
class SimulateController < ApplicationController def failure redirect_to uhoh_engine.root_url end end
Si ahora visitamos http://localhost:3000/simulate/failure
seremos redirigidos a http://localhost:3000/uhoh
porque estamos usando el helper de dicho engine para generar la URL. Esta es otra funcionalidad que cabe mencionar en el fichero README
de nuestro engine.
Con todo esto ya tenemos la funcionalidad de nuestro engine prácticamente completa, pero la página que muestra los errores tiene un aspecto muy sobrio, por lo que le pondremos varios adornos. Primero añadiremos una imagen, que pondremos en el directorio /app/assets/images/uhoh
, y para incluirla en la página podemos usar image_tag
igual que con cualquier otra imagen.
<%= 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>
Incluyamos también algo de CSS. SASS y CoffeeScript no pueden usarse por defecto en los engine pero se pueden añadir como dependencias. Si añadimos CSS al fichero failures.css
éste se incluirá automáticamente.
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; }
Lo mismo con el JavaScript. El código que incluyamos en failures.js
se incluirá automáticamente.
$(function() { $("li").click(function() { $(this).slideUp(); }); });
Si ahora recargamos la página tendrá mucho mejor aspecto, y podemos ocultar la excepción haciendo clic en ella, con lo que sabemos que el JavaScript ha sido incorporado correctamente.
Con esto terminamos este episodio. La posibilidad de montar engines es una gran funcionalidad de Rails 3.1, que merece la pena estudiar.