#221 Subdomains in Rails 3
- Download:
- source codeProject Files in Zip (109 KB)
- mp4Full Size H.264 Video (19.6 MB)
- m4vSmaller H.264 Video (13.7 MB)
- webmFull Size VP8 Video (35.2 MB)
- ogvFull Size Theora Video (26.4 MB)
Sono passati quasi due anni da quando è stato trattato l’argomento sottodomini. Rails 3 introduce alcuni supporti ai sottodomini, che vedremo in questo episodio.
L’applicazione che useremo sarà la solita già usata nell’altro episodio sui sottodomini. Si tratta di una semplice applicazione di blogging che supporta più di un blog. Di sotto è riportata la home page che elenca tutti i blog. Ciascun blog ha un insieme di articoli ad esso associati.
Se clicchiamo sul link di modifica di un blog, vedremo che ciascuno ha due attributi: un name
e un subdomain
. Vogliamo usare l’attributo subdomain
come sottodominio nell’URL per tale blog:
Configurazione dei sottodomini nell’ambiente di sviluppo
La prima cosa da configurare è la modalità di gestione dei sottodomini nella modalità di sviluppo. La nostra applicazione ha l’URL http://localhost:3000 che non ci consente di avere sottodomini, per cui dobbiamo trovare un modo alternativo.
Uno dei modi per farlo potrebbe essere quello di configurare la nostra applicazione per usare Passenger. Potremmo quindi assegnarle un dominio tipo http://blog.local
e modificare il file /etc/hosts
per configurare i sottodomini in questo modo:
127.0.0.1 personal.blog.local company.blog.local
Idealmente vorremmo poter usare un carattere wildcard che faccia match con ciascun sottodominio in nodo tale che non dobbiamo continuamente cambiare il file degli host quando sviluppiamo la nostra applicazione. Sfortunatamente non possiamo farlo, per cui dovremo trovare un’altra soluzione.
Una buona soluzione è offerta da Tim Pope sul suo blog. Lui ha comprato il nome di dominio smackaho.st
e lo ha reso un wildcard del localhost. Uno dei tizi che ha lasciato un commento sul suo blog ha fatto qualcosa del genere con un nome di dominio più corto, lvh.me
. Useremo questo approccio.
Se puntiamo a http://lvh.me:3000/ vedremo la home page della nostra applicazione, poichè lvh.me
viene risolto nell’indirizzo IP 127.0.0.1. La differenza sta nel fatto che ora possiamo anteposse un qualunque sottodominio che vogliamo all’URL e questo continuerà a puntare alla stessa applicazione.
Ora che siamo in grado di usare i sottodomini nella nostra applicazione, possiamo configurare i suoi instradamenti in modo tale che il sottodominio personal conduca alla action show del blog Personal. Ecco come attualmente appare il nostro file di route:
Bloggit::Application.routes.draw do |map| resources :comments resources :articles resources :blogs root :to => "blogs#index" end
Per ora l’instradamento radice punta alla action blogs/index
. Vogliamo che questo sia vero solo nel caso in cui non sia specificato alcun sottodominio; laddove vi sia indicato un sottodominio, dovrebbe essere mostrata invece la vista associata alla action blogs/show
. In Rails 3 si può fare tutto ciò aggiungendo un vincolo:
Bloggit::Application.routes.draw do |map| resources :comments resources :articles resources :blogs match '/' => 'blogs#show', :constraints => { :subdomain => /.+/ } root :to => "blogs#index" end
In un’applicazione Rails 2 avremmo dovuto usare un plugin per farlo, ma in Rails 3 tutto ciò è già incluso nel framework. L’opzione :subdomain
accetta sia una string, sia un’espressione regolare come argomento: in questo caso abbiamo usato un’espressione regolare che facesse match con almeno un carattere, in modo tale che ogni sottodominio facesse match con l’instradamento. Si noti come sia essenziale che l’instradamento radice venga messo dopo il route di sottodominio. Se così non fosse, l’instradamento di root intercetterebbe tutti gli indirizzi di sottodominio per primo, e gli instradamenti ai sottodomini non verrebbero mai chiamati. Come regola aurea generale, più il route è specifico, più deve stare in alto nella lista.
Se ora visitiamo il sottodominio personal
, otteniamo un errore, ma ce lo aspettavamo, dal momento che stiamo eseguendo al action show
del BlogsController
e non abbiamo fornito un id
:
La cosa è semplice da sistemare. Dobbiamo solo modificare la action show
in modo tale che trovi un blog in base al sottodominio piuttosto che per id
:
def show @blog = Blog.find_by_subdomain!(request.subdomain) end
Quando adesso ricarichiamo la pagina, vediamo gli articoli del nostro blog “Personal”:
Se togliamo il sottodominio, vedremo la home page che avevamo in precedenza:
C’è ancora un piccolo problema con gli instradamenti, tuttavia. Se andiamo su http://www.lvh.me:3000, la nostra applicazione cercherà un blog di sottodominio www
, mentre invece in questo caso dovrebbe puntare alla pagina index dei blog. Potremmo provare a correggere questa cosa agendo in qualche maniera furba sull’espressione regolare nei vincoli presenti nel file di routes, ma invece, al fine di concederci una maggior flessibilità e controllo sui sottodomini, faremo la stessa cosa nel codice Ruby. Tutto ciò è reso possibile da Rails 3, scrivendo una classe e spostando il codice coi vincoli lì dentro.
Per prima cosa cambieremo il file degli instradamenti affinchè utilizzi una nuova classe chiamata Subdomain
. Facciamo ciò, usando il metodo constraints
e passandogli la classe come argomento:
Bloggit::Application.routes.draw do |map| resources :comments resources :articles resources :blogs constraints(Subdomain) do match '/' => 'blogs#show' end root :to => "blogs#index" end
Poi dobbiamo creare la classe Subdomain
che andrà nella cartella /lib
. Questa classe dovrà avere un metodo di classe denominato matches?
che accetta un oggetto request
per parametro. Questo oggetto request è esattamente lo stesso oggetto che accediamo nei nostri controller e nelle nostre viste, per cui su di esso possiamo usare gli stessi metodi che usiamo in quei contesti. Questo metodo deve restituire un valore booleano che indica se il route dato fa match con la richiesta o meno. Nel nostro caso vogliamo che il metodo restituisca true
solo se la request ha un sottodominio e tale sottodominio non è www
, per cui la nostra classe sarà così fatta:
class Subdomain def self.matches?(request) request.subdomain.present? && request.subdomain != 'www' end end
Quando ora visitiamo http://www.lvh.me:3000, vediamo la home page proprio come volevamo:
Correggere i link
Ora sistemeremo i collegamenti a ciascun blog nella home page in modo tale che puntino al sottodominio corretto anzichè alla normale action show
per tale blog, dal momento che se così fosse, verrebbe fuori un errore causato dal fatto che ora la action show si aspetta un sottodominio.
Nel codice della vista per l’action index
abbiamo un link standard a ciascun blog fra tag h2
:
<% title "Blogs" %> <% for blog in @blogs %> <div> <h2><%= link_to blog.name, blog %></h2> <div class="actions"> <%= link_to "Edit", edit_blog_path(blog) %> | <%= link_to "Destroy", blog, :confirm => 'Are you sure?', :method => :delete %> </div> <% end %>
Cambiamo questo link affinchè punti all’URL radice con l’opportuno sottodominio. Sfortunatamente non possiamo passare una option subdomain
al metodo root_url
come potremmo fare con subdomain_fu. In questo caso dobbiamo creare il nome dell’host completo da zero e includere in quello il sottodominio. Il nome dell’host sarà creato a partire dal subdomain
del blog, dal domain
corrente e dalle proprietà port_string
dell’oggetto request
:
<h2><%= link_to blog.name, root_url(:host => blog.subdomain + '.' + request.domain + request.port_string) %></h2>
Al ricaricamento della pagina principale, ora i link ai blog punteranno ai sottodomini corretti.
Ripulire il codice per cambiare il sottodominio
Ora tutto funziona bene, ma il codice riportato sopra che crea i link ad altri sottodomini potrebbe essere un po’ più pulito, specialmente se dovessimo usarlo parecchio. Per ripulire il tutto, spostiamo la logica in un metodo helper a parte che chiamiamo with_subdomain
e che accetta il subdomain
come argomento. Innanzitutto cambiamo il codice della vista, affinchè chiami il metodo che stiamo per implementare:
<h2><%= link_to blog.name, root_url(:host => with_subdomain(blog.subdomain)) %></h2>
Mettiamo il metodo helper dentro il proprio module, in un file chiamato url_helper.rb
:
module UrlHelper def with_subdomain(subdomain) subdomain = (subdomain || "") subdomain += "." unless subdomain.empty? [subdomain, request.domain, request.port_string].join end end
Il codice nel module per prima cosa imposta il subdomain
ad una stringa vuota, se il valore passato è nil
, poi gli aggiunge un punto, nel caso in cui subdomain
non sia una stringa vuota. Infine concatena subdomain
, domain
e port_string
e restituisce il valore.
Aggiungiamo il module anche all’ApplicationController
, in modo tale che tutti i nostri controller possano utilizzare questo metodo:
class ApplicationController < ActionController::Base include UrlHelper protect_from_forgery layout 'application' end
Anche se abbiamo ripulito abbastanza il codice della vista, potrebbe esserlo ancor di più se potessimo semplicemente aggiungere un’opzione :subdomain
al metodo root_url
e ciò è possibile se ridefiniamo il metodo url_for
. Tutto ciò che dobbiamo fare è aggiungere il seguente metodo al nostro module UrlHelper
:
def url_for(options = nil) if options.kind_of?(Hash) && options.has_key?(:subdomain) options[:host] = with_subdomain(options.delete(:subdomain)) end super end
Il metodo ridefinito url_for
controlla per vedere se l’hash options
ha una chiave denominata :subdomain
al suo interno e, se così, imposta l’opzione :host
ad essere il valore di with_subdomain
per tale sottodominio. Infine chiama la super
, in modo tale che tutto il codice di default del metodo originale si eseguito e la parte restante dell’URL sia generata opportunamente. Non c’è bisogno di usare alias_method_chain
in questo caso, chiamare la super
è sufficiente.
Ora possiamo aggiornare il codice della nostra vista per usare la option :subdomain
:
<h2><%= link_to blog.name, root_url(:subdomain => blog.subdomain) %></h2>
Al ricaricamento della pagina index, cliccando su uno dei link ai blog il link condurra ancora all’URL corretto con il giusto sottodominio:
Sarebbe bello avere un link su ogni blog che potesse riportare alla pagina index. Possiamo aggiungerlo invocando root_url
con un valore false per l’opzione :subdomain
:
<p><%= link_to "All Blogs", root_url(:subdomain => false) %></p>
Ciò ci darà il link alla pagina index che volevamo:
Gestire domini di primo livello diversi
Un aspetto che non abbiamo ancora affrontato è come gestire nomi di dominio composti da più di due parti. Ebbene, anche se il nostro codice per il sottodominio funzionerà per i domini .com, non funzionerà invece per i domini che finiscono, per esempio, per .co.uk. Per far sì che funzioni anche per questi ultimi, dobbiamo cambiare la nostra applicazione ovunque chiami request.domain
o request.subdomain
affinchè possiamo indicare il numero di punti che ha il nome di dominio (al netto dei sottodomini). Rails assume un valore di default di 1, ma come appena citato per i domini del tipo .co.uk dobbiamo impostare tale valore a 2.
Dobbiamo cambiare la nostra applicazione in due punti: nel metodo UrlHelper
e nella classe Subdomain
usata nei nostri instradamenti:
def with_subdomain(subdomain) subdomain = (subdomain || "") subdomain += "." unless subdomain.empty? [subdomain, request.domain(2), request.port_string].join end
class Subdomain def self.matches?(request) request.subdomain(2).present? && request.subdomain(2) != "www" end end
Ovviamente non vogliamo veramente cablare tale valore nel codice. Lo dovremo realisticamente rendere dinamico in modo tale che in modalità di sviluppo possiamo impostare un valore di 1 (per un dominio del tipo lvh.me
) mentre in produzione useremo 2. Questo valore può essere impostato in un file di configurazione esterno che carica l’ambiente in modo opportuno.
Cookie
C’è un’ultima questione che affronteremo in questo episodio. Se osserviamo i cookie presenti nel nostro browser e li filtriamo per il nome di dominio che abbiamo utilizzato per questa applicazione, vedremo che viene salvata una sessione diversa per ciascun sottodominio visitato. Non vogliamo che avvenga questo, perchè significa che le sessioni non sono condivise fra i vari sottodomini.
Una soluzione a questo problema è stata presentata nell’episodio 123, ma ora in Rails 3 c’è un modo migliore per risolvere lo stesso problema. Nel file /config/initializers/session_store.rb
dobbiamo semplicemente aggiungere l’opzione :domain
al metodo Rails.application.config.session_store
, dandogli come valore :all
:
Rails.application.config.session_store :cookie_store, :key => '_bloggit_session', :domain => :all
Tuttavia non è sufficiente questo intervento da solo. L’opzione :domain
è stata aggiunta solo dopo l’uscita di Rails 3.0 beta 4, che nel momento in cui si sta scrivendo questo episodio è anche la versione corrente. Pertanto occorre lanciare l’applicazione su Edge Rails oppure attendere la prossima release candidate per vedere il tutto funzionare correttamente. Usata con una versione di Rails che la supporta, questa opzione rende le sessioni della nostra applicazione condivise fra i sottodomini. L’opzione :all
assume che la nostra applicazione abbia un dominio di primo livello di dimensione 1. Se così non fosse, potremmo definire un nome di dominio al posto di :all e quest’ultimo verrebbe utilizzato come dominio base per la sessione.
E’ tutto per questo episodio. Poter gestire sottodomini senza la necessità di installare plugin di terze parti è una bella novità di Rails 3 e può essere utilizzata in vari modi nelle nostre applicazioni.