Cómo separar frontend + backend con Rails API, Nuxt.js y Devise-JWT

He sido un gran fanático de Ruby on Rails desde los primeros días, y aunque el mundo web ha avanzado mucho desde entonces, Rails todavía tiene un lugar en este panorama, para el desarrollo rápido de aplicaciones con código limpio y fácil de mantener.

Pero el mundo web ha cambiado. El auge de las aplicaciones web progresivas (y las aplicaciones móviles antes que ellas) ha llevado a un mundo en el que los usuarios desean una interfaz interactiva sólida de sus aplicaciones web: hacer clic en los enlaces y descargar la siguiente página de la interfaz de usuario del servidor se siente como algo que la gente hizo en el 2000s.

Recientemente me he convertido en un gran fanático de Nuxt.js, que he empezado a pensar como "los rieles de la interfaz". Nuxt es un marco de desarrollo de aplicaciones basado en Webpack y Vue.js. Tiene un back-end pero, en mi experiencia, realmente brilla cuando se usa en modo generar para construir aplicaciones sin servidor que se pueden implementar en servicios que normalmente tienen contenido estático, como GitHub Pages o S3. Entre los ejemplos de aplicaciones sin servidor que he creado con Nuxt se incluyen Maple y el sitio web de mi empresa, que se ejecutan en páginas GitHub.

Una gran cantidad de rumores en el mundo de Rails en este momento está en torno a Rails API: una versión simplificada de Rails que se puede utilizar para crear aplicaciones de servidor solo API que luego se conectan a cualquier interfaz que desee: aplicaciones móviles o aplicaciones basadas en navegador como como los construidos con Nuxt.

Pero parece haber una escasez de guías sobre cómo realmente haría esto, especialmente al considerar la autenticación. ¿Cómo puede proporcionar una interfaz de inicio de sesión en la interfaz que registre a sus usuarios en el backend?

Entonces aquí va. Una publicación de blog algo larga que pasa, paso a paso, el proceso de construir una aplicación web simple desacoplada en Rails API y Nuxt, y construir un sistema de autenticación increíblemente básico usando Devise-JWT.

Nota: Esta guía supone un conocimiento razonablemente bueno de Rails, un conocimiento pasajero de Vue.js y al menos una comprensión básica de Docker.

Si desea ver la aplicación finalizada, o se queda atascado en cualquier momento, puede consultar el código fuente en GitHub.

Parte 1: crear un entorno de desarrollo

Antes de comenzar a codificar, necesitaremos un entorno de desarrollo. Me gusta usar Docker para esto, porque mantiene todo separado. Es probable que su base de datos esté en producción, el backend y la interfaz se ejecutarán en diferentes lugares, así que comencemos ya que queremos continuar para evitar el acoplamiento accidental.

Primero, creemos aplicaciones Rails API y Nuxt en blanco:

Esto supone que tiene las CLI "rieles" y "vue" instaladas en su máquina host. Por supuesto, puede usar las imágenes de Docker para estas CLI si no lo hace.

Las diversas opciones de rieles nuevos hacen que omita agregar pruebas, Action Cable y Spring, y también evita que se ejecute el paquete en su máquina host (queremos ejecutar esto dentro de Docker). De manera similar, usamos yarn generate-lock-entry para generar el archivo yarn.lock sin tener que instalar los paquetes.

A continuación, cree pequeños Dockerfiles para construir los entornos frontend y backend, y un docker-compose.yml para describir cómo encajan entre ellos y la base de datos:

Algunas cosas a tener en cuenta aquí:

  • Las gemas y los paquetes de hilo se instalan en los volúmenes montados. Esto evitará que necesite reconstruir toda la imagen de Docker cada vez que cambie el Gemfile o package.json.
  • Esto supone que su ID de usuario de host es 1001 y crea un usuario dentro de cada contenedor con la misma ID de usuario. Si su ID de usuario es diferente, puede establecer la variable de entorno UID antes de compilar y la pasará como un argumento a la compilación.
  • Probablemente desee agregar node_modules al frontend .dockerignore & .gitignore y vendor / bundle, log, tmp, a los backend. De esa manera no se copiarán durante el tiempo de construcción.

Ahora es el momento de construir todo:

La razón por la que necesita ejecutar paquetes e hilados después de la construcción es porque su archivo de composición acoplable monta sus volúmenes de host en los contenedores, por lo que debe instalar los paquetes en los volúmenes de host, así como las imágenes que se utilizan para crear los contenedores.

Un par de últimas cosas antes de que pueda abrir este entorno:

  1. Edite database.yml para establecer el nombre de host en "db" y el usuario en "postgres".
  2. Edite package.json para cambiar la secuencia de comandos "dev" a "HOST = 0.0.0.0 nuxt" (para que sea visible en su máquina host).
  3. Ejecute "docker-compose run backend rails db: create" para crear la base de datos de desarrollo.

