1 ¿Qué son los Motores?
Los motores pueden considerarse aplicaciones en miniatura que proporcionan funcionalidad a sus aplicaciones anfitrionas. Una aplicación de Rails es en realidad solo un motor "supercargado", con la clase Rails::Application
heredando mucho de su comportamiento de Rails::Engine
.
Por lo tanto, los motores y las aplicaciones pueden considerarse casi lo mismo, solo con diferencias sutiles, como verás a lo largo de esta guía. Los motores y las aplicaciones también comparten una estructura común.
Los motores también están estrechamente relacionados con los plugins. Ambos comparten una estructura de directorio lib
común y se generan utilizando el generador rails plugin new
. La diferencia es que un motor se considera un "plugin completo" por Rails (como lo indica la opción --full
que se pasa al comando generador). En realidad, usaremos la opción --mountable
aquí, que incluye todas las características de --full
, y algo más. Esta guía se referirá a estos "plugins completos" simplemente como "motores" a lo largo de la guía. Un motor puede ser un plugin, y un plugin puede ser un motor.
El motor que se creará en esta guía se llamará "blorgh". Este motor proporcionará funcionalidad de blogging a sus aplicaciones anfitrionas, permitiendo que se creen nuevos artículos y comentarios. Al comienzo de esta guía, trabajarás únicamente dentro del propio motor, pero en secciones posteriores verás cómo conectarlo a una aplicación.
Los motores también pueden estar aislados de sus aplicaciones anfitrionas. Esto significa que una aplicación puede tener una ruta proporcionada por un helper de enrutamiento como articles_path
y usar un motor que también proporciona una ruta llamada articles_path
, y los dos no entrarían en conflicto. Junto con esto, los controladores, modelos y nombres de tablas también están en un espacio de nombres. Verás cómo hacer esto más adelante en esta guía.
Es importante tener en cuenta en todo momento que la aplicación debe siempre tener prioridad sobre sus motores. Una aplicación es el objeto que tiene la última palabra en lo que sucede en su entorno. El motor solo debe estar mejorándolo, en lugar de cambiarlo drásticamente.
Para ver demostraciones de otros motores, revisa Devise, un motor que proporciona autenticación para sus aplicaciones principales, o Thredded, un motor que proporciona funcionalidad de foro. También está Spree que proporciona una plataforma de comercio electrónico, y Refinery CMS, un motor CMS.
Finalmente, los motores no habrían sido posibles sin el trabajo de James Adam, Piotr Sarnacki, el Equipo Central de Rails y varias otras personas. Si alguna vez los encuentras, ¡no olvides decir gracias!
2 Generando un Motor
Para generar un motor, necesitarás ejecutar el generador de plugins y pasarle opciones según sea apropiado para la necesidad. Para el ejemplo "blorgh", necesitarás crear un motor "montable", ejecutando este comando en una terminal:
$ rails plugin new blorgh --mountable
La lista completa de opciones para el generador de plugins puede verse escribiendo:
$ rails plugin --help
La opción --mountable
le dice al generador que deseas crear un motor "montable" y aislado por espacio de nombres. Este generador proporcionará la misma estructura esqueleto que lo haría la opción --full
. La opción --full
le dice al generador que deseas crear un motor, incluyendo una estructura esqueleto que proporciona lo siguiente:
- Un árbol de directorios
app
Un archivo
config/routes.rb
:Rails.application.routes.draw do end
Un archivo en
lib/blorgh/engine.rb
, que es idéntico en función a un archivoconfig/application.rb
estándar de Rails:module Blorgh class Engine < ::Rails::Engine end end
La opción --mountable
añadirá a la opción --full
:
- Archivos de manifiesto de activos (
blorgh_manifest.js
yapplication.css
) - Un stub de
ApplicationController
con espacio de nombres - Un stub de
ApplicationHelper
con espacio de nombres - Una plantilla de vista de diseño para el motor
Aislamiento de espacio de nombres a
config/routes.rb
:Blorgh::Engine.routes.draw do end
Aislamiento de espacio de nombres a
lib/blorgh/engine.rb
:module Blorgh class Engine < ::Rails::Engine isolate_namespace Blorgh end end
Además, la opción --mountable
le indica al generador que monte el motor dentro de la aplicación de prueba dummy ubicada en test/dummy
añadiendo lo siguiente al archivo de rutas de la aplicación dummy en test/dummy/config/routes.rb
:
mount Blorgh::Engine => "/blorgh"
2.1 Dentro de un Motor
2.1.1 Archivos Críticos
En la raíz del directorio de este nuevo motor vive un archivo blorgh.gemspec
. Cuando incluyas el motor en una aplicación más adelante, lo harás con esta línea en el Gemfile
de la aplicación de Rails:
gem "blorgh", path: "engines/blorgh"
No olvides ejecutar bundle install
como de costumbre. Al especificarlo como un gem dentro del Gemfile
, Bundler lo cargará como tal, analizando este archivo blorgh.gemspec
y requiriendo un archivo dentro del directorio lib
llamado lib/blorgh.rb
. Este archivo requiere el archivo blorgh/engine.rb
(ubicado en lib/blorgh/engine.rb
) y define un módulo base llamado Blorgh
.
require "blorgh/engine"
module Blorgh
end
CONSEJO: Algunos motores eligen usar este archivo para poner opciones de configuración globales para su motor. Es una idea relativamente buena, así que si deseas ofrecer opciones de configuración, el archivo donde se define el module
de tu motor es perfecto para eso. Coloca los métodos dentro del módulo y estarás listo.
Dentro de lib/blorgh/engine.rb
está la clase base para el motor:
module Blorgh
class Engine < ::Rails::Engine
isolate_namespace Blorgh
end
end
Al heredar de la clase Rails::Engine
, este gem notifica a Rails que hay un motor en la ruta especificada, y montará correctamente el motor dentro de la aplicación, realizando tareas como agregar el directorio app
del motor a la ruta de carga para modelos, mailers, controladores y vistas.
El método isolate_namespace
aquí merece una mención especial. Esta llamada es responsable de aislar los controladores, modelos, rutas y otras cosas en su propio espacio de nombres, lejos de componentes similares dentro de la aplicación. Sin esto, existe la posibilidad de que los componentes del motor puedan "filtrarse" en la aplicación, causando interrupciones no deseadas, o que componentes importantes del motor puedan ser sobrescritos por cosas con nombres similares dentro de la aplicación. Uno de los ejemplos de tales conflictos son los helpers. Sin llamar a isolate_namespace
, los helpers del motor se incluirían en los controladores de una aplicación.
NOTA: Se recomienda altamente que la línea isolate_namespace
se deje dentro de la definición de la clase Engine
. Sin ella, las clases generadas en un motor pueden entrar en conflicto con una aplicación.
Lo que significa este aislamiento del espacio de nombres es que un modelo generado por una llamada a bin/rails generate model
, como bin/rails generate model article
, no se llamará Article
, sino que estará en un espacio de nombres y se llamará Blorgh::Article
. Además, la tabla para el modelo está en un espacio de nombres, convirtiéndose en blorgh_articles
, en lugar de simplemente articles
. Similar al espacio de nombres del modelo, un controlador llamado ArticlesController
se convierte en Blorgh::ArticlesController
y las vistas para ese controlador no estarán en app/views/articles
, sino en app/views/blorgh/articles
. Los mailers, trabajos y helpers también están en un espacio de nombres.
Finalmente, las rutas también estarán aisladas dentro del motor. Esta es una de las partes más importantes sobre el espacio de nombres, y se discute más adelante en la sección Rutas de esta guía.
2.1.2 Directorio app
Dentro del directorio app
están los directorios estándar assets
, controllers
, helpers
, jobs
, mailers
, models
y views
con los que deberías estar familiarizado por una aplicación. Veremos más sobre modelos en una sección futura, cuando estemos escribiendo el motor.
Dentro del directorio app/assets
, están los directorios images
y stylesheets
que, nuevamente, deberías estar familiarizado debido a su similitud con una aplicación. Una diferencia aquí, sin embargo, es que cada directorio contiene un subdirectorio con el nombre del motor. Debido a que este motor va a estar en un espacio de nombres, sus activos también deberían estarlo.
Dentro del directorio app/controllers
hay un directorio blorgh
que contiene un archivo llamado application_controller.rb
. Este archivo proporcionará cualquier funcionalidad común para los controladores del motor. El directorio blorgh
es donde irán los otros controladores para el motor. Al colocarlos dentro de este directorio con espacio de nombres, evitas que entren en conflicto con controladores con nombres idénticos dentro de otros motores o incluso dentro de la aplicación.
NOTA: La clase ApplicationController
dentro de un motor se nombra igual que una aplicación de Rails para facilitar la conversión de tus aplicaciones en motores.
Al igual que para app/controllers
, encontrarás un subdirectorio blorgh
bajo los directorios app/helpers
, app/jobs
, app/mailers
y app/models
que contiene el archivo application_*.rb
asociado para reunir funcionalidades comunes. Al colocar tus archivos bajo este subdirectorio y poner tus objetos en un espacio de nombres, evitas que entren en conflicto con elementos con nombres idénticos dentro de otros motores o incluso dentro de la aplicación.
Por último, el directorio app/views
contiene una carpeta layouts
, que contiene un archivo en blorgh/application.html.erb
. Este archivo te permite especificar un diseño para el motor. Si este motor se va a usar como un motor independiente, entonces agregarías cualquier personalización a su diseño en este archivo, en lugar del archivo app/views/layouts/application.html.erb
de la aplicación.
Si no deseas imponer un diseño a los usuarios del motor, entonces puedes eliminar este archivo y hacer referencia a un diseño diferente en los controladores de tu motor.
2.1.3 Directorio bin
Este directorio contiene un archivo, bin/rails
, que te permite usar los subcomandos y generadores de rails
tal como lo harías dentro de una aplicación. Esto significa que podrás generar nuevos controladores y modelos para este motor muy fácilmente ejecutando comandos como este:
$ bin/rails generate model
Ten en cuenta, por supuesto, que cualquier cosa generada con estos comandos dentro de un motor que tenga isolate_namespace
en la clase Engine
estará en un espacio de nombres.
2.1.4 Directorio test
El directorio test
es donde irán las pruebas para el motor. Para probar el motor, hay una versión reducida de una aplicación de Rails incrustada dentro de él en test/dummy
. Esta aplicación montará el motor en el archivo test/dummy/config/routes.rb
:
Rails.application.routes.draw do
mount Blorgh::Engine => "/blorgh"
end
Esta línea monta el motor en la ruta /blorgh
, lo que lo hará accesible a través de la aplicación solo en esa ruta.
Dentro del directorio de prueba está el directorio test/integration
, donde deben colocarse las pruebas de integración para el motor. También se pueden crear otros directorios en el directorio test
. Por ejemplo, es posible que desees crear un directorio test/models
para tus pruebas de modelo.
3 Proporcionando Funcionalidad de Motor
El motor que cubre esta guía proporciona funcionalidad para enviar artículos y comentar, y sigue un hilo similar a la Guía de Inicio Rápido, con algunos giros nuevos.
NOTA: Para esta sección, asegúrate de ejecutar los comandos en la raíz del directorio del motor blorgh
.
3.1 Generando un Recurso de Artículo
Lo primero que se debe generar para un motor de blog es el modelo Article
y el controlador relacionado. Para generar esto rápidamente, puedes usar el generador de scaffold de Rails.
$ bin/rails generate scaffold article title:string text:text
Este comando mostrará esta información:
invoke active_record
create db/migrate/[timestamp]_create_blorgh_articles.rb
create app/models/blorgh/article.rb
invoke test_unit
create test/models/blorgh/article_test.rb
create test/fixtures/blorgh/articles.yml
invoke resource_route
route resources :articles
invoke scaffold_controller
create app/controllers/blorgh/articles_controller.rb
invoke erb
create app/views/blorgh/articles
create app/views/blorgh/articles/index.html.erb
create app/views/blorgh/articles/edit.html.erb
create app/views/blorgh/articles/show.html.erb
create app/views/blorgh/articles/new.html.erb
create app/views/blorgh/articles/_form.html.erb
create app/views/blorgh/articles/_article.html.erb
invoke resource_route
invoke test_unit
create test/controllers/blorgh/articles_controller_test.rb
create test/system/blorgh/articles_test.rb
invoke helper
create app/helpers/blorgh/articles_helper.rb
invoke test_unit
Lo primero que hace el generador de scaffold es invocar el generador active_record
, que genera una migración y un modelo para el recurso. Observa aquí, sin embargo, que la migración se llama create_blorgh_articles
en lugar del habitual create_articles
. Esto se debe al método isolate_namespace
llamado en la definición de la clase Blorgh::Engine
. El modelo aquí también está en un espacio de nombres, colocándose en app/models/blorgh/article.rb
en lugar de app/models/article.rb
debido a la llamada a isolate_namespace
dentro de la clase Engine
.
A continuación, el generador test_unit
se invoca para este modelo, generando una prueba de modelo en test/models/blorgh/article_test.rb
(en lugar de test/models/article_test.rb
) y un fixture en test/fixtures/blorgh/articles.yml
(en lugar de test/fixtures/articles.yml
).
Después de eso, se inserta una línea para el recurso en el archivo config/routes.rb
del motor. Esta línea es simplemente resources :articles
, convirtiendo el archivo config/routes.rb
del motor en esto:
Blorgh::Engine.routes.draw do
resources :articles
end
Observa aquí que las rutas se dibujan sobre el objeto Blorgh::Engine
en lugar de la clase YourApp::Application
. Esto es para que las rutas del motor estén confinadas al propio motor y puedan montarse en un punto específico como se muestra en la sección Directorio de Prueba. También hace que las rutas del motor estén aisladas de aquellas rutas que están dentro de la aplicación. La sección Rutas de esta guía lo describe en detalle.
A continuación, el generador scaffold_controller
se invoca, generando un controlador llamado Blorgh::ArticlesController
(en app/controllers/blorgh/articles_controller.rb
) y sus vistas relacionadas en app/views/blorgh/articles
. Este generador también genera pruebas para el controlador (test/controllers/blorgh/articles_controller_test.rb
y test/system/blorgh/articles_test.rb
) y un helper (app/helpers/blorgh/articles_helper.rb
).
Todo lo que este generador ha creado está ordenadamente en un espacio de nombres. La clase del controlador está definida dentro del módulo Blorgh
:
module Blorgh
class ArticlesController < ApplicationController
# ...
end
end
NOTA: La clase ArticlesController
hereda de Blorgh::ApplicationController
, no del ApplicationController
de la aplicación.
El helper dentro de app/helpers/blorgh/articles_helper.rb
también está en un espacio de nombres:
module Blorgh
module ArticlesHelper
# ...
end
end
Esto ayuda a prevenir conflictos con cualquier otro motor o aplicación que pueda tener un recurso de artículo también.
Puedes ver lo que el motor tiene hasta ahora ejecutando bin/rails db:migrate
en la raíz de nuestro motor para ejecutar la migración generada por el generador de scaffold, y luego ejecutando bin/rails server
en test/dummy
. Cuando abras http://localhost:3000/blorgh/articles
verás el scaffold predeterminado que se ha generado. ¡Haz clic! Acabas de generar las primeras funciones de tu primer motor.
Si prefieres jugar en la consola, bin/rails console
también funcionará igual que una aplicación de Rails. Recuerda: el modelo Article
está en un espacio de nombres, así que para referenciarlo debes llamarlo como Blorgh::Article
.
irb> Blorgh::Article.find(1)
=> #<Blorgh::Article id: 1 ...>
Una última cosa es que el recurso articles
para este motor debería ser la raíz del motor. Siempre que alguien vaya a la ruta raíz donde se monta el motor, deberían ver una lista de artículos. Esto se puede hacer si se inserta esta línea en el archivo config/routes.rb
dentro del motor:
root to: "articles#index"
Ahora la gente solo necesitará ir a la raíz del motor para ver todos los artículos, en lugar de visitar /articles
. Esto significa que en lugar de http://localhost:3000/blorgh/articles
, solo necesitas ir a http://localhost:3000/blorgh
ahora.
3.2 Generando un Recurso de Comentarios
Ahora que el motor puede crear nuevos artículos, solo tiene sentido agregar funcionalidad de comentarios también. Para hacer esto, necesitarás generar un modelo de comentario, un controlador de comentario y luego modificar el scaffold de artículos para mostrar comentarios y permitir que la gente cree nuevos.
Desde la raíz del motor, ejecuta el generador de modelos. Dile que genere un modelo Comment
, con la tabla relacionada teniendo dos columnas: una columna de article_id
integer y una columna de text
text.
$ bin/rails generate model Comment article_id:integer text:text
Esto mostrará lo siguiente:
invoke active_record
create db/migrate/[timestamp]_create_blorgh_comments.rb
create app/models/blorgh/comment.rb
invoke test_unit
create test/models/blorgh/comment_test.rb
create test/fixtures/blorgh/comments.yml
Esta llamada al generador generará solo los archivos de modelo necesarios, poniendo los archivos bajo un directorio blorgh
y creando una clase de modelo llamada Blorgh::Comment
. Ahora ejecuta la migración para crear nuestra tabla blorgh_comments:
$ bin/rails db:migrate
Para mostrar los comentarios en un artículo, edita app/views/blorgh/articles/show.html.erb
y agrega esta línea antes del enlace "Edit":
<h3>Comments</h3>
<%= render @article.comments %>
Esta línea requerirá que haya una asociación has_many
para comentarios definida en el modelo Blorgh::Article
, que no existe en este momento. Para definir una, abre app/models/blorgh/article.rb
y agrega esta línea en el modelo:
has_many :comments
Convirtiendo el modelo en esto:
module Blorgh
class Article < ApplicationRecord
has_many :comments
end
end
NOTA: Debido a que el has_many
está definido dentro de una clase que está dentro del módulo Blorgh
, Rails sabrá que deseas usar el modelo Blorgh::Comment
para estos objetos, por lo que no hay necesidad de especificar eso usando la opción :class_name
aquí.
A continuación, debe haber un formulario para que se puedan crear comentarios en un artículo. Para agregar esto, pon esta línea debajo de la llamada a render @article.comments
en app/views/blorgh/articles/show.html.erb
:
<%= render "blorgh/comments/form" %>
A continuación, el parcial que esta línea renderizará necesita existir. Crea un nuevo directorio en app/views/blorgh/comments
y en él un nuevo archivo llamado _form.html.erb
que tenga este contenido para crear el parcial requerido:
<h3>New comment</h3>
<%= form_with model: [@article, @article.comments.build] do |form| %>
<p>
<%= form.label :text %><br>
<%= form.text_area :text %>
</p>
<%= form.submit %>
<% end %>
Cuando se envíe este formulario, intentará realizar una solicitud POST
a una ruta de /articles/:article_id/comments
dentro del motor. Esta ruta no existe en este momento, pero se puede crear cambiando la línea resources :articles
dentro de config/routes.rb
en estas líneas:
resources :articles do
resources :comments
end
Esto crea una ruta anidada para los comentarios, que es lo que requiere el formulario.
La ruta ahora existe, pero el controlador al que va esta ruta no. Para crearlo, ejecuta este comando desde la raíz del motor:
$ bin/rails generate controller comments
Esto generará las siguientes cosas:
create app/controllers/blorgh/comments_controller.rb
invoke erb
exist app/views/blorgh/comments
invoke test_unit
create test/controllers/blorgh/comments_controller_test.rb
invoke helper
create app/helpers/blorgh/comments_helper.rb
invoke test_unit
El formulario estará haciendo una solicitud POST
a /articles/:article_id/comments
, que corresponderá con la acción create
en Blorgh::CommentsController
. Esta acción necesita ser creada, lo que se puede hacer poniendo las siguientes líneas dentro de la definición de clase en app/controllers/blorgh/comments_controller.rb
:
def create
@article = Article.find(params[:article_id])
@comment = @article.comments.create(comment_params)
flash[:notice] = "Comment has been created!"
redirect_to articles_path
end
private
def comment_params
params.require(:comment).permit(:text)
end
Este es el paso final requerido para que el nuevo formulario de comentarios funcione. Sin embargo, mostrar los comentarios no está del todo correcto todavía. Si fueras a crear un comentario ahora mismo, verías este error:
Missing partial blorgh/comments/_comment with {:handlers=>[:erb, :builder],
:formats=>[:html], :locale=>[:en, :en]}. Searched in: *
"/Users/ryan/Sites/side_projects/blorgh/test/dummy/app/views" *
"/Users/ryan/Sites/side_projects/blorgh/app/views"
El motor no puede encontrar el parcial requerido para renderizar los comentarios. Rails busca primero en el directorio app/views
de la aplicación (test/dummy
) y luego en el directorio app/views
del motor. Cuando no puede encontrarlo, lanzará este error. El motor sabe buscar blorgh/comments/_comment
porque el objeto de modelo que está recibiendo es de la clase Blorgh::Comment
.
Este parcial será responsable de renderizar solo el texto del comentario, por ahora. Crea un nuevo archivo en app/views/blorgh/comments/_comment.html.erb
y pon esta línea dentro:
<%= comment_counter + 1 %>. <%= comment.text %>
La variable local comment_counter
nos la proporciona la llamada <%= render @article.comments %>
, que la definirá automáticamente e incrementará el contador a medida que itera a través de cada comentario. Se usa en este ejemplo para mostrar un pequeño número junto a cada comentario cuando se crea.
Eso completa la función de comentarios del motor de blogging. Ahora es el momento de usarlo dentro de una aplicación.
4 Conectando a una Aplicación
Usar un motor dentro de una aplicación es muy fácil. Esta sección cubre cómo montar el motor en una aplicación y la configuración inicial requerida, así como vincular el motor a una clase User
proporcionada por la aplicación para proporcionar propiedad para artículos y comentarios dentro del motor.
4.1 Montando el Motor
Primero, el motor necesita ser especificado dentro del Gemfile
de la aplicación. Si no hay una aplicación a mano para probar esto, genera una usando el comando rails new
fuera del directorio del motor así:
$ rails new unicorn
Por lo general, especificarías el motor dentro del Gemfile
como un gem normal y corriente.
gem "devise"
Sin embargo, debido a que estás desarrollando el motor blorgh
en tu máquina local, necesitarás especificar la opción :path
en tu Gemfile
:
gem "blorgh", path: "engines/blorgh"
Luego ejecuta bundle
para instalar el gem.
Como se describió anteriormente, al colocar el gem en el Gemfile
se cargará cuando Rails se cargue. Primero requerirá lib/blorgh.rb
del motor, luego lib/blorgh/engine.rb
, que es el archivo que define las piezas principales de funcionalidad para el motor.
Para hacer que la funcionalidad del motor sea accesible desde dentro de una aplicación, necesita ser montado en el archivo config/routes.rb
de esa aplicación:
mount Blorgh::Engine, at: "/blog"
Esta línea montará el motor en /blog
en la aplicación. Haciéndolo accesible en http://localhost:3000/blog
cuando la aplicación se ejecuta con bin/rails server
.
NOTA: Otros motores, como Devise, manejan esto de manera un poco diferente al hacerte especificar helpers personalizados (como devise_for
) en las rutas. Estos helpers hacen exactamente lo mismo, montando piezas de la funcionalidad del motor en una ruta predefinida que puede ser personalizable.
4.2 Configuración del Motor
El motor contiene migraciones para las tablas blorgh_articles
y blorgh_comments
que necesitan ser creadas en la base de datos de la aplicación para que los modelos del motor puedan consultarlas correctamente. Para copiar estas migraciones en la aplicación ejecuta el siguiente comando desde la raíz de la aplicación:
$ bin/rails blorgh:install:migrations
Si tienes múltiples motores que necesitan migraciones copiadas, usa railties:install:migrations
en su lugar:
$ bin/rails railties:install:migrations
Puedes especificar una ruta personalizada en el motor de origen para las migraciones especificando MIGRATIONS_PATH.
$ bin/rails railties:install:migrations MIGRATIONS_PATH=db_blourgh
Si tienes múltiples bases de datos también puedes especificar la base de datos de destino especificando DATABASE.
$ bin/rails railties:install:migrations DATABASE=animals
Este comando, cuando se ejecuta por primera vez, copiará todas las migraciones del motor. Cuando se ejecute la próxima vez, solo copiará las migraciones que no se hayan copiado ya. La primera ejecución de este comando mostrará algo como esto:
Copied migration [timestamp_1]_create_blorgh_articles.blorgh.rb from blorgh
Copied migration [timestamp_2]_create_blorgh_comments.blorgh.rb from blorgh
La primera marca de tiempo ([timestamp_1]
) será la hora actual, y la segunda marca de tiempo ([timestamp_2]
) será la hora actual más un segundo. La razón de esto es para que las migraciones para el motor se ejecuten después de cualquier migración existente en la aplicación.
Para ejecutar estas migraciones dentro del contexto de la aplicación, simplemente ejecuta bin/rails db:migrate
. Al acceder al motor a través de http://localhost:3000/blog
, los artículos estarán vacíos. Esto se debe a que la tabla creada dentro de la aplicación es diferente de la creada dentro del motor. Adelante, juega con el motor recién montado. Encontrarás que es lo mismo que cuando era solo un motor.
Si deseas ejecutar migraciones solo desde un motor, puedes hacerlo especificando SCOPE
:
$ bin/rails db:migrate SCOPE=blorgh
Esto puede ser útil si deseas revertir las migraciones del motor antes de eliminarlo. Para revertir todas las migraciones del motor blorgh puedes ejecutar un código como:
$ bin/rails db:migrate SCOPE=blorgh VERSION=0
4.3 Usando una Clase Proporcionada por la Aplicación
4.3.1 Usando un Modelo Proporcionado por la Aplicación
Cuando se crea un motor, puede querer usar clases específicas de una aplicación para proporcionar enlaces entre las piezas del motor y las piezas de la aplicación. En el caso del motor blorgh
, hacer que los artículos y los comentarios tengan autores tendría mucho sentido.
Una aplicación típica podría tener una clase User
que se usaría para representar autores para un artículo o un comentario. Pero podría haber un caso en el que la aplicación llame a esta clase algo diferente, como Person
. Por esta razón, el motor no debería codificar específicamente las asociaciones para una clase User
.
Para mantenerlo simple en este caso, la aplicación tendrá una clase llamada User
que representa a los usuarios de la aplicación (entraremos en hacer esto configurable más adelante). Puede generarse usando este comando dentro de la aplicación:
$ bin/rails generate model user name:string
El comando bin/rails db:migrate
necesita ejecutarse aquí para asegurar que nuestra aplicación tenga la tabla users
para uso futuro.
Además, para mantenerlo simple, el formulario de artículos tendrá un nuevo campo de texto llamado author_name
, donde los usuarios pueden elegir poner su nombre. El motor luego tomará este nombre y creará un nuevo objeto User
a partir de él o encontrará uno que ya tenga ese nombre. Luego, el motor asociará el artículo con el objeto User
encontrado o creado.
Primero, el campo de texto author_name
necesita ser agregado al parcial app/views/blorgh/articles/_form.html.erb
dentro del motor. Esto se puede agregar sobre el campo title
con este código:
<div class="field">
<%= form.label :author_name %><br>
<%= form.text_field :author_name %>
</div>
A continuación, necesitamos actualizar nuestro método Blorgh::ArticlesController#article_params
para permitir el nuevo parámetro del formulario:
def article_params
params.require(:article).permit(:title, :text, :author_name)
end
El modelo Blorgh::Article
debería tener entonces algún código para convertir el campo author_name
en un objeto User
real y asociarlo como el author
de ese artículo antes de que el artículo se guarde. También necesitará tener un attr_accessor
configurado para este campo, para que los métodos setter y getter estén definidos para él.
Para hacer todo esto, deberás agregar el attr_accessor
para author_name
, la asociación para el autor y la llamada before_validation
en app/models/blorgh/article.rb
. La asociación author
se codificará para la clase User
por el momento.
attr_accessor :author_name
belongs_to :author, class_name: "User"
before_validation :set_author
private
def set_author
self.author = User.find_or_create_by(name: author_name)
end
Al representar el objeto de la asociación author
con la clase User
, se establece un enlace entre el motor y la aplicación. Debe haber una forma de asociar los registros en la tabla blorgh_articles
con los registros en la tabla users
. Debido a que la asociación se llama author
, debería añadirse una columna author_id
a la tabla blorgh_articles
.
Para generar esta nueva columna, ejecuta este comando dentro del motor:
$ bin/rails generate migration add_author_id_to_blorgh_articles author_id:integer
NOTA: Debido al nombre de la migración y la especificación de la columna después de ella, Rails sabrá automáticamente que deseas agregar una columna a una tabla específica y escribirá eso en la migración por ti. No necesitas decirle más que esto.
Esta migración necesitará ejecutarse en la aplicación. Para hacerlo, primero debe copiarse usando este comando:
$ bin/rails blorgh:install:migrations
Observa que solo una migración se copió aquí. Esto se debe a que las dos primeras migraciones se copiaron la primera vez que se ejecutó este comando.
NOTE Migration [timestamp]_create_blorgh_articles.blorgh.rb from blorgh has been skipped. Migration with the same name already exists.
NOTE Migration [timestamp]_create_blorgh_comments.blorgh.rb from blorgh has been skipped. Migration with the same name already exists.
Copied migration [timestamp]_add_author_id_to_blorgh_articles.blorgh.rb from blorgh
Ejecuta la migración usando:
$ bin/rails db:migrate
Ahora con todas las piezas en su lugar, se llevará a cabo una acción que asociará un autor - representado por un registro en la tabla users
- con un artículo, representado por la tabla blorgh_articles
del motor.
Finalmente, el nombre del autor debería mostrarse en la página del artículo. Agrega este código sobre la salida "Title" dentro de app/views/blorgh/articles/_article.html.erb
:
<p>
<strong>Author:</strong>
<%= article.author.name %>
</p>
4.3.2 Usando un Controlador Proporcionado por la Aplicación
Debido a que los controladores de Rails generalmente comparten código para cosas como autenticación y acceso a variables de sesión, heredan de ApplicationController
por defecto. Sin embargo, los motores de Rails están diseñados para ejecutarse independientemente de la aplicación principal, por lo que cada motor obtiene un ApplicationController
con espacio de nombres. Este espacio de nombres previene colisiones de código, pero a menudo los controladores del motor necesitan acceder a métodos en el ApplicationController
de la aplicación principal. Una forma fácil de proporcionar este acceso es cambiar el ApplicationController
con espacio de nombres del motor para heredar del ApplicationController
de la aplicación principal. Para nuestro motor Blorgh esto se haría cambiando app/controllers/blorgh/application_controller.rb
para que luzca así:
module Blorgh
class ApplicationController < ::ApplicationController
end
end
Por defecto, los controladores del motor heredan de Blorgh::ApplicationController
. Así que, después de hacer este cambio, tendrán acceso al ApplicationController
de la aplicación principal, como si fueran parte de la aplicación principal.
Este cambio requiere que el motor se ejecute desde una aplicación de Rails que tenga un ApplicationController
.
4.4 Configurando un Motor
Esta sección cubre cómo hacer que la clase User
sea configurable, seguida de consejos generales de configuración para el motor.
4.4.1 Estableciendo Configuraciones en la Aplicación
El siguiente paso es hacer que la clase que representa a un User
en la aplicación sea personalizable para el motor. Esto se debe a que esa clase no siempre puede ser User
, como se explicó anteriormente. Para hacer que esta configuración sea personalizable, el motor tendrá una configuración llamada author_class
que se usará para especificar qué clase representa a los usuarios dentro de la aplicación.
Para definir esta configuración, debes usar un mattr_accessor
dentro del módulo Blorgh
para el motor. Agrega esta línea a lib/blorgh.rb
dentro del motor:
mattr_accessor :author_class
Este método funciona como sus hermanos, attr_accessor
y cattr_accessor
, pero proporciona un método setter y getter en el módulo con el nombre especificado. Para usarlo, debe referenciarse usando Blorgh.author_class
.
El siguiente paso es cambiar el modelo Blorgh::Article
a esta nueva configuración. Cambia la asociación belongs_to
dentro de este modelo (app/models/blorgh/article.rb
) a esto:
belongs_to :author, class_name: Blorgh.author_class
El método set_author
en el modelo Blorgh::Article
también debería usar esta clase:
self.author = Blorgh.author_class.constantize.find_or_create_by(name: author_name)
Para ahorrar tener que llamar a constantize
en el resultado de author_class
todo el tiempo, podrías simplemente sobrescribir el método getter author_class
dentro del módulo Blorgh
en el archivo lib/blorgh.rb
para siempre llamar a constantize
en el valor guardado antes de devolver el resultado:
def self.author_class
@@author_class.constantize
end
Esto luego convertiría el código anterior para set_author
en esto:
self.author = Blorgh.author_class.find_or_create_by(name: author_name)
Resultando en algo un poco más corto y más implícito en su comportamiento. El método author_class
siempre debería devolver un objeto Class
.
Dado que cambiamos el método author_class
para devolver una Class
en lugar de una String
, también debemos modificar nuestra definición belongs_to
en el modelo Blorgh::Article
:
belongs_to :author, class_name: Blorgh.author_class.to_s
Para establecer esta configuración dentro de la aplicación, se debe usar un inicializador. Al usar un inicializador, la configuración se establecerá antes de que la aplicación comience y llame a los modelos del motor, que pueden depender de que esta configuración exista.
Crea un nuevo inicializador en config/initializers/blorgh.rb
dentro de la aplicación donde está instalado el motor blorgh
y pon este contenido en él:
Blorgh.author_class = "User"
ADVERTENCIA: Es muy importante aquí usar la versión String
de la clase, en lugar de la clase en sí. Si usaras la clase, Rails intentaría cargar esa clase y luego referenciar la tabla relacionada. Esto podría llevar a problemas si la tabla no existiera ya. Por lo tanto, se debe usar una String
y luego convertirla en una clase usando constantize
en el motor más adelante.
Adelante, intenta crear un nuevo artículo. Verás que funciona exactamente de la misma manera que antes, excepto que esta vez el motor está usando la configuración en config/initializers/blorgh.rb
para saber qué clase es.
Ahora no hay dependencias estrictas sobre qué clase es, solo sobre cuál debe ser la API para la clase. El motor simplemente requiere que esta clase defina un método find_or_create_by
que devuelva un objeto de esa clase, para ser asociado con un artículo cuando se crea. Este objeto, por supuesto, debería tener algún tipo de identificador por el cual pueda ser referenciado.
4.4.2 Configuración General del Motor
Dentro de un motor, puede llegar un momento en el que desees usar cosas como inicializadores, internacionalización u otras opciones de configuración. La buena noticia es que estas cosas son completamente posibles, porque un motor de Rails comparte gran parte de la funcionalidad de una aplicación de Rails. De hecho, la funcionalidad de una aplicación de Rails es en realidad un superconjunto de lo que proporcionan los motores.
Si deseas usar un inicializador - código que debería ejecutarse antes de que se cargue el motor - el lugar para ello es la carpeta config/initializers
. La funcionalidad de este directorio se explica en la sección de Inicializadores de la guía de Configuración, y funciona exactamente de la misma manera que el directorio config/initializers
dentro de una aplicación. Lo mismo ocurre si deseas usar un inicializador estándar.
Para locales, simplemente coloca los archivos de locales en el directorio config/locales
, tal como lo harías en una aplicación.
5 Probando un Motor
Cuando se genera un motor, se crea una aplicación dummy más pequeña dentro de él en test/dummy
. Esta aplicación se usa como un punto de montaje para el motor, para hacer que probar el motor sea extremadamente simple. Puedes extender esta aplicación generando controladores, modelos o vistas desde dentro del directorio y luego usarlos para probar tu motor.
El directorio test
debe tratarse como un entorno de prueba típico de Rails, permitiendo pruebas unitarias, funcionales y de integración.
5.1 Pruebas Funcionales
Un asunto que vale la pena considerar al escribir pruebas funcionales es que las pruebas se ejecutarán en una aplicación - la aplicación test/dummy
- en lugar de tu motor. Esto se debe a la configuración del entorno de prueba; un motor necesita una aplicación como anfitrión para probar su funcionalidad principal, especialmente los controladores. Esto significa que si fueras a hacer un GET
típico a un controlador en una prueba funcional de un controlador como esta:
module Blorgh
class FooControllerTest < ActionDispatch::IntegrationTest
include Engine.routes.url_helpers
def test_index
get foos_url
# ...
end
end
end
Puede que no funcione correctamente. Esto se debe a que la aplicación no sabe cómo enrutar estas solicitudes al motor a menos que le digas explícitamente cómo. Para hacer esto, debes establecer la variable de instancia @routes
en el conjunto de rutas del motor en tu código de configuración:
module Blorgh
class FooControllerTest < ActionDispatch::IntegrationTest
include Engine.routes.url_helpers
setup do
@routes = Engine.routes
end
def test_index
get foos_url
# ...
end
end
end
Esto le dice a la aplicación que aún deseas realizar una solicitud GET
a la acción index
de este controlador, pero deseas usar la ruta del motor para llegar allí, en lugar de la de la aplicación.
Esto también asegura que los métodos helpers de URL del motor funcionen como se espera en tus pruebas.
6 Mejorando la Funcionalidad del Motor
Esta sección explica cómo agregar y/o sobrescribir la funcionalidad MVC del motor en la aplicación principal de Rails.
6.1 Sobrescribiendo Modelos y Controladores
Los modelos y controladores del motor pueden ser reabiertos por la aplicación principal para extenderlos o decorarlos.
Las sobrescrituras pueden organizarse en un directorio dedicado app/overrides
, ignorado por el autoloader, y precargado en un callback to_prepare
:
# config/application.rb
module MyApp
class Application < Rails::Application
# ...
overrides = "#{Rails.root}/app/overrides"
Rails.autoloaders.main.ignore(overrides)
config.to_prepare do
Dir.glob("#{overrides}/**/*_override.rb").sort.each do |override|
load override
end
end
end
end
6.1.1 Reabriendo Clases Existentes Usando class_eval
Por ejemplo, para sobrescribir el modelo del motor
# Blorgh/app/models/blorgh/article.rb
module Blorgh
class Article < ApplicationRecord
# ...
end
end
simplemente crea un archivo que reabra esa clase:
# MyApp/app/overrides/models/blorgh/article_override.rb
Blorgh::Article.class_eval do
# ...
end
Es muy importante que la sobrescritura reabra la clase o el módulo. Usar las palabras clave class
o module
las definiría si no estuvieran ya en memoria, lo cual sería incorrecto porque la definición vive en el motor. Usar class_eval
como se muestra arriba asegura que estás reabriendo.
6.1.2 Reabriendo Clases Existentes Usando ActiveSupport::Concern
Usar Class#class_eval
es genial para ajustes simples, pero para modificaciones de clase más complejas, podrías querer considerar usar ActiveSupport::Concern
. ActiveSupport::Concern gestiona el orden de carga de módulos y clases dependientes interrelacionados en tiempo de ejecución, permitiéndote modularizar significativamente tu código.
Agregando Article#time_since_created
y Sobrescribiendo Article#summary
:
# MyApp/app/models/blorgh/article.rb
class Blorgh::Article < ApplicationRecord
include Blorgh::Concerns::Models::Article
def time_since_created
Time.current - created_at
end
def summary
"#{title} - #{truncate(text)}"
end
end
# Blorgh/app/models/blorgh/article.rb
module Blorgh
class Article < ApplicationRecord
include Blorgh::Concerns::Models::Article
end
end
# Blorgh/lib/concerns/models/article.rb
module Blorgh::Concerns::Models::Article
extend ActiveSupport::Concern
# `included do` hace que el bloque se evalúe en el contexto
# en el que se incluye el módulo (es decir, Blorgh::Article),
# en lugar de en el módulo mismo.
included do
attr_accessor :author_name
belongs_to :author, class_name: "User"
before_validation :set_author
private
def set_author
self.author = User.find_or_create_by(name: author_name)
end
end
def summary
"#{title}"
end
module ClassMethods
def some_class_method
'some class method string'
end
end
end
6.2 Carga Automática y Motores
Por favor, consulta la guía Carga Automática y Recarga de Constantes para obtener más información sobre la carga automática y los motores.
6.3 Sobrescribiendo Vistas
Cuando Rails busca una vista para renderizar, primero buscará en el directorio app/views
de la aplicación. Si no puede encontrar la vista allí, verificará en los directorios app/views
de todos los motores que tengan este directorio.
Cuando se le pide a la aplicación que renderice la vista para la acción index de Blorgh::ArticlesController
, primero buscará en la ruta app/views/blorgh/articles/index.html.erb
dentro de la aplicación. Si no puede encontrarla, buscará dentro del motor.
Puedes sobrescribir esta vista en la aplicación simplemente creando un nuevo archivo en app/views/blorgh/articles/index.html.erb
. Luego puedes cambiar completamente lo que esta vista normalmente mostraría.
Prueba esto ahora creando un nuevo archivo en app/views/blorgh/articles/index.html.erb
y pon este contenido en él:
<h1>Articles</h1>
<%= link_to "New Article", new_article_path %>
<% @articles.each do |article| %>
<h2><%= article.title %></h2>
<small>By <%= article.author %></small>
<%= simple_format(article.text) %>
<hr>
<% end %>
6.4 Rutas
Las rutas dentro de un motor están aisladas de la aplicación por defecto. Esto se hace mediante la llamada isolate_namespace
dentro de la clase Engine
. Esto esencialmente significa que la aplicación y sus motores pueden tener rutas con nombres idénticos y no entrarán en conflicto.
Las rutas dentro de un motor se dibujan en la clase Engine
dentro de config/routes.rb
, así:
Blorgh::Engine.routes.draw do
resources :articles
end
Al tener rutas aisladas como esta, si deseas enlazar a un área de un motor desde dentro de una aplicación, necesitarás usar el método proxy de enrutamiento del motor. Las llamadas a métodos de enrutamiento normales como articles_path
pueden terminar yendo a ubicaciones no deseadas si tanto la aplicación como el motor tienen tal helper definido.
Por ejemplo, el siguiente ejemplo iría a articles_path
de la aplicación si esa plantilla se renderiza desde la aplicación, o a articles_path
del motor si se renderiza desde el motor:
<%= link_to "Blog articles", articles_path %>
Para hacer que esta ruta use siempre el método helper de enrutamiento articles_path
del motor, debemos llamar al método en el método proxy de enrutamiento que comparte el mismo nombre que el motor.
<%= link_to "Blog articles", blorgh.articles_path %>
Si deseas referenciar la aplicación dentro del motor de manera similar, usa el helper main_app
:
<%= link_to "Home", main_app.root_path %>
Si usaras esto dentro de un motor, siempre iría a la raíz de la aplicación. Si dejaras fuera el método "proxy de enrutamiento" main_app
, podría potencialmente ir a la raíz del motor o de la aplicación, dependiendo de dónde se llame.
Si una plantilla renderizada desde dentro de un motor intenta usar uno de los métodos helpers de enrutamiento de la aplicación, puede resultar en una llamada a un método indefinido. Si encuentras tal problema, asegúrate de que no estás intentando llamar a los métodos de enrutamiento de la aplicación sin el prefijo main_app
desde dentro del motor.
6.5 Activos
Los activos dentro de un motor funcionan de manera idéntica a una aplicación completa. Debido a que la clase del motor hereda de Rails::Engine
, la aplicación sabrá buscar activos en los directorios app/assets
y lib/assets
del motor.
Como todos los demás componentes de un motor, los activos deben estar en un espacio de nombres. Esto significa que si tienes un activo llamado style.css
, debería colocarse en app/assets/stylesheets/[engine name]/style.css
, en lugar de app/assets/stylesheets/style.css
. Si este activo no está en un espacio de nombres, existe la posibilidad de que la aplicación anfitriona pueda tener un activo llamado de manera idéntica, en cuyo caso el activo de la aplicación tendría prioridad y el del motor sería ignorado.
Imagina que tenías un activo ubicado en app/assets/stylesheets/blorgh/style.css
. Para incluir este activo dentro de una aplicación, solo usa stylesheet_link_tag
y referencia el activo como si estuviera dentro del motor:
<%= stylesheet_link_tag "blorgh/style.css" %>
También puedes especificar estos activos como dependencias de otros activos usando declaraciones de requerimiento de Pipeline de Activos en archivos procesados:
/*
*= require blorgh/style
*/
Recuerda que para usar lenguajes como Sass o CoffeeScript, deberías agregar la biblioteca relevante al .gemspec
de tu motor.
6.6 Activos Separados y Precompilación
Hay algunas situaciones en las que los activos de tu motor no son necesarios para la aplicación anfitriona. Por ejemplo, supongamos que has creado una funcionalidad de administración que solo existe para tu motor. En este caso, la aplicación anfitriona no necesita requerir admin.css
o admin.js
. Solo el diseño de administración del gem necesita estos activos. No tiene sentido que la aplicación anfitriona incluya "blorgh/admin.css"
en sus hojas de estilo. En esta situación, deberías definir explícitamente estos activos para la precompilación. Esto le dice a Sprockets que agregue tus activos del motor cuando se active bin/rails assets:precompile
.
Puedes definir activos para la precompilación en engine.rb
:
initializer "blorgh.assets.precompile" do |app|
app.config.assets.precompile += %w( admin.js admin.css )
end
Para obtener más información, lee la guía del Pipeline de Activos.
6.7 Otras Dependencias de Gemas
Las dependencias de gemas dentro de un motor deben especificarse dentro del archivo .gemspec
en la raíz del motor. La razón es que el motor puede instalarse como un gem. Si las dependencias se especificaran dentro del Gemfile
, no serían reconocidas por una instalación de gem tradicional y, por lo tanto, no se instalarían, causando que el motor no funcione correctamente.
Para especificar una dependencia que debería instalarse con el motor durante una instalación de gem tradicional, especifícala dentro del bloque Gem::Specification
dentro del archivo .gemspec
en el motor:
s.add_dependency "moo"
Para especificar una dependencia que solo debería instalarse como una dependencia de desarrollo de la aplicación, especifícala así:
s.add_development_dependency "moo"
Ambos tipos de dependencias se instalarán cuando se ejecute bundle install
dentro de la aplicación. Las dependencias de desarrollo para el gem solo se usarán cuando se ejecuten el desarrollo y las pruebas para el motor.
Ten en cuenta que si deseas requerir inmediatamente dependencias cuando se requiera el motor, debes requerirlas antes de la inicialización del motor. Por ejemplo:
require "other_engine/engine"
require "yet_another_engine/engine"
module MyEngine
class Engine < ::Rails::Engine
end
end
Comentarios
Se te anima a ayudar a mejorar la calidad de esta guía.
Por favor contribuye si ves algún error tipográfico o errores fácticos. Para comenzar, puedes leer nuestra sección de contribuciones a la documentación.
También puedes encontrar contenido incompleto o cosas que no están actualizadas. Por favor agrega cualquier documentación faltante para main. Asegúrate de revisar Guías Edge primero para verificar si los problemas ya están resueltos o no en la rama principal. Revisa las Guías de Ruby on Rails para estilo y convenciones.
Si por alguna razón detectas algo que corregir pero no puedes hacerlo tú mismo, por favor abre un issue.
Y por último, pero no menos importante, cualquier tipo de discusión sobre la documentación de Ruby on Rails es muy bienvenida en el Foro oficial de Ruby on Rails.