Archives pour la catégorie Google Closure

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!

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

JavaScript ne propose pas (encore) de moyen d’organiser son code et notamment de déclarer les dépendances entre les différents fichiers JavaScript de votre projet. Dans un langage compilé comme ActionScript ou Java, vous avez les directives « import pack.sub.MaClasse » qui vous permettent d’indiquer au compilateur qu’une classe requiert une autre classe pour fonctionner.

Si vous avez quelques dizaines de lignes de code JavaScript dans votre projet, vous pouvez toutes les mettre dans un seul fichier ou directement dans le fichier HTML mais quand vous commencez à avoir plusieurs centaines / milliers de lignes de code, vous allez rapidement découper votre code en différent fichiers.

Comme il n’y a encore rien dans le langage pour vous aider, plusieurs librairies / systèmes ont été créés, notamment:

Exemple simple avec RequireJS

Pour en apprendre plus sur RequireJS, une des plus populaires, je vous conseille de lire ce très long et très bon article @mklabs :

RequireJS ➤ mo-du-la-ri-té !

Même si RequireJS est devenu assez « standard » (il y a quelques trolls sur le net pour déterminer le champion des champions), vous pouvez toujours décider de ce que vous allez utiliser.

Vous trouverez plein d’exemples dans l’article cité plus haut mais voici comment on déclare un module requireJS:

//my/shirt.js now has some dependencies, a cart and inventory
//module in the same directory as shirt.js
define(["./cart", "./inventory"], function(cart, inventory) {
        //return an object to define the "my/shirt" module.
        return {
            color: "blue",
            size: "large",
            addToCart: function() {
                inventory.decrement(this);
                cart.add(this);
            }
        }
    }
);

On utilise la méthode globale define (amené par require.js), on lui passe (si nécessaire) en premier argument la liste des dépendances puis une fonction (callback) qui sera appelée lorsque les dépendances auront été chargées. A cette callback, on passe autant d’arguments que de dépendances (dans l’ordre). Ces arguments sont des références vers les dépendances chargées.

Il y a plusieurs manières plus ou moins élégantes de le faire mais il faut que le module renvoie son « interface publique », c’est-à-dire les méthodes qu’il va exposer. Le reste sera « privé » grâce à l’utilisation d’une closure, ici la callback principale.

Ensuite, pour aller chercher la dépendance, vous allez utiliser la méthode globale « require() » qui prend en argument l’identifiant du module JavaScript que vous voulez utiliser:

require("module/name").callSomeFunction()

Voilà, cela fonctionne très bien mais, et c’est très personnel, plusieurs points me chagrinent:

  • La syntaxe qui peut rapidement devenir verbeuse
  • Le fait d’enfermer son module dans une callback permet de ne pas polluer le namespace globale mais pour une application (pas une librairie), je ne trouve pas cela « trop » grave d’arriver avec son namespace
  • Code plus difficile à lire (pour moi) qu’avec une approche simple par prototype « à plat »

Bref, j’ai décidé d’aller voir ailleurs pour enfin tester la méthode de Google Closure

Google Closure en deux mots

Google Closure est une famille de projets soutenus par Google et utilisés par Google dont:

Voici une vidéo du Google IO 2010 qui explique un peu tout:

Ici on va principalement s’intéresser au Compiler pour notre gestion de dépendances. Le compilateur en lui-même se compose d’un JAR mais Google propose aussi d’autres outils en Python pour vous aider à automatiser votre travail.

La Google Closure Library est un ensemble de « classes » JavaScript et de composants que l’on présente souvent comme une alternative à jQuery (alors que ce n’est pas vraiment le cas). Pour notre exemple, on va utiliser seulement un fichier, « base.js » qui contient certaines méthodes qui nous seront utiles pour déclarer nos dépendances.

Un exemple

Le plus simple pour trouver des exemples et d’aller voir dans le code de la librairie Google Closure directement:

http://code.google.com/p/closure-library/source/browse/#svn%2Ftrunk%2Fclosure%2Fgoog

Vous verrez que les fichiers sont très verbeux, avec énormément de commentaires JSDoc qui sont en fait utilisés par le compilateur pour transformer JavaScript en un langage presque typé.

Prenons par exemple le fichier ClickToEditWrapper.js:

http://code.google.com/p/closure-library/source/browse/trunk/closure/goog/editor/clicktoeditwrapper.js

Voici ce que vous allez trouver dans les premières lignes du fichier:

goog.provide('goog.editor.ClickToEditWrapper');

goog.require('goog.Disposable');
goog.require('goog.asserts');
goog.require('goog.debug.Logger');
goog.require('goog.dom');
goog.require('goog.dom.Range');
goog.require('goog.dom.TagName');
goog.require('goog.editor.BrowserFeature');
goog.require('goog.editor.Command');
goog.require('goog.editor.Field.EventType');
goog.require('goog.editor.range');
goog.require('goog.events.BrowserEvent.MouseButton');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventType');

et ensuite, le protoype est enrichi de manière dynamique:

/** @return {goog.editor.Field} The field. */
goog.editor.ClickToEditWrapper.prototype.getFieldObject = function() {
  return this.fieldObj_;
};

/** @return {goog.dom.DomHelper} The dom helper of the uneditable element. */
goog.editor.ClickToEditWrapper.prototype.getOriginalDomHelper = function() {
  return this.originalDomHelper_;
}

Ici, on utilise 2 méthodes qui sont en fait présentes dans le fichier base.js de la Google Closure Library: goog.provide(…) et goog.require(…).

goog.provide(…)

L’instruction goog.provide(…) prend en seul paramètre, un namespace comme « goog.editor.ClickToEditWrapper ». Avec cette instruction, vous indiquez que le fichier qui contient cette directive est celui qui va pouvoir fournir le namespace « goog.editor.ClickToEditWrapper ». C’est un peu comme si vous déclariez le package et le nom de la classe en même temps (le fully-qualified classname pour les intimes).

Notez qu’un fichier peut contenir plusieurs instruction goog.provide(…), en effet, un fichier peut définir plusieurs « classes » JavaScript.

goog.require(…)

Comme son nom l’indique, goog.require(…) indique que la classe en question a une dépendance forte vers un autre namespace. Si vous faîtes un peu de Java / ActionScript 3, cela ressemble aux instructions « import » ou aux fichiers .h en C. Bien entendu, vous pourrez avoir autant de goog.require(…) qu’il en faudra.

Ici, plusieurs points que j’apprécie:

  • La syntaxe est moins intrusive et plus élégante à mon goût (ressemble *très* grossièrement à de l’AS3)
  • Pas de callback à laquelle on passe des références aux dépendances, on utilisera simplement nos objets définis dans nos namespaces.
  • Plus facile à lire car assez linéaire et peut être séparé par sub-package, comme ce que font les formatters de code Java / AS3 (séparer bg.model.xxx de bg.application.xxx)
  • Dépendance au fichier base.js (8Ko minifié, 2.5Ko gzippé + min) qui contient en plus pas mal de « petits trucs » bien pratiques (mixins, typeOf, inherits, …)

La suite dans la seconde partie, où j’expliquerai comment compiler en ligne de commande, puis comment utiliser les scripts Python fournis pour construire son arbre de dépendances.