Présentation des mappages source JavaScript

Avez-vous déjà souhaité pouvoir conserver votre code côté client lisible et, surtout, débogable même après l'avoir combiné et minimisé, sans affecter les performances ? Vous pouvez désormais le faire grâce à la magie des cartes sources.

Les mappages source permettent de mapper un fichier combiné/minimisé vers un état non compilé. Lorsque vous effectuez une compilation pour la production, en plus de réduire et de combiner vos fichiers JavaScript, vous générez une carte de source qui contient des informations sur vos fichiers d'origine. Lorsque vous interrogez un certain numéro de ligne et de colonne dans votre code JavaScript généré, vous pouvez effectuer une recherche dans la carte source, qui renvoie l'emplacement d'origine. Les outils de développement (actuellement les builds nocturnes WebKit, Google Chrome ou Firefox 23 et versions ultérieures) peuvent analyser automatiquement la carte source et donner l'impression que vous exécutez des fichiers non minifiés et non combinés.

La démonstration vous permet de faire un clic droit n'importe où dans le champ de texte contenant la source générée. Si vous sélectionnez "Obtenir l'emplacement d'origine", la carte source sera interrogée en transmettant le numéro de ligne et de colonne générés, et la position dans le code d'origine sera renvoyée. Assurez-vous que votre console est ouverte pour pouvoir voir la sortie.

Exemple d'utilisation de la bibliothèque de cartes sources JavaScript de Mozilla.

Monde réel

Avant de consulter l'implémentation concrète suivante des cartes sources, assurez-vous d'avoir activé la fonctionnalité de cartes sources dans Chrome Canary ou WebKit nightly en cliquant sur la roue dentée des paramètres dans le panneau des outils pour les développeurs, puis en cochant l'option "Activer les cartes sources".

Activer les cartes sources dans les outils de développement WebKit

Les cartes sources sont activées par défaut dans les outils de développement intégrés de Firefox 23 et versions ultérieures.

Activer les cartes sources dans les outils de développement Firefox

Pourquoi les mappages source sont-ils importants ?

Pour le moment, le mappage de source ne fonctionne que entre le code JavaScript non compressé/combiné et le code JavaScript compressé/non combiné. Toutefois, l'avenir s'annonce prometteur avec les discussions sur les langages compilés en JavaScript tels que CoffeeScript, et même la possibilité d'ajouter la prise en charge des préprocesseurs CSS tels que SASS ou LESS.

À l'avenir, nous pourrons facilement utiliser presque toutes les langues comme si elles étaient prises en charge nativement dans le navigateur avec des cartes sources:

  • CoffeeScript
  • ECMAScript 6 et versions ultérieures
  • SASS/LESS et autres
  • Presque tous les langages qui se compilent en JavaScript

Regardez cette vidéo montrant le débogage de CoffeeScript dans une version expérimentale de la console Firefox:

Google Web Toolkit (GWT) a récemment ajouté la prise en charge des cartes sources. Ray Cromwell de l'équipe GWT a réalisé une excellente vidéo montrant la prise en charge des cartes sources en action.

Un autre exemple que j'ai créé utilise la bibliothèque Traceur de Google, qui vous permet d'écrire du code ES6 (ECMAScript 6 ou Next) et de le compiler en code compatible avec ES3. Le compilateur Traceur génère également une carte source. Regardez cette démonstration de traits et de classes ES6 utilisés comme s'ils étaient compatibles en mode natif dans le navigateur, grâce à la carte source.

Le champ de texte de la démonstration vous permet également d'écrire du code ES6 qui sera compilé instantanément et générera une carte source ainsi que le code ES3 équivalent.

Débogage Traceur ES6 à l'aide de mappages sources.

Démo: Écrire du code ES6, le déboguer et voir le mappage de source en action

Comment fonctionne la carte de source ?

