Turbolinks and Django
Last revision | Oct. 19, 2020 |
Spanish version | click here |
What is Turbolinks?
Turbolinks is a gem included in Ruby on Rails that avoids having to reload the CSS and JavaScript files each time you click on a link in your application, making this feel faster and more agile in the browser.
The way it works is as follows: instead of reloading the page when a link is clicked, Turbolinks intercepts the click event, calls the server via AJAX and replaces the body tag in the page with the response.
With Turbolinks your applications will feel almost as if they were made with a modern JavaScript framework such as Angular or React, but without the complexity that this implies. In addition, this maintaining a cache of the last visited pages, allowing those pages to be displayed immediately without making a call to the server.
How do we use it in our Django app?
In comparison with the RoR gem, in Django (so far) we have not found a package that works or integrates into the assets pipeline as the version of Ruby on Rails does. Although, since version 5 of Turbolinks this was completely rewritten using TypeScript to simplify the API, review the philosophical decisions of the past and cut some technical luggage; it can now be used without the need for your application to be written in RoR, making use of the standalone version, which you can install using your preferred JavaScript package manager, as explained in its documentation.
That said, the standalone version is what we're going to use in our Django application, so first let's do, is add Turbolinks to our main template.
{% load static %}
<head>
<title>Django + Turbolinks</title>
...
<script src="{% static 'js/turbolinks.js' %}></script>
...
</head>
You can download it using CDNJS and include it in the assets of your application; if you notice, we have loaded it in the header and not at the end of <body>
, this is because if we leave it in the end, it will be loaded in each change of the application.
If you use the template blocks in Django to render certain scripts in certain pages, it is better to avoid or stop using that method and move everything to the header, to ensure the optimal functioning of Turbolinks
*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 %}
Start up
Now we will see how to combine everything to launch our application with Turbolinks.
Fingerprint
Turbolinks track the URLs of the static files that you have in the header of your document, that's why it is advisable to add a unique identifier to your urls, which is usually generated with each new deploy.
<head>
…
<link rel=“stylesheet” href=“/application-258e88d.css” />
<script src=“/application-cbd3cd4.js”></script>
</head>
You can use Django Pipelines for this process and, in turn, get a better organization of the assets in your application.
Idempotency
If you have read the documentation, you will have noticed the events that Turbolinks adds to your application, one of these is the turbolinks: load
, well if you moved all the initializers within this, you will notice that the events are duplicated, you can use this plugin rstacruz/onmount to avoid this kind of errors.
$.onmount(
‘.push-button’,
function() {
$(this).on('click', doSomething)
},
function() {
$(this).off('click', doSomething)
}
);
In this way, every time the element exists in the rendered DOM, we add the click event to it, and when it disappears, we deactivate it, a good tip is to invoke onmount
every time Turbolinks loads a page.
$(document).on('ready turbolinks:load', function() {
$.onmount();
});
Async - Defer
Since all the assets must be in the header of the application, you do not want to add more time to the user and damage the experience of this, that is why you must consider what is important to load first and what could wait, for this use the async
or defer
attributes as appropriate.
No-Cache
If you have a page that requires you to always have the most recent version of its content, use the following tag to avoid it being added to the Turbolinks cache.
<meta name="turbolinks-cache-control" content="no-cache">
Final
This is how the final app will looks like.
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'
},
...
As you can notice plugins
is loaded using defer because I do not need them to be available before the whole document finishes being rendered, in comparison with editor
which uses async since I need it to be available before the document ends and it will be initialized within app
.
tag head
<head>
<meta charset="utf-8" />
<title>{% block title %}{% endblock %}</title>
...
{% stylesheet "theme" %}
{% javascript "editor" %}
{% javascript "turbolinks" %}
{% javascript "app" %}
{% javascript "plugins" %}
</head>
All the static files of the application are loaded in the header and instead of using the {% static '<path>'%}
template tag, we use the builtins Django-Pipelines
tags, which add the options to have the fingerprint and ship the minified version in the 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();
});
And we finish initializing onmount each time Turbolinks loads a page, so that the plugins and/or events are activated according to the elements that have been rendered.
Conclusions
I hope this post has been useful and save you a few hours of searching the internet, this way has worked for us, to give to our users a better experience in our application, without having to separate the Back-End from the Front-End, or re-write it using modern libraries.
Notes
- Always check the way in which Turbolinks renders a view that has already been saved in the cache, since it could be that some plugins (tooltips for example) have been initialized.
- If you have the opportunity and the time, it is better to divide your javascript into chunks and write it in a more modular or modern way.
-
Being B your final point or view, always test it according to these four ways:
- From A to B.
- From A to B, then from B to A, and again from A to B.
- Only B.
- Only B, then reload B.
- Leave the performance tests to the end.
- You can experiment with Stimulus.