JavaScript avec Django

Django est initialement conçu pour réaliser des sites et applications web avec le pattern MVT (Modèle-Vue-Template). Loin de moi l’idée de faire ici un cours sur le design pattern ou de relancer l’éternel débat MVC-MVT-… Le point que je veux soulever est que Django est conçu pour gérer la majorité de la cinématique utilisateur côté serveur.

Le problème est qu’aujourd’hui les techniques évoluent et nous sommes amenés à déplacer une bonne partie de cette cinématique côté client, dans le navigateur, avec notamment l’utilisation du JavaScript.

Cette évolution est très bénéfique car elle a (normalement) les conséquences suivantes:

  • réduction de la charge serveur
  • réduction du volume des échanges client-serveur
  • amélioration de la gestion du cache
  • amélioration de l’expérience utilisateur
  • retour massif vers du stateless et les fondamentaux du web

J’en oublie probablement, mais l’essentiel est de comprendre que cela modifie profondement les échanges client-serveur et, bien maitrisé, améliore globalement le fonctionnement d’un site ou d’une application.

Pour nous, utilisateurs de Django, cela pose quelques nouvelles problèmatiques comme:

  • comment gérer proprement les dépendances JavaScript ?
  • comment éviter de dupliquer trop de code (gestion des URLs, templates, localisation, …) ?
  • comment tester le code JavaScript ?
  • comment optimiser le code client (JavaScript, CSS, …) ?

Je n’ai pas la prétention de pouvoir fournir une réponse universelle et exhaustive à toutes ces problématiques, mais je peux fournir quelques recettes que j’utilise.

Éviter la duplication des mécaniques de Django

Django fournis certains mécanismes relativement pratiques et, dans la philosophie DRY, il est intéressant d’en profiter proprement côté client dans le code JavaScript.

C’est là qu’intervient Django.js qui fourni les outils suivants (entre autres):

  • Résolution des URLs
  • Accès à certaines variables du context
  • Facilitation de l’internationalisation
  • Facilitation de l’utilisation de jQuery (CSRF)
  • Facilitation de l’écriture de templates (templates tags js, css, …)

Ainsi, Django.js permet d’écrire:

  • template.html:

    {% load js %}
    {% django_js %}
    {% js "js/m-lib.js" %}
    
  • my-lib.js:

    $(function() {
        console.log(
            Django.url('my-url', arg1),
            Django.context.STATIC_URL,
            Django.context.LANGUAGE_CODE,
            Django.static('test.json'),
            Django.user.username,
            gettext('a localized text')
        );
    });
    

J’utilise cette libraire de mon cru sur plusieurs projets, professionnels et personnels, depuis plus d’un an maintenant et cela me simplifie considérablement le développement JavaScript avec Django. Je la maintient activement et essaie de la faire évoluer pour couvrir un maximum de problématiques récurrentes dans l’utilisation de JavaScript avec Django.

Gerer ses dépendances JavaScript

Fini l’installation manuelle des dépendances JavaScript, maintenant il y a Bower ! (entres autres)

Comme toutes les executables JavaScript fournies pour Node.js, je l’installe globalement avec npm:

sudo npm install -g bower

Plutôt que de distribuer les dépendances JavaScript dans chacun de mes projets, j’utilise Bower et je distribue 2 fichiers:

  • un fichier .bowerrc spécifiant où télécharger les dépendances
  • un fichier bower.json (anciennement component.json) spécifiant la liste des dépendances.

Pour un projet agencé comme suit:

├─ myproject
│  ├─ myapp
│  │  ├─ __init__.py
│  │  └─ models.py
│  ├─ __init__.py
│  ├─ static
│  │  ├─ bower
│  │  ├─ css
│  │  ├─ images
│  │  ├─ js
│  │  └─ less
│  ├─ settings.py
│  ├─ urls.py
│  └─ wsgy.py
├─ .bowerrc
├─ bower.json
├─ manage.py
├─ MANIFEST.in
└─ setup.pip

J’indique à Bower d’utiliser le repertoire myproject/static/bower pour télécharger les dépendances. Je crée donc le fichier .bowerrc comme suit:

{
    "directory": "./myproject/static/bower/"
}

Et je n’ai plus qu’à utiliser Bower normalement:

$ bower install --save jquery bootstrap font-awesome moment handlebars

Toutes mes dépendances sont automatiquement ajoutées au fichier bower.json qui est créé automatiquement s’il n’existait pas:

{
    "name": "myproject",
    "version": "1.0.0",
    "dependencies": {
        "jquery": "2.0.2",
        "handlebars": "~1.0.0",
        "bootstrap": "~2.3.2",
        "font-awesome": "~3.2.0",
        "moment": "~2.0.0"
    }
}