Le seul compilateur/minificateur JavaScript compatible avec la génération de mappe de sources pour le moment est le compilateur Closure. (Je vous expliquerai comment l'utiliser plus tard.) Une fois que vous avez combiné et minifié votre code JavaScript, un fichier de mappage source est créé à côté.

Actuellement, le compilateur Closure n'ajoute pas le commentaire spécial à la fin qui est nécessaire pour indiquer aux outils de développement d'un navigateur qu'une mappe source est disponible:

//# sourceMappingURL=/path/to/file.js.map

Cela permet aux outils de développement de faire correspondre les appels à leur emplacement dans les fichiers sources d'origine. Auparavant, le pragma de commentaire était //@, mais en raison de problèmes liés à celui-ci et aux commentaires de compilation conditionnelle IE, nous avons décidé de le remplacer par //#. Actuellement, Chrome Canary, WebKit Nightly et Firefox 24 et versions ultérieures sont compatibles avec le nouveau pragma de commentaire. Cette modification de syntaxe affecte également sourceURL.

Si vous n'aimez pas l'idée de ce commentaire étrange, vous pouvez également définir un en-tête spécial dans votre fichier JavaScript compilé:

X-SourceMap: /path/to/file.js.map

Comme le commentaire, cela indiquera à votre consommateur de mappage source où rechercher le mappage source associé à un fichier JavaScript. Cet en-tête permet également de contourner le problème de référencement des cartes sources dans les langues qui ne sont pas compatibles avec les commentaires sur une seule ligne.

Exemple d'activation et de désactivation des mappages source dans les outils de développement WebKit.

Le fichier de mappage source n'est téléchargé que si vous avez activé les mappages sources et que vos outils de développement sont ouverts. Vous devrez également importer vos fichiers d'origine afin que les outils de développement puissent les référencer et les afficher si nécessaire.

Comment générer une carte de sources ?

Vous devez utiliser le compilateur Closure pour minifier, concaténer et générer une carte source pour vos fichiers JavaScript. La commande est la suivante:

java -jar compiler.jar \
--js script.js \
--create_source_map ./script-min.js.map \
--source_map_format=V3 \
--js_output_file script-min.js

Les deux options de commande importantes sont --create_source_map et --source_map_format. Cette opération est nécessaire, car la version par défaut est la version 2, et nous ne souhaitons travailler qu'avec la version 3.

Anatomie d'une carte source

Pour mieux comprendre un fichier de mappage source, nous allons prendre un petit exemple de fichier de mappage source qui serait généré par le compilateur Closure et nous allons examiner plus en détail le fonctionnement de la section "mappings". L'exemple suivant est une légère variation de l'exemple de la spécification V3.

{
    version : 3,
    file: "out.js",
    sourceRoot : "",
    sources: ["foo.js", "bar.js"],
    names: ["src", "maps", "are", "fun"],
    mappings: "AAgBC,SAAQ,CAAEA"
}

Vous pouvez voir ci-dessus qu'une carte de source est un littéral d'objet contenant de nombreuses informations intéressantes:

  • Numéro de version sur lequel la carte source est basée
  • Nom du fichier du code généré (votre fichier de production minifié/combiné)
  • sourceRoot vous permet d'ajouter une structure de dossier au début des sources. Il s'agit également d'une technique d'économie d'espace.
  • sources contient tous les noms de fichiers combinés.
  • names contient tous les noms de variables/méthodes qui apparaissent dans votre code.
  • Enfin, la propriété "mappings" est là où la magie opère à l'aide de valeurs VLQ Base64. C'est là que se trouve le véritable gain d'espace.

VLQ Base64 et réduction de la taille de la carte source

À l'origine, la spécification de la carte source comportait une sortie très détaillée de tous les mappages, ce qui faisait que la carte source était environ 10 fois plus grande que le code généré. La version 2 a réduit cette valeur d'environ 50 %, et la version 3 l'a encore réduite de 50 %. Ainsi, pour un fichier de 133 Ko, vous obtenez une carte source d'environ 300 Ko.

Comment ont-ils réduit la taille tout en conservant les mappages complexes ?

La VLQ (VLQ) est utilisée en plus de l'encodage de la valeur en base64. La propriété "mappings" est une chaîne très longue. Cette chaîne contient des points-virgules (;) qui représentent un numéro de ligne dans le fichier généré. Chaque ligne contient des virgules (,) qui représentent chaque segment de cette ligne. Chacun de ces segments est de 1, 4 ou 5 dans les champs à longueur variable. Certains peuvent sembler plus longs, mais ils contiennent des bits de continuation. Chaque segment s'appuie sur le précédent, ce qui permet de réduire la taille du fichier, car chaque bit est relatif à ses segments précédents.

Décomposition d'un segment dans le fichier JSON de la carte source.

Comme indiqué ci-dessus, chaque segment peut avoir une longueur variable de 1, 4 ou 5 octets. Ce diagramme est considéré comme une longueur variable de quatre avec un bit de continuation (g). Nous allons décomposer ce segment et vous montrer comment la carte source détermine l'emplacement d'origine.

Les valeurs affichées ci-dessus sont purement les valeurs décodées en base64. Un traitement supplémentaire est nécessaire pour obtenir leurs valeurs réelles. Chaque segment calcule généralement cinq éléments:

  • Colonne générée
  • Fichier d'origine dans lequel cette erreur est apparue
  • Numéro de ligne d'origine
  • Colonne d'origine
  • Et, si possible, le nom d'origine

Tous les segments ne portent pas de nom, de nom de méthode ou d'argument. Par conséquent, leur longueur varie entre quatre et cinq variables. La valeur g du diagramme de segment ci-dessus est ce que l'on appelle un bit de continuation. Elle permet une optimisation supplémentaire lors de l'étape de décodage VLQ en base64. Un bit de continuation vous permet de vous appuyer sur une valeur de segment afin de stocker de grands nombres sans avoir à stocker un grand nombre, une technique très astucieuse d'économie d'espace qui trouve ses racines dans le format MIDI.

Une fois traité, le AAgBC du diagramme ci-dessus renvoie 0, 0, 32, 16, 1, le 32 étant le bit de continuation qui permet de créer la valeur suivante de 16. B décodé en base64 est 1. Les valeurs importantes utilisées sont donc 0, 0, 16 et 1. Cela nous indique que la ligne 1 (les lignes sont comptabilisées par les points-virgules) de la colonne 0 du fichier généré correspond au fichier 0 (le tableau de fichiers 0 est foo.js), à la ligne 16 de la colonne 1.

Pour montrer comment les segments sont décodés, je vais faire référence à la bibliothèque JavaScript Source Map de Mozilla. Vous pouvez également consulter le code de mappage de source des outils de développement WebKit, également écrit en JavaScript.

Pour comprendre correctement comment nous obtenons la valeur 16 à partir de B, nous devons avoir une compréhension de base des opérateurs par bit et du fonctionnement de la spécification pour le mappage de source. Le chiffre précédent, g, est signalé comme bit de continuation en comparant le chiffre (32) et le VLQ_CONTINUATION_BIT (100000 ou 32 au format binaire) à l'aide de l'opérateur AND (&) au niveau du bit.

32 & 32 = 32
// or
100000
|
|
V
100000

Cela renvoie un 1 à chaque position de bit où les deux valeurs apparaissent. Par conséquent, une valeur décodée en base64 de 33 & 32 renverrait 32, car ils ne partagent que l'emplacement de 32 bits, comme indiqué dans le diagramme ci-dessus. La valeur de décalage du bit est alors augmentée de 5 pour chaque bit de continuation précédent. Dans le cas ci-dessus, il n'est décalé que de 5 fois, ce qui correspond à un décalage de 1 (B) vers la gauche de 5.

1 <<../ 5 // 32

// Shift the bit by 5 spots
______
|    |
V    V
100001 = 100000 = 32

Cette valeur est ensuite convertie à partir d'une valeur VLQ signée en décalant le nombre (32) d'une position vers la droite.

32 >> 1 // 16
//or
100000
|
 |
 V
010000 = 16

Voilà comment transformer 1 en 16. Ce processus peut sembler trop compliqué, mais il prend tout son sens lorsque les chiffres augmentent.

Problèmes XSSI potentiels

La spécification mentionne les problèmes d'inclusion de script intersites pouvant survenir lors de la consommation d'un mappage de source. Pour atténuer ce problème, nous vous recommandons d'ajouter ")]}" au début de la première ligne de votre mappage source afin d'invalider délibérément le code JavaScript et de générer une erreur de syntaxe. Les outils de développement WebKit peuvent déjà gérer cela.

if (response.slice(0, 3) === ")]}") {
    response = response.substring(response.indexOf('\n'));
}

Comme indiqué ci-dessus, les trois premiers caractères sont découpés pour vérifier s'ils correspondent à l'erreur de syntaxe dans la spécification. Si c'est le cas, tous les caractères précédant la première entité de nouvelle ligne (\n) sont supprimés.

sourceURL et displayName en action: évaluation et fonctions anonymes

Bien qu'elles ne fassent pas partie de la spécification de la carte source, les deux conventions suivantes vous permettent de simplifier considérablement le développement lorsque vous travaillez avec des évaluations et des fonctions anonymes.

Le premier assistant ressemble beaucoup à la propriété //# sourceMappingURL et est mentionné dans la spécification de la version 3 de la carte source. En incluant le commentaire spécial suivant dans votre code, qui sera évalué, vous pouvez nommer les évaluations afin qu'elles apparaissent sous des noms plus logiques dans vos outils de développement. Découvrez une démonstration simple à l'aide du compilateur CoffeeScript:

Démo: Afficher le code eval() sous forme de script via sourceURL

//# sourceURL=sqrt.coffee
Aperçu du commentaire spécial sourceURL dans les outils de développement

L'autre aide vous permet de nommer des fonctions anonymes à l'aide de la propriété displayName disponible dans le contexte actuel de la fonction anonyme. Profillez la démonstration suivante pour voir la propriété displayName en action.

btns[0].addEventListener("click", function(e) {
    var fn = function() {
        console.log("You clicked button number: 1");
    };

    fn.displayName = "Anonymous function of button 1";

    return fn();
}, false);
Affichage de la propriété displayName en action.

Lorsque vous effectuez un profilage de votre code dans les outils de développement, la propriété displayName s'affiche plutôt que (anonymous). Toutefois, displayName est pratiquement inutilisé et ne sera pas intégré à Chrome. Mais tout espoir n'est pas perdu. Une proposition bien meilleure a été suggérée, appelée debugName.

Au moment de la rédaction de cet article, le nom d'évaluation n'est disponible que dans les navigateurs Firefox et WebKit. La propriété displayName n'est disponible que dans les versions nocturnes de WebKit.

Mobilisons-nous ensemble

Actuellement, une discussion très longue est en cours sur l'ajout de la compatibilité avec les mappages sources à CoffeeScript. Consultez le problème et ajoutez votre soutien pour que la génération de mappage de source soit ajoutée au compilateur CoffeeScript. Ce sera un grand succès pour CoffeeScript et ses adeptes.

UglifyJS présente également un problème de mappage source que vous devez examiner.

De nombreux outils génèrent des maps sources, y compris le compilateur CoffeeScript. Je considère que ce point est désormais sans objet.

Plus nous disposons d'outils permettant de générer des fichiers de mappage source, mieux c'est. Alors, n'hésitez pas à demander ou à ajouter la prise en charge des fichiers de mappage source à votre projet Open Source préféré.

Ce n'est pas parfait

Les cartes sources ne prennent pas en charge les expressions de surveillance pour le moment. Le problème est que tenter d'inspecter un nom d'argument ou de variable dans le contexte d'exécution actuel ne renvoie rien, car il n'existe pas vraiment. Cela nécessiterait une sorte de mappage inverse pour rechercher le nom réel de l'argument/de la variable que vous souhaitez inspecter par rapport au nom réel de l'argument/de la variable dans votre code JavaScript compilé.

Ce problème est bien sûr résoluble. En accordant plus d'attention aux cartes sources, nous pourrons commencer à voir des fonctionnalités incroyables et une meilleure stabilité.

Problèmes

jQuery 1.9 a récemment ajouté la compatibilité avec les cartes sources lorsqu'elles sont diffusées à partir de CDN officiels. Il a également signalé un bug particulier lorsque les commentaires de compilation conditionnelle IE (//@cc_on) sont utilisés avant le chargement de jQuery. Depuis, un commit a été effectué pour atténuer ce problème en encapsulant la sourceMappingURL dans un commentaire multiligne. La leçon à retenir est de ne pas utiliser de commentaire conditionnel.

Ce problème a depuis été corrigé, et la syntaxe a été remplacée par //#.

Outils et ressources

Voici d'autres ressources et outils à consulter:

Les cartes sources sont un utilitaire très puissant dans l'ensemble d'outils d'un développeur. Il est très utile de pouvoir garder votre application Web simple, mais facile à déboguer. Il s'agit également d'un outil d'apprentissage très puissant pour les développeurs débutants, qui peuvent voir comment les développeurs expérimentés structurent et écrivent leurs applications sans avoir à parcourir du code minifié illisible.

N'attendez plus ! Commencez dès maintenant à générer des cartes sources pour tous vos projets.