Turbolinks & Django

Jul 01, 2019

¿Que es Turbolinks?

Turbolinks es una gema incluida en Ruby on Rails que evita tener que recargar los archivos CSS y JavaScript cada vez que oprimes un link en tu aplicación, haciendo que las aplicaciones se sientan más rápidas y ágiles en el navegador.

La forma en que funciona es la siguiente: en vez de recargar la página cuando se oprime un link, Turbolinks intercepta el evento click, hace un llamado AJAX al servidor y reemplaza el cuerpo (body) de la página con la respuesta.

Con Turbolinks tus aplicaciones se van a sentir casi como si estuviesen hechas con un framework JavaScript como Angular o React, pero sin la complejidad que eso implica. Además de mantener un caché de las últimas páginas visitadas y eso permite que esas páginas se puedan mostrar de forma inmediata sin hacer un llamado al servidor.

¿Cómo lo usamos en nuestras aplicaciones Django?

En comparación con la gema de RoR en Django (hasta el momento) no hemos encontrado una libreria que funcione o se integre en el assets pipeline como lo hace la version de Rails. Aunque, desde la version 5 de Turbolinks este fue reescrito por completo usando TypeScript para simplificar la API, revisar las decisiones filosóficas del pasado y recortar algunos equipajes técnicos; es por ello que ahora puede ser usado sin necesidad de que tu aplicación este escrita en RoR, haciendo uso de la version standalone la cual puedes instalar utilizando tu manejador de paquetes de JavaScript preferido, a cómo se explica en su documentación.

Dicho esto, la version standalone es lo que vamos a usar en nuestra aplicación en Django, así que primero vamos a hacer es agregar Turbolinks a nuestra plantilla principal.

{% load static %}
<head>
  <title>Django + Turbolinks</title>
  ...
  <script src="{% static 'js/turbolinks.js' %}></script>
  ...
</head>

En este caso, estamos cargando el script Turbolinks, utilizando la administración normal de estaticos de Django, esto funcionará, pero más adelante explicare por qué esta no es la forma recomendada de cargarlo.

Puedes descargar la version standalone usando CDNJS e incluirlo en los assets de tu aplicación; si notas, lo hemos cargado en la cabecera y no al final de <body>, esto debido a que si lo dejamos en el final, sera recargado en cada cambio de la aplicación.

Si usas los bloques de las plantillas en Django para renderizar ciertos scripts en ciertas paginas es mejor que dejes de usar ese método y muevas todo a la cabecera, para asegurar el optimo funcionamiento de Turbolinks, mira el siguiente ejemplo

*base.html*
<html>
    <head>
        <title>Turbolinks + Django</title>
    </head>
    <body>
        <main>
            {% block main %}
            {% endblock main %}
        </main>
        {% block javascripts %}
        {% endblock javascripts %}
    </body>
</html>
*main.html*
{% extendeds 'base.html' %}
{% load static %}

{% block main %}
    <h1>Hi!</h1>
{% endblock main %}

{% block javascripts %}
<script src="{% static 'js/cool_script.js' %}"></script>
{% endblock javascripts %}

La razón por la que deberíamos evitar esto es que Turbolinks, como mencione anteriormente, funciona cambiando el <body> al cambiar de página, y al hacerlo volverá a ejecutar todos sus scripts (imagínese que jQuery se está reevaluando), lo cual puede que no se lo que quieras.


Puesta en marcha

Ahora veremos cómo combinar todo para poner en marcha nuestra aplicación, siguiendo unos cuantos pasos y recomendaciones.

Fingerprint

Turbolinks hace track de las URLs de los archivos estáticos que tienes en la cabecera de tu documento, es por eso que es recomendable agregar un identificador único a tus urls, que por lo general, es generado con cada nuevo deploy.

<head><link rel=“stylesheet” href=“/application-258e88d.css” />
  <script src=“/application-cbd3cd4.js”></script>
</head>

Puedes auxiliarte de Django Pipelines para este proceso y de paso organizar mejor los assets de tu aplicación.

Idempotencia

Si has leído la documentación, habrás notado los eventos que agrega Turbolinks a tu aplicación, uno de estos es el turbolinks:load, pues bien si moviste todos los inicializadores dentro de esto, notaras que se te duplican los eventos, puedes hacer uso de este plugin rstacruz/onmount para evitar este tipo de errores.