Todo bien, ahora debería poder escribir:

docker-componer

Y su entorno (base de datos, backend y frontend) se activará. Puede verificar las interfaces web en: 8080 y: 3000 y debería ver las páginas predeterminadas para cada una.

El frontend es en realidad un servidor webpack-dev-server, al que quizás no estés acostumbrado si eres un codificador de Rails de la vieja escuela. Los cambios que realice en la interfaz de usuario se reflejarán inmediatamente en su navegador, sin necesidad de volver a cargarlos. Este es un cambio emocionante!

¡Correcto! ¡Haga que su proyecto se comprometa con el control de versiones y luego comencemos a construir algunas cosas!

Parte 2: hacer que hablen entre sí

Construyamos nuestro primer método API. Solo vamos a crear un recurso llamado "ejemplo" que tenga un nombre y un color, y crearemos 3 de estos para probar.

Edite routes.rb para mover la nueva ruta a un ámbito de API, y agregue un método de índice simple al ExampleController:

Asegúrese de que su ApplicationController le dice a todos sus hijos que pueden responder a las solicitudes de JSON (esto es necesario para algunas gemas, como Devise):

[Editar: es posible que tengas que esperar hasta que Devise esté instalado para hacer esto, porque parece que hay un problema de dependencia si lo haces tan temprano. Avíseme si tiene alguna idea sobre cómo presentarlos tan pronto.]

Ahora, inmediatamente debería poder visitar http: // localhost: 8080 / api / examples para ver su API en acción:

Ahora para la parte divertida: conectaremos la interfaz al backend. Pero primero, ¡no te olvides de comprometerte con el control de versiones!

Vamos a usar axios para hablar con la API y vuetificar como un kit de interfaz de usuario. (Puede usar HTML y CSS en Vue, pero también hay integraciones de interfaz de usuario más profundas: vuetify es un kit de interfaz de usuario basado en Diseño de materiales). Ambos tienen integraciones con Nuxt disponibles en la comunidad, así que instalemos esos:

docker-compose ejecutar hilo frontend add @ nuxtjs / axios @ nuxtjs / vuetify

y agregue al archivo de configuración de Nuxt:

(Obviamente, en producción, querrás mover la configuración de axios al entorno, pero por ahora está bien codificarla).

Ahora reemplacemos los diseños / default.vue y pages / index.vue que Nuxt ha creado para nosotros:

Default.vue es un diseño vuetify estándar, con una barra de herramientas que contiene un enlace a la página de inicio. La opción nuxt del enlace le dice que use el enrutador de Nuxt para manejar el enlace, en lugar de hacerlo en el navegador.

index.vue es un poco más complejo: el método montado () se llama cuando se inicializa la plantilla, y eso a su vez llama a updateExamples (), que usa la integración axios para establecer la variable de ejemplos a los resultados del método API. La contiene un conjunto reactivo de mosaicos que se rellena automáticamente según lo que se encuentre en los ejemplos (por lo que estará vacío al principio y luego se completará cuando se complete el método API).

Pero si intentas visitar esto ahora obtienes un desafortunado error:

JavaScript no puede consultar puntos finales en otros dominios a menos que esos dominios establezcan los encabezados CORS de manera adecuada. Afortunadamente, Rails API ha predicho que querremos eso.

Descomente la línea "rack-cors" en el Gemfile y descomente el código en cors.rb, cambiando example.com a localhost: 3000 (o *). Instala la gema:

docker-compose run -u root backend bundle

y reinicie los contenedores de la ventana acoplable presionando Ctrl-C en los que se están ejecutando y volviendo a componer la ventana acoplable. Crucemos los dedos ahora verán algo como esto:

Woohoo! ¡Su aplicación frontend muestra datos directamente desde el backend! Intente agregar un nuevo ejemplo a la base de datos (por ejemplo, "qux / cyan") para confirmar que su interfaz realmente está recuperando datos de su back-end.

Comprométete con el control de versiones y cómprate un café. El siguiente paso es la autenticación y es un poco más complicado.

Parte 3: Autenticación con Devise-JWT

Devise-JWT es una extensión de la popular biblioteca de autenticación Rails Devise para agregar soporte para JSON Web Token, una implementación popular de inicio de sesión único. Si buscas en Google, encontrarás muchas personas que dicen que es fácil agregar JWT para idear tú mismo, pero no si quieres aprovechar adecuadamente todas las funciones de Devise, y no si quieres cerrar sesión (es decir, hacer un Ficha Invalida).

Primero, agregue "idear" y "idear-jwt" a su Gemfile y ejecute:

docker-compose run -u root backend bundle

