React + Flux con backend Rails API - Parte 1
Costruire un'app moderna spezzata in due: un'API Rails minimale e un frontend React con architettura Flux. Parte 1: setup di rails-api, token authentication, CORS.
Versione italiana dell’articolo originale del 28 gennaio 2015; traduzione a cura del team.
Sto lavorando al frontend di un progetto che stiamo sviluppando qui in Fancy Pixel. Stiamo prendendo quella che sembra una buona abitudine: tagliare quella che sarebbe un’app monolitica Rails in un backend leggero che serve API e un frontend che le consuma. Lo abbiamo già fatto in un passato non troppo lontano con Angular.js. Andava tutto bene, finché non è andato più bene. C’è qualcosa che non mi convince in Angular, non entro nei dettagli (l’hanno già fatto in molti), ma diciamo che c’è troppa magia coinvolta per i miei gusti (dice quello che usa Rails). La magia mi sta bene finché posso capire come smanettare con gli interni quando le cose vanno a sud. Con Angular lo sforzo sembra troppo: ma è solo gusto personale. Non posso poi negare che i grossi cambiamenti strutturali introdotti con la 2.0 siano stati l’ultimo chiodo nella bara. Volevo provare qualcosa di nuovo, qualcosa che imponesse un’architettura solida alle nostre app, lasciandomi controllare i singoli ingranaggi dell’engine. React ha ricevuto molta buona stampa negli ultimi mesi, quindi ho colto l’occasione per buttarmi. In questo post in tre parti troverai più o meno tutto quello che ho imparato scrivendo un frontend con React, architettura Flux vanilla, che consuma un’API scritta in Rails.
Scegliere il backend
Vista la nostra esperienza, la scelta ovvia per noi era Rails, ma con una variante: rails-api. Rails-api è una versione “spogliata” di Rails, dove gran parte del middleware “inutile” non è inclusa (ma puoi includerlo se ti serve). Usare Rails per servire JSON può sembrare overkill ai più, ma la pagina GitHub di rails-api ha degli ottimi argomenti per controbattere, e li trovo azzeccati. TL;DR: Rails è meraviglioso, sfruttiamolo.
La tecnologia per il frontend
React è una libreria JavaScript per costruire user interface, sviluppata e open source dagli ingegneri di Facebook. Il suo principale punto di forza è la capacità di fornire un modo dinamico e veloce per costruire app isomorfiche. “Isomorfico” vuol dire che l’app può essere renderizzata con facilità sia sul server che sul client, cosa utile per la SEO. Personalmente della SEO non potrebbe importarmene di meno, anche se ne capisco l’importanza… ciò che mi ha convinto di React è il Virtual DOM e il modo in cui i dati sono organizzati e gestiti nelle view. Il Virtual DOM è qualcosa che vedremo implementato anche in altri framework JS (Ember lo fa già, se non sbaglio). Le view possono essere renderizzate sul server al primo request, poi la tecnologia sottostante renderizza le pagine successive in un Virtual DOM, che viene poi diffato con il DOM reale: solo le differenze diventano cambiamenti nella pagina visibile. Ed è veloce. Brillante. Questo ci consente di scrivere frontend come si faceva una volta: in modo dichiarativo. Specifichiamo come dovrebbe apparire la UI, e quando i dati cambiano React si occupa del refresh della pagina, cambiando solo le parti che devono essere cambiate.
Flux
Questo copre backend e view, ci manca qualcosa nel mezzo: un’architettura da seguire. Flux è un’architettura per costruire web UI e funziona molto bene insieme a React (ma può davvero essere applicata ovunque).
Iniziano i guai
Non sono mai stato un grande fan dell’implementare web UI: il CSS diventa sempre un casino, i file JavaScript diventano monoliti spaventosi dove il codice di bassa qualità va a morire, mentre gli sviluppatori testano le proprie doti speleologiche e perdono la sanità mentale. Forse sono solo io che faccio schifo, ma neppure usare Sass e CoffeeScript ha mai risolto davvero i miei problemi. Ero entusiasta di provare qualcosa di davvero nuovo, ma sapevo che iniziare con una tecnologia così giovane sarebbe stata una rogna importante. Caso esemplare: imparare e iniziare a essere produttivo (cioè: scrivere codice utilizzabile) ha richiesto un bel po’ di grattate di testa. Non c’è ancora una “best practice” chiara per fare i task comuni, né una configurazione iniziale chiara. Diciamola così: se vieni dal mondo RoR, dove la convention over configuration riduce di parecchio il boilerplate e ti “forza” a seguire le best practice consolidate, con Flux farai fatica. Questo post copre le soluzioni a cui siamo arrivati: non saranno perfette, ma sono ragionevolmente sicuro che non ci siano anti-pattern dentro, e sono un buon punto di partenza.
Mettere tutto insieme
Iniziamo a scrivere un po’ di codice. Faremo una semplice app Rails con signup, login e la possibilità di postare una story. Come una versione mignon di Medium. Chiamiamola appropriatamente Small.
Se ti interessano solo Flux e React, salta pure la parte Rails e vai dritto alla Parte 2.
Rails API
Un po’ di tempo fa mi sono imbattuto in questo articolo di Alan Peabody. Ho avuto un’esperienza simile alla sua: inizi a lavorare su un progetto, usi gli strumenti giusti, fai del tuo meglio per imporre buoni pattern, ma alla fine il codice frontend diventa spaventoso, qualcosa che nessuno vuole più mantenere. Rompiamo con l’asset pipeline, come dice il titolo, e lavoriamo per rendere Rails di nuovo bello. Useremo la gem rails-api per questo task. Puoi generare una nuova app col suo comando CLI, o puoi integrarla dopo. Io scelgo la seconda opzione, senza una vera ragione: pura abitudine.
rails new small
Aggiungiamo poi le gem rails-api, devise, active_model_serializers al Gemfile, e già che ci siamo togliamo tutte le gem che generano asset o contenuto delle view, jbuilder incluso. Il nostro Gemfile dovrebbe sembrare così (sezione di test omessa):
source 'https://rubygems.org'
gem 'rails', '4.2.0'
gem 'rails-api', '~> 0.4.0'
gem 'active_model_serializers', '~> 0.8.3' # NOTE: not the 0.9
gem 'devise', '~> 3.4.1'
gem 'sqlite3'
gem 'sdoc', '~> 0.4.0', group: :doc
gem 'thin'
group :development, :test do
gem 'faker'
gem 'byebug'
gem 'web-console', '~> 2.0'
gem 'spring'
end
Ora dobbiamo modificare l’application controller perché erediti da ActionController::API, e dire addio a protect_from_forgery. Dato che serviamo solo JSON, ha senso aggiungere
respond_to :json
all’application controller: aiuta a DRY-are tutto. Già che ci siamo, possiamo anche eliminare le cartelle assets e views, non ci serviranno.
Autenticazione
Devo prima definire una risorsa? Forse, ma è banale, togliamoci di mezzo l’autenticazione. Stiamo costruendo un’API, quindi nessuna session sarà coinvolta: dobbiamo autenticare l’utente in ogni request. Userò l’Oauth2 Resource Owner Password Credentials Grant, che suona fighissimo ma in pratica è solo un token nell’header della request che autentica il caller.
La gem Devise implementava una strategia token_authenticatable, ma è stata rimossa per motivi di sicurezza. Ci sono gem che implementano la strategia (come Doorkeeper), ma dato che è piuttosto facile da implementare lo faccio da solo. Prima installiamo Devise aggiungendolo al Gemfile e lanciando rails generate devise:install dopo un bundle install, poi creiamo il model User:
rails generate devise User
Token authentication
La token authentication è stata rimossa da Devise un paio d’anni fa, questo link spiega perché. Dobbiamo implementarla noi, ma è abbastanza facile. Il token sarà composto da due informazioni: l’id dell’utente seguito dal token vero e proprio, separati da :. Per questo sample useremo l’id del database dell’utente, per semplicità — ma ovviamente non è una cosa intelligente da fare.
Per cominciare aggiungiamo un access_token all’utente (e già che ci siamo, anche uno username):
class AddAccessTokenToUser < ActiveRecord::Migration
def change
add_column :users, :access_token, :string
add_column :users, :username, :string
end
end
ed ecco il model User:
# app/models/user.rb
class User < ActiveRecord::Base
devise :database_authenticatable, :recoverable, :validatable
after_create :update_access_token!
validates :username, presence: true
validates :email, presence: true
private
def update_access_token!
self.access_token = "#{self.id}:#{Devise.friendly_token}"
save
end
end
L’autenticazione dell’utente vivrà nell’application controller:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
include AbstractController::Translation
before_action :authenticate_user_from_token!
respond_to :json
##
# User Authentication
# Authenticates the user with OAuth2 Resource Owner Password Credentials Grant
def authenticate_user_from_token!
auth_token = request.headers['Authorization']
if auth_token
authenticate_with_auth_token auth_token
else
authentication_error
end
end
private
def authenticate_with_auth_token auth_token
unless auth_token.include?(':')
authentication_error
return
end
user_id = auth_token.split(':').first
user = User.where(id: user_id).first
if user && Devise.secure_compare(user.access_token, auth_token)
# User can access
sign_in user, store: false
else
authentication_error
end
end
##
# Authentication Failure
# Renders a 401 error
def authentication_error
# User's token is either invalid or not in the right format
render json: {error: t('unauthorized')}, status: 401 # Authentication timeout
end
end
Concludiamo il processo di auth fornendo le route e il session controller:
# config/routes.rb
Rails.application.routes.draw do
devise_for :user, only: []
namespace :v1, defaults: { format: :json } do
resource :login, only: [:create], controller: :sessions
end
end
e il session controller:
# app/controllers/v1/sessions_controller.rb
module V1
class SessionsController < ApplicationController
skip_before_action :authenticate_user_from_token!
# POST /v1/login
def create
@user = User.find_for_database_authentication(email: params[:username])
return invalid_login_attempt unless @user
if @user.valid_password?(params[:password])
sign_in :user, @user
render json: @user, serializer: SessionSerializer, root: nil
else
invalid_login_attempt
end
end
private
def invalid_login_attempt
warden.custom_failure!
render json: {error: t('sessions_controller.invalid_login_attempt')}, status: :unprocessable_entity
end
end
end
Il SessionSerializer è un oggetto Active Model Serializer, qualcosa tipo:
# app/serializers/v1/session_serializer.rb
module V1
class SessionSerializer < ActiveModel::Serializer
attributes :email, :token_type, :user_id, :access_token
def user_id
object.id
end
def token_type
'Bearer'
end
end
end
E voilà. Migra, lancia il server e crea un utente via console. Dovresti ottenere qualcosa del genere:
$ curl localhost:3000/v1/login --ipv4 --data "username=user@example.com&password=password"
{
"token_type": "Bearer",
"user_id": 1,
"access_token": "1:MPSMSopcQQWr-LnVUySs"
}
Creare una risorsa
Non entro nei dettagli qui, il task è puro RoR. Creiamo una risorsa Story e un controller che gestisce la creazione. Trovi l’app Rails completa in questo repo. Andiamo avanti.
CORS
Vale la pena notare che la UI ora non sarà servita da Rails: potrebbe vivere addirittura su un server diverso. Esistono soluzioni per tenere UI e API sullo stesso dominio, ma per ora ci serve abilitare il Cross-origin resource sharing (CORS). Lo facciamo aggiungendo la gem rack-cors al Gemfile e poi mettendo questo in config/application.rb:
config.middleware.insert_before 'Rack::Runtime', 'Rack::Cors' do
allow do
origins '*'
resource '*',
headers: :any,
methods: [:get, :put, :post, :patch, :delete, :options]
end
end
Questo apre verso qualsiasi dominio, quindi maneggia con cura.
Prossima puntata
Fine per la parte Rails. È una gioia scrivere solo l’API in Rails. È più facile da testare, più facile da mantenere, ed è velocissimo. Ora procediamo col frontend. Per leggibilità spezzo l’articolo qui, salta alla Parte 2 per iniziare a costruire il frontend.