Mes ressources sont accessibles sous {STATIC_URL}/bower/.

Toutes les opérations de Bower fonctionne normalement:

# Installation de dépendances
$ bower install --save jquery
# Installation de toutes les dépendances listées
$ bower install
# Mise à jour de toutes les dépendances
$ bower update
# Recherche
$ bower search

Je vous invite à lire la documentation de Bower pour plus d’informations.

Avec cette méthode je ne distribue plus les dépendances JavaScript avec les sources, je les liste avec leur version, comme je le ferait pour les dépendances Python avec un fichier de requirements pour pip.

Le seul point noir de cette méthode est qu’il ne faut surtout pas inclure tout le répertoire myproject/static/bower au packaging de l’application Django sous peine d’ajouter quelques dizaines voir centaines de megaoctets pour rien. Il faut lister explicitement les fichiers utilisés pour chaque dépendance dans le fichier MANIFEST.in:

include setup.py README.rst MANIFEST.in

recursive-include myproject *
recursive-include requirements *

prune myproject/static/bower
recursive-include myproject/static/components/bootstrap *.less
recursive-include myproject/static/components/bootstrap/js *.js
recursive-include myproject/static/components/font-awesome/less *.less
recursive-include myproject/static/components/font-awesome/font *
include myproject/static/components/handlebars/handlebars.js
recursive-include myproject/static/components/jquery *.js
recursive-include myproject/static/components/moment/min *.js

global-exclude *~ *.egg *.pyc

De plus, cela évitera de polluer votre répertoire STATIC_ROOT lors du collectstatic, en particulier si vous avez des post-processors type compresseur CSS/JS (cf. Optimiser ses livrables).

Si vous voulez maîtriser vos dépendances à l’exécution, il faut regarder du côté de RequireJS (par exemple), mais je ne détaillerais pas ici son usage.

Tester son code JavaScript

Django.js fourni des outils permettant de faciliter l’exécution de tests JavaScript notamment en utilisant LiveServerTestCase, introduit par Django 1.4, qui permet de démarrer un serveur de test pour chaque exécution de test.

Il fourni:

  • des vues pour afficher ses tests Jasmine ou QUnit
  • des classes de test et mixins pour automatiser leur exécution avec une gestion d’erreur

Voici un exemple permettant d’afficher un runner de test Jasmine sur l’url /tests en mode DEBUG ou pendant les tests.

  • myproject/views.py

    from djangojs.views import JasmineView
    
    class JsTestsView(JasmineView):
        js_files = (
            'js/lib/my-lib.js',
            'js/test/*.specs.js',
            'js/other/specs.*.js',
        )
    
  • myproject/urls.py

    if settings.DEBUG or settings.TESTING:
        from myproject.views import JsTestsView
    
        urlpatterns += patterns('',
            url(r'^tests$', JsTestsView.as_view(), name='js-tests'),
        )
    

La classe JasmineView fourni de base un template de runner et va injecter elle même tous les fichiers *.js spécifiés. Il est possible d’étendre ce template, voir d’utiliser son propre template.

Maintenant, pour que ces tests soient exécutés en même temps que les autres tests de l’application, il suffit d’utiliser la classe JsTestCase et le mixin JasmineSuite:

from djangojs.runners import JsTestCase
from djangojs.runners import JasmineSuite


class JasminTests(JasmineSuite, JsTestCase):
    title = 'My Javascript test suite'
    url_name = 'js-tests'

La suite de tests JavaScript sera executant comme les autres par la commande:

$ python manage.py test myproject

Cette fonctionnalité utilise PhantomJS qui doit être installé au préalable.

Pour l’instant, Django.js s’interface avec Jasmine et QUnit uniquement mais d’autres frameworks feront leur apparition.

Cette solution fonctionne très bien, mais il est possible que vous ayez des besoins bien plus poussés en JavaScript. Dans ce cas là, ne pas réinventer la roue: il faut peut-être se tourner vers les outils JavaScript existants, tels que Grunt.

Optimiser ses livrables

Faire du JavaScript pour fournir une meilleur expérience utilisateur c’est bien beau, encore faut-il que l’utilisateur puisse charger tous vos livrables en un temps acceptable.

C’est là qu’interviennent django-require et django-pipeline pour compiler et compresser JavaScript, Less, SassCSS

django-pipeline vous permet de tout compiler et compresser en “bundles” en les déclarant dans votre fichier settings.py.

django-require quant à lui vous servira si vous utilisez RequireJS.

Personnellement, je combine les deux:

  • django-pipeline pour:
    • compiler le Less en CSS
    • agréger et compresser les CSS
    • aggréger et compresser le javascript non chargé par RequireJS
    • compiler et injecter mes templates (Handlebars) dans les vues
  • django-require pour créer des bundles avec AlmondJS.

django-pipeline

J’utilise django-pipeline pour gérer tout ce qui est pré-compilation (Less), compilation, compression…

Par défaut, django-pipeline utilise yuglify pour la compression des CSS et du JavaScript et Less pour la compilation des fichiers .less. Tous deux sont installables par npm:

$ sudo npm install -g less yuglify

Il est possible d’utiliser SlimIt pour compresser le JavaScript. Il est écrit en Python et a de très bon taux de compression. Il est installable simplement avec pip:

$ pip install slimit

Voici un exemple de configuration de django-pipeline dans un fichier settings.py:

STATICFILES_STORAGE = 'pipeline.storage.PipelineStorage'

PIPELINE_COMPILERS = (
    'pipeline.compilers.less.LessCompiler',
)

PIPELINE_CSS = {
    'style': {
        'source_filenames': (
            'less/style.less',
            'bower/a-component/styles.css',
        ),
        'output_filename': 'css/style.min.css',
    },
}

PIPELINE_JS_COMPRESSOR = 'pipeline.compressors.slimit.SlimItCompressor'

PIPELINE_JS = {
    'modernizr': {
        'source_filenames': (
            'bower/modernizr/modernizr.js',
            'bower/respond/respond.src.js',
        ),
        'output_filename': 'bower/modernizr.min.js',
    },
    'my-lib': {
        'source_filenames': (
            'bower/jquery/jquery.js',
            'js/my-lib.js',
        ),
        'output_filename': 'js/my-lib.min.js',
    },
}

Dans cette configuration, nous utilisons:

  • Less pour compiler nos feuilles de style
  • yuglify (par défaut) pour compresser le CSS (y compris celui produit par Less)
  • SlimIt pour compresser le JavaScript

Nous aurons dans nos templates les valeurs suivantes pour accéder aux fichiers produits par django-pipeline:

{% compressed_css 'style' %}
{% compressed_js 'modernizr' %}
{% compressed_js 'my-lib' %}

En mode debug, nous aurons accès aux fichiers non compressés, tandis qu’en production nous n’aurons plus qu’un seul fichier compressé pour chaque bundle.

Couplé avec Bower, cela permet de travailler facilement avec des versions non compressées.

django-require

django-require permet de compresser ses modules RequireJS avec RequireJS Optimizer ou bien AlmondJS.

Considérons un projet avec deux modules, main.js et main-lite.js:

└─ myproject
   ├─ __init__.py
   ├─ static
   │  ├─ bower
   │  ├─ css
   │  ├─ images
   │  ├─ js
   │  │  ├─ main.js
   │  │  └─ main-lite.js
   │  └─ less
   ├─ settings.py
   ├─ urls.py
   └─ wsgy.py

Pour compresser main.js et main-lite.js en bundles autonomes avec AlmondJS, nous aurons la configuration suivante:

STATICFILES_STORAGE = "require.storage.OptimizedStaticFilesStorage"

REQUIRE_BASE_URL = "js"
REQUIRE_BUILD_PROFILE = "app.build.js"
REQUIRE_JS = "../bower/requirejs/require.js"
REQUIRE_ENVIRONMENT = "node"

REQUIRE_STANDALONE_MODULES = {
    "main": {
        "out": "main.min.js",
        "build_profile": "app.build.js",
    },
    "main-lite": {
        "out": "main-lite.min.js",
        "build_profile": "app.build.js",
    },
}

Dans cette exemple, nous utilisons:

  • l’URL STATIC_URL/js comme base
  • Node.js comme environnement de build
  • une configuration commune app.build.js
  • une version de RequireJS installée par Bower

Nous aurons accès à ces modules dans nos templates par:

{% require_module 'main' %}

En mode debug, ils seront chargés (ainsi que leurs dépendances) de façon asynchrone par RequireJS tandis qu’en production un fichier unique compressé sera créé par AlmondJS.

Combiner les deux

Il est possible de combiner django-pipeline et django-require facilement en créant notre propre classe de storage en utilisant les mixins:

from django.contrib.staticfiles.storage import CachedFilesMixin

from pipeline.storage import PipelineStorage
from require.storage import OptimizedFilesMixin


class MyStorage(OptimizedFilesMixin, PipelineStorage):
    pass

Nous n’avons plus qu’à déclarer cette classe dans notre fichier settings.py:

STATICFILES_STORAGE = "myproject.storage.MyStorage"

Je ne suis volontairement pas rentré dans les détails sur Django.js, django-pipeline et django-require car il y a beaucoup à dire à leur sujet et ils feront l’objet d’articles dédiés par la suite.