$.onmount(.push-button’,
  function() {
    $(this).on('click', doSomething)
  },
  function() {
    $(this).off('click', doSomething)
  }
);

De esta forma, cada vez que el elemento existe en el DOM renderizado, le agregamos el evento click y cuando desaparece, le desactivamos, un buen tip es invocar onmount cada vez que Turbolinks carga una pagina.

$(document).on('ready turbolinks:load', function() {
  $.onmount();
});

Async - Defer

Dado que todos los assets deben de estar en la cabecera de la aplicación, no querrás agregarle más tiempo al usuario y dañar la experiencia de este, es por ello que debes de considerar que es importante cargar primero y que puede esperar, para ello utiliza los atributos async o defer según sea conveniente.

Defer vs Async

No-Cache

Si tienes alguna pagina que requiera tener siempre la version mas reciente de su contenido, utiliza la siguiente etiqueta para evitar que sea agregada a la cache de Turbolinks

<meta name=“turbolinks-cache-control” content=“no-cache” />

Final

Así seria el ejemplo final de la aplicación.

settings.py

 444   | PIPELINE = {
 445   |    'JAVASCRIPT': {
 446'plugins': {
 447'source_filenames': (
 448'js/plugins/file-upload.js',
 449'js/plugins/jquery.scrollbar.js',
 450),
 451'extra_context': {'defer': True},
 452'output_filename': 'js/plugins.js',
 453},
 454'editor': {
 455'source_filenames': (
 456'js/editor/editor-deps.js',
 457'js/editor/editor.js',
 458),
 459'extra_context': {'async': True},
 460'output_filename': 'js/editor.js'
 461},
 462'app': {
 463'source_filenames': (
 464'js/application.js',
 465),
 466'output_filename': 'js/app.js'
 467},

A como puedes notar plugins es cargado de forma defer por que no necesito que estén disponibles antes de que todo el documento termine de ser renderizado, en comparación con editor el cual es async puesto que necesito que este disponible antes de que el documento termine y que sera inicializado dentro de app.

<head>

  10   │   <head>
  11   │     <meta charset="utf-8" />
  12   │     <title>{% block title %}{% endblock %}</title>
  13   |     ...
  18   │     {% stylesheet "theme" %}
  19   │     {% javascript "editor" %}
  20   │     {% javascript "turbolinks" %}
  21   │     {% javascript "app" %}
  22   │     {% javascript "plugins" %}
  23   │   </head>

Todos los estáticos de la aplicación son cargados en la cabecera y en vez de usar el template tag {% static '<path>' %} utilizamos los tags de Django-Pipelines para que tengan el fingerprint y sean minificados en el deploy.

App.js

   2$(document).on('turbolinks:load', function() {
   3...
  11   │   $.App.init();
  12...
  30   │   $.onmount();
  31});
  32...
 228/* Enable footable plugin */
 229   │ $.onmount('.footable', function () {
 230$(this).footable();
 231});

Y finalizamos inicializando onmount cada vez que Turbolinks cargue una pagina, para que se activen los plugins y/o eventos según los elementos que se han renderizado.

Concluciones

Espero que este post te haya servido de utilidad y te ahorre unas cuantas horas de búsqueda en el internet, esta forma nos ha funcionado para mejorar la experiencia de nuestra aplicación, sin necesidad de tener que separar el Back-End de el Front-End.

Notas

  • Revisa siempre la forma en como Turbolinks renderiza una vista que ya ha sido guardada en la cache, puesto que pueda ser que algunos plugins (tooltips por ejemplo) se hayan quedado inicializados
  • Si tienes la oportunidad y el tiempo, es mejor que dividas tu javascript en chunks y lo escribas de una forma mas modular o moderna.
  • Siendo B tu punto o vista final, siempre pruebalo según estas cuatro formas:

    • De A hacia B.
    • De A hacia B, luego de B hacia A y otra vez de A hacia B.
    • Solo B.
    • Solo B y recargar B.
  • Deja las pruebas de rendimiento para el final.
  • Puedes experimentar con Stimulus para aumentar tu HTML sin complicaciones