#294 Playing with PJAX
- Download:
- source codeProject Files in Zip (88.9 KB)
- mp4Full Size H.264 Video (16.9 MB)
- m4vSmaller H.264 Video (9.47 MB)
- webmFull Size VP8 Video (11.4 MB)
- ogvFull Size Theora Video (22.3 MB)
Pjax es un plugin de jQuery escrito por Chris Wanstrath que permite actualizar una sección de una página con una petición AJAX en lugar de hacer una petición HTTP completa. La demo page1 muestra cómo funciona. Por defecto, al hacer clic en cualquiera de los enlaces de la página ésta se recargaría, lo que puede comprobarse porque la fecha que se muestra cambiaría. Con Pjax (que se activa haciendo clic en la casilla correspondiente) los enlaces ya no recargan la página, aunque sí que cambia la sección principal de ésta.
Pjax utiliza pushState
, por lo que usuario no percibe que se están haciendo peticiones AJAX: cada vez que se actualiza la página, cambian el título y la URL de la misma, e incluso se añade al historial del navegador la página anterior, por lo que el botón de volver atrás funciona adecuadamente. Pjax degrada bien y si el navegador del usuario no soporta pushState
o JavaScript está inhabilitado entonces se harán peticiones HTTP normales.
Integración de Pjax en una aplicación Rails
Hay varias maneras de integrar pjax en una aplicación Rails, en este episodio veremos dos de ellas. A continuación se muestra la aplicación con la que vamos a trabajar, que muestra un listado de productos en el que al hacer clic sobre un producto concreto se muestra una barra laterla para visualizar información acerca de dicho producto.
Ahora mismo al hacer clic en el enlace se carga toda la página de nuevo. Para ver este tipo de cambios, tanto la plantilla de la página como el layout de la aplicación generan números aleatorios al cargarse por lo que si los dos números cambian sabremos que se ha producido una recarga completa. Utilizaremos a continuación pjax para actualizar sólo las áreas de la página que tengan que cambiar.
David Heinemeier Hanson ha escrito una gema llamada pjax_rails que hace que sea muy fácil añadir pjax en nuestras aplicaciones Rails 3.1, y aunque no encaja exactamente con lo que necesitamos la usaremos igualmente para ver cómo funciona. Se instala añadiendo la gema pjax_rails a nuestro Gemfile
y ejecutando bundle
.
source 'http://rubygems.org' gem 'rails', '3.1.1' gem 'sqlite3' # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', '~> 3.1.4' gem 'coffee-rails', '~> 3.1.1' gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails' gem 'pjax_rails'
Cuando Bundler haya acabado tenemos que modificar el manifiesto application.js
para que cargue el JavaScript de pjax.
// This is a manifest file that'll be compiled into including all the files listed below. // Add new JavaScript/Coffee code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // the compiled file. // //= require jquery //= require jquery_ujs //= require pjax //= require_tree .
Por último tenemos que envolver la llamada a yield
en el layout con un div
que tenga el atributo data-pjax-container
.
<!DOCTYPE html> <html> <head> <title><%= content_for?(:title) ? content_for(:title) : "Store" %></title> <%= stylesheet_link_tag "application" %> <%= javascript_include_tag "application" %> <%= csrf_meta_tag %> </head> <body> <div id="container"> <div class="layout_time">Layout random: <strong><%= rand(900)+100 %></strong></div> <% flash.each do |name, msg| %> <%= content_tag :div, msg, :id => "flash_#{name}" %> <% end %> <div data-pjax-container> <%= yield %> </div> </div> </body> </html>
Si ahora reiniciamos el servidor y recargamos la página, se activará pjax sobre todos los enlaces de la página. Al hacer clic en uno de ellos la página no se volverá a cargar, lo que podremos ver porque sólo cambia el número aleatorio que genera la plantilla. Esto es casi lo que queremos, pero si miramos con más detalle veremos que el título de la página no está cambiando.
Esto se corrige añadiendo la etiqueta title
en la plantilla. Esto es un poco chapucero, de hecho hace que el HTML de la página no valide, pero al menos funciona.
<% content_for :title, @product ? @product.name : "Products" %> <title><%= yield (:title) %></title> <!-- Rest of page omitted -->
Tras recargar la página veremos que el título cambia cuando se hace clic en uno de los enlaces y que el botón de ir atrás y el histórico del navegador funcionan correctamente. Para el usuario todo funciona igual que antes, sólo que un poco más rápido. Esto está bien, pero la gema pjax_rails
es un poco agresiva, porque activa pjax sobre todos los enlaces que se muestren, lo que no queremos para nuestra aplicación. De la misma forma, vemos que se actualiza siempre toda la plantilla, no habiendo forma de decir qué partes de la página queremos actualizar. Podríamos desactivar pjax para ciertos enlaces, pero ciertamente sería una forma extraña de trabajar. Si tenemos una aplicación muy sencilla cuyo layout es siempre el mismo, es posible que pjax-rails no nos sea muy útil.
La gema rack-pjax de Gert Goet utiliza un middleware de Rack para dar una solución más flexible que pjax_rails. Primero sustituiremos la gema pjax_rails por rack-pjax
en el Gemfile
y ejecutaremos bundle
.
# gem 'pjax_rails' gem 'rack-pjax
Tras lo cual tenemos que añadir el middleware al fichero de configuración de nuestra aplicación.
module Store class Application < Rails::Application config.middleware.use Rack::Pjax # Other config commands omitted end end
Rack-pjax no incorpora un plugin de jQuery, por lo que lo tendremos que instalar por separado. Creemos el directorio vendor/assets/javascripts
y con curl
descargaremos el fichero jquery.pjax.js
.
$ mkdir -p vendor/assets/javascripts $ curl https://raw.github.com/defunkt/jquery-pjax/master/jquery.pjax.js > vendor/assets/javascripts/jquery.pjax.js
Tenemos que incluir este archivo en el fichero de manifiesto de JavaScript de nuestra aplicación por lo que cambiaremos el JavaScript de pjax_rails por este otro:
//= require jquery //= require jquery_ujs //= require jquery.pjax //= require_tree .
Al contrario que pjax_rails, rack-pjax no activa pjax sobre todos los enlaces por defecto, por lo que tenemos que especificar qué enlaces queremos activar. Se trata de de las páginas del controlador ProductsController
, por lo que lo haremos en el fichero CoffeeScript de los productos.
jQuery -> $('.product a').pjax('[data-pjax-container]')
Con este código primero nos aseguramos que el DOM ha terminado de cargar y luego localizamos los enlaces sobre los que queremos que actúe pjax, en nuestro caso se trata de todos los enlaces que cuelgan debajo de la clase product
. Luego llamamos al método pjax
sobre ellos y especificamos qué es lo que queremos actualizar cuando se haga clic en el enlace.
Con rack-pjax ya no es necesario reescribir el elemento title
, por lo que podemos quitarlo de la plantilla index
. Al recargar la página veremos que todo funciona igual que antes; sólo se actualiza la plantilla (y no el layout) al hacer clic en el enlace, porque solo se genera el número aleatorio de la plantilla. Aunque no lo hayamos especificado en la plantilla también se actualiza el título porque éste se detecta desde el layout.
Lo que resulta realmente útil es que ahora ya podemos mover nuestro elemento data-pjax-container
a donde queramos. Ya no tiene que estar colocado alrededor de la llamada yield
en el fichero de layout. Lo moveremos a la plantilla index
y lo pondremos alrededor de la sección que muestra el detalle de un producto.
<% content_for :title, @product ? @product.name : "Products" %> <div class="template_time">Template random: <strong><%= rand(900)+100 %></strong></div> <h1>Products</h1> <% for product in @products %> <div class="product"> <h2> <%= link_to product.name, :product_id => product %> <span class="price"><%= number_to_currency(product.price) %></span> </h2> </div> <% end %> <p><%= link_to "New Product", new_product_path %></p> <div data-pjax-container> <% if @product %> <div id="product_details"> <h3><%= @product.name %></h3> <dl> <dt>Price:</dt> <dd><%= number_to_currency(@product.price) %></dd> <dt>Released:</dt> <dd><%= @product.released_at.strftime("%B %e, %Y") %></dd> <dt>Category:</dt> <dd><%= @product.category %></dd> </dl> <p class="actions"> <%= link_to "Edit", edit_product_path(@product) %> | <%= link_to "Destroy", @product, method: :delete, confirm: "Are you sure?" %> </p> </div> <% end %> </div>
Cuando ahora hagamos clic en un enlace sólo cambiará el panel lateral, y no cambiará ninguno de los números aleatorios.
Los que tengan curiosidad por conocer el funcionamiento de esta gema pueden estudiar su código fuente, que es realmente sencillo y ocupa tan sólo 50 líneas de código en un único fichero Ruby. Hay que tener en cuenta que el servidor sigue mostrando todo el layout y la plantilla, y es rack-pjax quien decide qué partes enviar al cliente. Si por motivos de rendimiento la plantilla resultase costosa de generar deberíamos buscar una solución alternativa, pero para nuestra aplicación será suficiente.
Con esto terminamos este episodio acerca de pjax. Es una solución sencilla que merece la pena contemplar si creemos que se adapta a las necesidades de nuestra aplicación.