#169 Dynamic Page Caching
- Download:
- source codeProject Files in Zip (117 KB)
- mp4Full Size H.264 Video (17.9 MB)
- m4vSmaller H.264 Video (12.2 MB)
- webmFull Size VP8 Video (31.9 MB)
- ogvFull Size Theora Video (25.9 MB)
L’applicazione con cui lavoreremo in questo episodio è un forum per suonatori di piano. La pagina qui sotto è la home page e al momento ci sta mettendo un paio di secondi per caricarsi.
Anche se potrei fingere dicendovi che la pagina ci impiega così tanto a caricare in quanto ci sono logiche molto complesse che caricano un sacco di roba dietro le tende, la realtà è che in questo caso ho solo simulato il ritardo aggiungendo semplicemente una sleep 2 al metodo che serve la richiesta. Per lo scopo della dimostrazione non conta quale sia la causa reale del ritardo, faremo finta che sia un ritardo effettivo e non simulato, e poichè si tratta della home page, ci teniamo che sia anche molto rapida a caricarsi. Per migliorare i tempi di caricamento, faremo tutto il possibile per ottimizzare le performance di tale pagina.
Caching della pagina
Il modo più efficace per migliorare la reattività di una pagina in Rails è quello di farne caching. (Il caching di una pagina è stato affrontato come argomento nell’episodio 89). Attiviamo il caching della pagine e vediamo come andare avanti.
Il caching è disabilitato per default nella modalità di sviluppo, per cui dovremo aprire il file di configurazione /config/environments/development.rb
e abilitarla, impostando config.action_controller.perform_caching
a true
. Fatto ciò, possiamo aggiungere il caching alla action index
del nostro controller dei forum:
class ForumsController < ApplicationController before_filter :admin_required, :except => [:index, :show] caches_page :index def index @forums = Forum.all sleep 2 end # altri metodi omessi. end
Ora dobbiamo riavviare il nostro server per fare in modo che le modifiche alla configurazione siano sentite, dopodichè possiamo ricaricare la nostra pagina. Dovrebbe metterci un paio di secondi per caricare la prima volta, ma caricherà praticamente subito ad ogni successivo aggiornamento dal momendo che la pagina viene fornita a partire dalla cache.
Il caching funziona generando file di HTML statico nella directory /public
. Quando una richiesta viene fatta ad un’applicazione Rails, il server sotto cui l’applicazione sta girando controlla per prima cosa se può servire la richiesta mediante le risorse statiche che ha a disposizione. Per esempio, per servire la richiesta per questa pagina, cercherà un file chiamato /public/episodes/169-caching-delle-pagine-dinamico.html
. Se il server trova il file, lo restituisce e l’applicazione Rails non verrà nemmeno a sapere che quella pagina è stata chiesta; se invece il web server non trova quel file, inoltra la richiesta a Rails affinchè la processi lui. A questo punto, se l’azione richiesta ha il caching abilitato, allora Rails, dopo aver processato la richiesta, scriverà il file HTML nel posto opportuno sotto la cartella /public
, in modo tale che alle richieste successive per la stessa pagina, il web server possa servire direttamente la risposta senza coinvolgere nuovamente Rails. (Fra parentesi, questo è il motivo per cui avete dovuto rimuovere la pagina statica index.html
che era stata creata per default alla creazione dell’applicazione, per fare in modo che come home page si vedesse la vostra pagina generata dalla action del controller. Funziona esattamente allo stesso modo in cui funziona qualsiasi altro file di cache e impedisce alla action root reale di essere processata).
Le pagine salvate in cache, in quanto statiche, non possono avere contenuti dinamici, il che è un problema nel nostro caso in cui abbiamo contenuti che dipendono da chi sia l’utente attualmente autenticato. Se facessimo cache di tutta la pagina così com’è ora, chiunque visitasse tale pagina si vedrebbe autenticato come eifion.
<div id="user_nav"> <p>Welcome <strong>eifion</strong>! Not you? <a href="/logout">Logout</a></p> </div>
Anche se cliccassimo sul link di “log out”, risulteremmo comunque autenticati perchè la pagina di logout ridirige indietro alla home page dopo averci fatto uscire, che però è in cache, per cui alla fine verremmo serviti con la medesima risorsa statica.
Pare che ci sia un po’ troppo contenuto dinamico in questa pagina per permetterci di utilizzare il caching delle pagine. Potremmo però provare ad usare il cosiddetto "fragment caching" per fare cache delle sole parti della pagina che mostrano la lista dei forum, ma anche qui c’è contenuto dinamico, dal momento che i link di “edit” e di “destroy” per ogni forum devono essere mostrati ai soli utenti amministratori.
Utilizzo del Javascript con il caching delle pagine
Anche se può sembrare che la nostra pagina non sia cacheabile, in realtà esiste un modo perchè lo sia. Possiamo rimuovere il contenuto dinamico dalla pagina, usare il caching della pagina per fare cache della struttura di pagina e poi aggiungere il contenuto dinamico mediante JavaScript.
Prima di cominciare, disabiliteremo temporaneamente il caching e lo sleep
sul nostro controller dei forum. Dovremo anche assicurarci di rimuovere la pagina statica eventualmente generata, altrimenti non vedremo nessuno dei cambiamenti che faremo:
rm public/index.html
La prima parte della pagina che renderemo dinamica sono i link che permettono agli utenti amministratori di modificare o cancellare i forum ed il link new forum in fondo alla pagina. Il codice della vista è mostrato sotto con due parti della pagina che sono mostrati solo agli amministratori, racchiusi in un controllo if:
<% title "Piano Forums" %> <div id="forums"> <% for forum in @forums %> <div class="forum"> <h2><%= link_to h(forum.name), forum %></h2> <p><%= h forum.description %></p> <% if admin? %> <p class="admin"> <%= link_to "Edit", edit_forum_path(forum) %> | <%= link_to "Destroy", forum, :confirm => 'Are you sure?', :method => :delete %> </p> <% end %> </div> <% end %> </div> <% if admin? %> <p class="admin"><%= link_to "New Forum", new_forum_path %></p> <% end %>
Riumoviamo gli if e nascondiamo i link col CSS:
<p class="admin" style="display:none;"> <%= link_to "Edit", edit_forum_path(forum) %> | <%= link_to "Destroy", forum, :confirm => 'Are you sure?', :method => :delete %> </p>
<p class="admin" style="display:none;"><%= link_to "New Forum", new_forum_path %></p>
Se ora ricarichiamo la pagina, i link amministrativi sono nascosti:
Ora usiamo JavaScript per fare in modo che i link ritornino ad essere visibili se l’utente è un amministratore. In cima alla vista chiamiamo un metodo helper che aggiunge i riferimenti ai due file JavaScript alla sezione HEAD della pagina. (Questo metodo helper è parte dei nifty layout generator di Ryan Bates). Il primo file JavaScript è un riferimento alla jQuery library, anche se in realtà volendo, si può anche usare Prototype; il secondo riferimento è ad una action nell’UsersController
:
<% javascript 'jquery', '/users/current' %>
Al caricamento della pagina, il codice si sopra genera il seguente HTML:
<script src="/javascripts/jquery.js" type="text/javascript"></script> <script src="/users/current.js" type="text/javascript"></script>
La prima linea carica le librerie jQuery che dovremo scaricare ed aggiungere alla cartella /public/javascripts
della nostra applicazione. La seconda riga chiama la action show
del controller degli utenti, con current
passato come id fittizio. Dobbiamo cambiare la action show per fare in modo che possa rispondere a richieste JavaScript.
Il controller avrà bisogno di un metodo show
, ma può essere lasciato vuoto:
def show end
Poi abbiamo bisogno di un nuovo template di vista da usarsi in risposta alle richieste JavaScript. Nella cartella /app/views/users
creiamo un file chiamato show.js.erb
e mettiamogli il seguente codice dentro:
$(document).ready(function () { <% if admin? %> $('.admin').show(); <% end %> });
Il codice sopra usa la funzione jQuery $(document).ready()
per eseguire del JavaScript una volta che il DOM si è caricato completamente. Il codice sarà vuoto a meno che l’utente non sia amministratore, ma se lo è, tutti gli elementi della pagina che hanno classe admin
saranno mostrati.
Aggiornare lo stato
Anche la parte di pagina che mostra lo stato dell’utente deve essere generata dinamicamente, così come i messeggi flash mostrati al login e al logout. Entrambe queste tre parti della pagina si trovano nel file di layout dell’applicazione:
<%- flash.each do |name, msg| -%> <%= content_tag :div, msg, :id => "flash_#{name}" %> <%- end -%> <div id="user_status"> <% if current_user %> <p>Welcome <strong><%= current_user.username %></strong>! Not you? <%= link_to "Logout", logout_path %></p> <% else %> <%= link_to "Register", new_user_path %> <%= link_to "Log in", login_path %> <% end %> </div>
Aggiornare queste tre parti della pagina con JavaScript sarà più semplice se esse si trovano in un partial, per cui spostiamo il codice di sopra in un file partial nella cartella /app/views/layouts/dynamic_header.html.erb
. Poi possiamo sostituire il codice precedente con un riferimento al nostro nuovo partial:
<%= render 'layouts/dynamic_header' %>
Ciò che dobbiamo fare ora è di rimuovere i contenuti del partial dalla nostra home page statica e cached, e aggiungerli mediante JavaScript. Per questo, dobbiamo inoltrare in messaggio al partial per dirgli di non visualizzare se stesso nella home page. Un modo piuttosto ingegnoso per farlo, consiste nel aggiungere una variabile di istanza al codice della vista della home page. Il layout è renderizzato dopo la vista, per cui la variabile sarà disponibile per il layout.
In cima al file /app/views/forums/index.html.erb
aggiungiamo una variabile chiamata hide_dynamic ed impostiamola al valore true:
<% title "Piano Forums" %> <% javascript 'jquery', '/users/current' %> <% @hide_dynamic = true %> <div id="forums"> <!-- resto della pagina... -->
Ora possiamo modificare la chiamata al layout dynamic_header
per fare in modo che questo non sia renderizzato se @hide_dynamic
è true
:
<%= render :partial => 'layouts/dynamic_header' unless @hide_dynamic %>
Se ora aggiorniamo la pagina, vediamo che lo stato dell’utente e il link di log out sono scomparsi. Modifichiamo il nostro JavaScript in modo tale che possano ricomparire:
Dobbiamo cambiare la nostra funzione jQuery affinchè renderizzi il partial per mostrare lo stato dell’utente corrente e tutti i messaggi flash. Basta aggiungere una linea al nostro codice jQuery:
$(document).ready(function () { $('#container').prepend('<%= escape_javascript render("layouts/dynamic_header") %>'); <% if admin? %> $('.admin').show(); <% end %> });
Vogliamo aggiungere il partial come primo figlio del div
con id
container
; per fare ciò, abbiamo usato la funzione jQuery prepend()
. Questa funzione accetta una stringa di HTML per argomento, per cui le possiamo passare il partial renderizzato, racchiuso fra escape_javascript
per essere sicuri che il contenuto del partial non generi errori JavaScript.
Ora, al ricaricamento della pagina, il pannello di stato dell’utente ricomparirà, in quanto sarà stato aggiunto dal JavaScript chiamato nella pagina HTML statica che è la stessa sia per gli utenti loggati, sia per quelli anonimi.
Riabilitare il caching
Ora che tutte le parti dinamiche della pagina sono aggiornate da codice JavaScript, possiamo riabilitare il page caching ed riaggiungere lo sleep
per simulare la complessità (in questo caso non presente realmente) della pagina.
Al primo ricaricamento della pagina, ci vorranno un paio di secondi, ma ai successivi refresh, la pagina si caricherà pressochè instantaneamente, essendo la pagina servita dalla cache. La differenza, ora, sta nel fatto che possiamo autenticarci e scollegarci e la pagina si aggiornerà per riflettere il nostro stato, ed ogni link amministrativo si vedrà o meno a seconda di come effettivamente ci aspettiamo che sia.
Questo approccio non sarà efficiente come il completo caching della pagina, in quanto le parti JavaScript continuano ancora a tirare in ballo Rails, ma si dovrebbero notare aumenti di performance per file complessi, dal momento che la maggior parte della pagina viene servita dal web server come HTML statico.