Entonces querrás seguir las instrucciones para instalar Devise. Como estamos usando una API, no tiene que preocuparse demasiado por las cosas de la capa de vista. Lo acabo de hacer:

> rails g idear: instalar
> rieles g idean usuario
> rieles db: migrar

Devise-JWT no tiene un instalador, pero tiene buenas instrucciones de instalación. Debes decidir cómo quieres invalidar los tokens. Fui por la opción de lista negra. Por ejemplo:

> rails g modelo jwt_blacklist jti: string: index exp: datetime

Edite la migración para agregar null: false y elimine las marcas de tiempo y agregue "include Devise :: JWT :: RevocationStrategies :: Blacklist" al modelo.

Para habilitar JWT, deberá agregar jwt_secret al archivo secrets.yml (puede crear nuevos secretos con "secreto de rails") y agregar esto a devise.rb:

necesita actualizar la línea de diseño en su modelo de usuario para agregar:

: jwt_authenticatable, jwt_revocation_strategy: JwtBlacklist

Ejecute "rails db: migrate" nuevamente para crear su lista negra.

Mueva el devise_for route en routes.rb a su: api scope y reinicie sus contenedores.

Ahora debería poder probar su API usando algo como YARC. Primero, cree un usuario en la consola de Rails:

> rieles c
>> User.create! (Correo electrónico: "test@ejemplo.com", contraseña: "contraseña")

Luego, intente crear un mensaje POST para su / api / users / sign_in endpoint que tenga este aspecto:

Y debería ver un encabezado de autorización regresar con el inicio de sesión exitoso:

Increíble. ¡Estamos llegando allí, lo prometo!

Queremos usar la propia biblioteca de autenticación de Nuxt, que espera que el JWT se devuelva en el cuerpo de la solicitud de inicio de sesión, y también desea realizar una solicitud de API por separado para conocer los detalles del usuario, como su información de perfil. Así que anulemos el controlador de sesión de Devise.

Cambie la configuración del dispositivo en routes.rb para decir que ha anulado el controlador y agregue un punto final "actual" adicional:

Luego agreguemos ese archivo de controlador y dos vistas. Si usa jbuilder aquí, deberá descomentar jbuilder en el Gemfile, ejecutar "docker-compose run -u root backend bundle" y reiniciar sus contenedores nuevamente.

Ahora prueba estos métodos. Realice la POST anterior y esta vez el token debe volver al cuerpo de respuesta, así como a los encabezados.

Intente hacer un GET al / api / users / endpoint actual con ese encabezado "Autorización: Bearer $ token" y vea si puede recuperar la identificación y el correo electrónico del usuario.

Woop! Una última cosa: hagamos que el ExampleController requiera autenticación. Añadir a ese controlador:

before_action: authenticate_user!

Si vuelve a su aplicación frontend, ahora debería ver que está fallando debido a un error 401 no autorizado. ¡Es hora de agregar autenticación a la interfaz!

Instale la biblioteca de autenticación Nuxt:

Reinicie sus contenedores antes de continuar para habilitar esta nueva configuración.

Creemos una página de usuario login.vue con controles de inicio / cierre de sesión basados ​​en el estado actual del usuario:

Están sucediendo muchas cosas aquí, pero debería ser razonablemente fácil de resolver. El método login () llama al método equivalente en la biblioteca de autenticación, pasando el objeto JSON apropiado que Devise está esperando. Si hay un error, establece el error, que está siendo vigilado por .

Y finalmente, querrá que los usuarios sean redirigidos si no han iniciado sesión. De manera predeterminada, el middleware de autenticación redirige a los usuarios a iniciar sesión. Vue si no han iniciado sesión, por lo que solo tenemos que habilitar el middleware de autenticación en el índice .vue:

middleware: ['auth'],

También es posible que desee agregar un enlace / iniciar sesión en la barra de herramientas en default.vue. Tu decides.

¡Ahora prueba tu aplicación frontend nuevamente!

Redirigido a la página de inicio de sesión al cerrar sesión¡Pero puedo ver mis ejemplos al iniciar sesión!

¡Aquí es donde te dejo! Realmente es solo el comienzo de la integración de autenticación, pero espero que puedan ver que no es una gran cantidad de trabajo comenzar. Deberá agregar el registro y hacer cosas como asegurarse de que los tokens caducados no tengan precedencia sobre las credenciales al iniciar sesión. [Editar: Los encargados del módulo de autenticación han acordado que los tokens caducados no deben enviarse con la solicitud de inicio de sesión, y @ nuxtjs / auth 4.2.0 soluciona el problema.] La documentación de autenticación y las aplicaciones de demostración son un buen lugar para comenzar.

Si este artículo te ha sido útil, compártelo, dame unos aplausos o avísame cómo te va en los comentarios.

Actualización: ¡Ahora puedes leer este seguimiento sobre cómo implementarlo en Heroku!