Turbolinks y Django
Ultima revision | Oct. 19, 2020 |
Version en ingles | click aquí |
¿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 la navegación se sienta más rápida y agíl en el navegador. Este funciona de la siguiente forma: 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, que permite que estas páginas se puedan mostrar de casi 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 he 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 donde fue reescrito por completo usando TypeScript para simplificar la API; lo que significa que puede ser usado sin necesidad de que tu aplicación este escrita en RoR, a esta version se le conoce como standalone y se puede descargar usando CDNJS e incluirlo en los assets de tu aplicación.
Lo primero que vamos a hacer es agregar Turbolinks a nuestra plantilla principal (base.html).
{% load static %}
<head>
<title>Django + Turbolinks</title>
...
<script src="{% static 'js/turbolinks.js' %}></script>
...
</head>
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.
NOTA: 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.
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 mencioné anteriormente, reemplaza 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 sea 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.
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
PIPELINE = {
'JAVASCRIPT': {
'plugins': {
'source_filenames': (
'js/plugins/plugin-a.js',
'js/plugins/plugin-b.js',
),
'extra_context': {'defer': True},
'output_filename': 'js/plugins.js',
},
'editor': {
'source_filenames': (
'js/editor/editor-deps.js',
'js/editor/editor.js',
),
'extra_context': {'async': True},
'output_filename': 'js/editor.js'
},
'app': {
'source_filenames': (
'js/application.js',
),
'output_filename': 'js/app.js'
},
...
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
.
Tag <head>
<head>
<meta charset="utf-8" />
<title>{% block title %}{% endblock %}</title>
...
{% stylesheet "theme" %}
{% javascript "editor" %}
{% javascript "turbolinks" %}
{% javascript "app" %}
{% javascript "plugins" %}
</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.
Middleware
# Taken from: https://github.com/dgladkov/django-turbolinks/blob/master/turbolinks/middleware.py#L11 # NOQA
def same_origin(current_uri, redirect_uri):
a = urlparse(current_uri)
if not a.scheme:
return True
b = urlparse(redirect_uri)
return (a.scheme, a.hostname, a.port) == (b.scheme, b.hostname, b.port)
class TurbolinksMiddleware:
''' Send the `Turbolinks-Location` header in response to a visit
that was redirected, and Turbolinks will replace the browser’s topmost
history entry.
Taken from: https://github.com/viewflow/django-material/blob/v2/material/middleware.py#L38 # NOQA
Note: This is needed to handle redirects with TurboLinks.
'''
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
turbolinks_referrer = request.META.get('HTTP_TURBOLINKS_REFERRER')
is_response_redirect = response.has_header('Location')
if turbolinks_referrer:
if is_response_redirect:
location = response['Location']
prev_location = request.session.pop(
'_turbolinks_redirect_to', None
)
if prev_location is not None:
# relative subsequent redirect
if location.startswith('.'):
location = prev_location.split('?')[0] + location
request.session['_turbolinks_redirect_to'] = location
# cross domain blocker
if not same_origin(location, turbolinks_referrer):
return HttpResponseForbidden()
else:
if request.session.get('_turbolinks_redirect_to'):
location = request.session.pop('_turbolinks_redirect_to')
response['Turbolinks-Location'] = location
return response
App.js
$(document).on('turbolinks:load', function() {
...
$.App.init();
...
$.onmount();
});
...
/* Enable plugin a */
$.onmount('.my-plugin', function () {
$(this).plugin_a();
});
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, puede 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.