Gérer ses dépendances JavaScript avec Google Closure Compiler / Library (2 / 2)

Suite de Gérer ses dépendances JavaScript avec Google Closure Compiler / Library (1 / 2)

Compiler plusieurs fichiers en un seul

Jusque là, on a vu comment ajouter des directives goog.provide(..) et goog.require(…) dans son document, mais il faut maintenant s’en servir. Vous pouvez passer au JAR du compilateur, une liste de fichier JS à assembler avec l’option –js:

java -jar compiler.jar --compilation_level ADVANCED_OPTIMIZATIONS --js file1.js --js file2.js --js file3.js

Il y a plusieurs niveaux de compilation:

  • WHITESPACE_ONLY : remplace uniquement les espaces et les sauts de lignes. Ne modifie pas la code en lui-même
  • SIMPLE_OPTIMIZATIONS : Fait en plus une minification « simple » en renommant les variables locales par des noms plus courts (a, b, c, …). Similaire à ce que font les autres minifiers comme Uglify & co
  • ADVANCED_OPTIMIZATIONS: Niveau « hardcore » qui va aller bien plus loin en renommant aussi les objets globaux. Attention, bien lire la documentation car ce mode est « agressif » et peux casser la compatibilité de votre code si vous n’utilisez pas les mécanismes d’externs / exportSymbol. Ce mode va supprimer le code « mort » (jamais appelé) et permet un taux de compression maximal.

Dans la ligne de commande que j’ai donné plus haut, vous voyez que j’ai explicitement donné les chemins vers les 3 fichiers JavaScript. Ce n’est pas trop laborieux quand il y en a 3 mais quand vous aurez plusieurs dizaines de fichiers, je peux vous assurer que vous n’aurez pas envie d’aller rajouter tout cela à la main. D’autant plus qu’il ne faut pas vous tromper dans l’ordre d’inclusion des fichiers :).

Construire un arbre de dépendances avec les scripts Python fournis

Au lieu de compiler simplement plusieurs fichiers, on va demander aux outils de Google Closure Compiler de nous construire un arbre de dépendances de manière « intelligente ». En effet, grâce aux indication goog.require(…), vous allez pouvoir déterminer quel script doit être présent en premier dans le fichier final.

Pour faire simple, les fichiers qui ont le moins de dépendances vont se trouver en premier dans le fichier. Si com.ns.A dépend de com.ns.B, com.ns.B devra apparaître en premier dans le fichier final.

On a donc à notre disposition un fichier closurebuilder.py:

https://developers.google.com/closure/library/docs/closurebuilder?hl=fr

Attention, pour fonctionner, vous devrez utiliser Python 2.7.2, pas plus!

On va donner à celui-ci plusieurs paramètres:

  • Une liste de « root », c’est-à-dire une liste de répertoire qui seront scannés de manière récursive pour aller chercher vos fichiers source.
  • Un « namespace » qui va être le namespace (d’un goog.provide) dont on va aller chercher les dépendances

Imaginons que vous ayez le fichier suivant dans le répertoire « myproject/start.js »:

goog.provide('myproject.start');

goog.require('goog.dom');

myproject.start = function() {
  var newDiv = goog.dom.createDom('h1', {'style': 'background-color:#EEE'},
    'Hello world!');
  goog.dom.appendChild(document.body, newDiv);
};

// Ensures the symbol will be visible after compiler renaming.
goog.exportSymbol('myproject.start', myproject.start);

Pour calculer les dépendances, vous allez utiliser la ligne de commande suivante:

closure-library/closure/bin/build/closurebuilder.py \
  --root=closure-library/ \
  --root=myproject/ \
  --namespace="myproject.start"

Ici, on donne comme « root » le dossier contenant la librairie Closure et le dossier « myproject » qui contient les sources (ici, un fichier seulement). Voici ce que l’on obtient:

closure-library/closure/bin/build/closurebuilder.py: Scanning paths...
closure-library/closure/bin/build/closurebuilder.py: 596 sources scanned.
closure-library/closure/bin/build/closurebuilder.py: Building dependency tree..
closure-library/closure/goog/base.js
closure-library/closure/goog/debug/error.js
closure-library/closure/goog/string/string.js
closure-library/closure/goog/asserts/asserts.js
closure-library/closure/goog/array/array.js
closure-library/closure/goog/dom/classes.js
closure-library/closure/goog/object/object.js
closure-library/closure/goog/dom/tagname.js
closure-library/closure/goog/useragent/useragent.js
closure-library/closure/goog/math/size.js
closure-library/closure/goog/math/coordinate.js
closure-library/closure/goog/dom/dom.js
myproject/start.js

On a simplement indiqué que l’on avait besoin du namespace « goog.dom » et le script Python est allé scanner ce fichier pour aller voir de manière transitive, de quoi dépendait « goog.dom » pour les ajouter à la liste dans le bon ordre. Par exemple, « goog.dom » dépend de « goog.math.coodinate », qui est donc inclut avant.

De base, vous avez donc généré votre arbre de dépendance que vous pouvez par exemple stocker dans un fichier. Cela peut aussi vous servir pour créer un fichier nommé « deps.js » qui va, en environnement de développement, charger les sources une par une dans la page. C’est plus long mais beaucoup plus facile à debugger.

Mais ce que l’on veut, c’est compiler en un seul JS, on va donc ajouter 2 paramètres:

closure-library/closure/bin/build/closurebuilder.py \
  --root=closure-library/ \
  --root=myproject/ \
  --namespace="myproject.start" \
  --output_mode=compiled \
  --compiler_jar=compiler.jar \
  > myproject/start-compiled.js

On indique qu’on veut le compiler en donnant le chemin vers le JAR de Google Closure Compiler puis avec un « > », on indique que l’on va rediriger la sortie vers le fichier myproject/start-compiled.js.

Par cette étape de compilation, on pourra vous avertir sur des erreurs que l’on aura fait, comme par exemple avoir un goog.require(« ns.SomeClass ») sans un seul fichier qui déclare goog.provide(« ns.SomeClass »). Mais surtout, vous voyez qu’ici, on a plus à faire la liste des dépendances, on a simplement donné une référence vers le namespace qui sera « le plus haut dans l’arbre des dépendances ».

Notez que ce processus peut être facilement intégré à votre système de Build (Maven & co), notamment à des projets comme Plover ou  wro4j.

Le point noir : la gestion des dépendances qui ne sont pas à soi (third-party)

Alors bien sûr, c’est juste superbe, plus besoin de s’occuper de ses dépendances, un système facile à intégrer à son build, des annotations pas trop envahissantes, très bien.

Mais il est très commun d’intégrer dans son code source, des librairies externes comme jQuery. Ces librairies ne comportent pas les annotations goog.require et goog.provide, et si vous compilez en mode « avancé », le Closure Compiler va donc considérer que ce code ne fait rien et éliminer le code mort.

Google a prévu ce cas-là et propose un mécanisme d’externs:

https://developers.google.com/closure/compiler/docs/api-tutorial3#externs

Vous allez en fait dans un fichier séparé, déclarer l’API publique de la librairie pour qu’elle ne soit pas renommée pendant la compilation. Pour vous faciliter un peu la tâche, Google vous propose des externs pour les librairies les plus populaires:

http://code.google.com/p/closure-compiler/source/browse/#svn%2Ftrunk%2Fcontrib%2Fexterns

comme jQuery:

http://code.google.com/p/closure-compiler/source/browse/trunk/contrib/externs/jquery-1.7.js

Cela fonctionne et vous n’aurez pas à modifier les sources de jQuery, juste à préciser ce fichier au moment de la compilation.

Seulement, si vous avez une librairie dont les externs n’ont pas été générés, il ne vous restera plus qu’à créer ce fichier à la main. Certains script de génération auto existent mais cela reste très peu confortable.

Après pas mal d’essais en vain, j’en ai conclu que vous devrez laisser tomber le mode de compilation « avancé » pour revenir au « simple » qui effectue une minification classique, aussi bonne que les autres outils disponibles sur le net.

Faire fonctionner la résolution des dépendances avec des fichiers third-party

Même si vous repassez en mode « simple », vous aurez toujours un problème qui est que vos librairies 3rd-party ne contiennent pas les annotations goog.require() mais surtout goog.provide(). A partir de là, vous avez 2 solutions:

  • Les ajouter en en-tête du fichier en modifiant le fichier de la librairie que vous avez téléchargé. Ce n’est pas très propre car vous venez d’altérer la librairie à la main, donc vous devrez y penser quand vous aurez à mettre à jour la librairie. Cependant, cela peut être une solution simple et rapide si vous savez que vous ne modifiez pas vos dépendances tous les 2 jours.
  • Ecrire les instructions goog.provide() à la volée en Python. Cette technique est expliquée sur ce blog: Learning Closure: managing dependencies and compiling

Conclusion

Comme d’habitude, vous devrez utiliser le bon outil qui va convenir à votre projet. Le principal problème de Google Closure est la gestion des dépendances externes, qui peut rapidement devenir laborieuse, sauf si on prend quelques raccourcis. Mais les autre systèmes de gestion de dépendances ont aussi leur défauts ;).

Dans l’ensemble, le mode de compilation « avancé » sera réservé aux projets « pur Google Closure » ou presque mais la minification simple fait déjà du très bon travail. Pour moi, les points les plus importants sont:

  • Gestion des dépendances automatiques, par analyse du code
  • Syntaxe légère et compréhensible
  • Dépendance vers base.js pas encombrante (ce n’est pas comme si vous étiez obligé d’embarquer tout jQuery)
  • Possibilité d’intégrer cela au système de build

Comme d’habitude, si vous avez un mot à dire, une critique ou un retour d’expérience, n’hésitez pas à utiliser les commentaires!

2 réflexions au sujet de « Gérer ses dépendances JavaScript avec Google Closure Compiler / Library (2 / 2) »

  1. admin Auteur de l’article

    Salut Thomas,
    Oui, j’ai vu cela ce matin, c’est intéressant pour les langages compilés. Pour du JS pur utilisé avec Closure, le mécanisme de require + deps.js permet de débugger tranquille. Plus d’articles sur Closure la semaine prochaine normalement sur ce blog :)

    Fabien

    Répondre

Répondre à ThomasG Annuler la réponse.

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *