Présentation des mappages source JavaScript

Ryan Seddon

Avez-vous déjà rêvé de pouvoir garder votre code côté client lisible et, plus important encore, débogable, même après l'avoir combiné et réduit, sans nuire aux performances ? C'est désormais possible grâce à la magie des cartes sources.

Les cartes sources permettent de mapper un fichier combiné/minimise à un état non compilé. Lorsque vous compilez votre application pour la production, en minimisant et en combinant vos fichiers JavaScript, vous générez un mappage source contenant des informations sur vos fichiers d'origine. Lorsque vous interrogez un certain numéro de ligne et de colonne dans le code JavaScript généré, vous pouvez effectuer une recherche dans la carte source qui renvoie l'emplacement d'origine. Les outils pour les développeurs (actuellement les versions nocturnes de WebKit, Google Chrome ou Firefox 23 ou version ultérieure) peuvent analyser la carte source automatiquement et donner l'impression que vous exécutez des fichiers non réduits et non combinés.

La démonstration vous permet de faire un clic droit n'importe où dans la zone de texte contenant la source générée. Sélectionnez "Obtenir l'emplacement d'origine" pour interroger la carte source en transmettant les numéros de ligne et de colonne générés, puis renvoyer la position dans le code d'origine. Vérifiez que votre console est ouverte afin d'afficher le résultat.

Exemple de la bibliothèque de cartes source JavaScript Mozilla en action.

Contenus réels

Avant d'afficher l'implémentation suivante des cartes sources, assurez-vous d'avoir activé la fonctionnalité des cartes sources dans Chrome Canary ou WebKit la nuit en cliquant sur la roue dentée des paramètres dans le panneau des outils de développement et en cochant l'option "Activer les cartes sources".

Activation des cartes sources dans les outils de développement WebKit

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

Comment activer les cartes sources dans les outils de développement Firefox

Pourquoi devrais-je m'intéresser aux cartes sources ?

À l'heure actuelle, le mappage de sources ne fonctionne qu'entre du JavaScript non compressé/combiné et du JavaScript compressé/non combiné. Mais l'avenir s'annonce radieux avec les discussions sur les langages compilés vers JavaScript tels que CoffeeScript, et même la prise en charge de préprocesseurs CSS tels que SASS ou LESS.

À l'avenir, nous pourrions facilement utiliser presque toutes les langues comme si elles étaient compatibles de manière native dans le navigateur avec les mappages sources:

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

Regardez cet enregistrement d'écran de CoffeeScript en cours de débogage dans une version expérimentale de la console Firefox:

Google Web Toolkit (GWT) est récemment compatible avec les cartes sources. Ray Cromwell, de l'équipe GWT, a réalisé un excellent enregistrement d'écran montrant la prise en charge des cartes sources.

J'ai également créé un autre exemple qui utilise la bibliothèque Traceur de Google, qui vous permet d'écrire en 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 des caractéristiques et classes ES6 utilisées comme s'ils étaient compatibles de manière native dans le navigateur, grâce à la carte source.

La zone de texte de la démo vous permet également d'écrire ES6 qui sera compilé à la volée et de générer une carte source ainsi que le code ES3 équivalent.

Débogage de Traceur ES6 à l'aide de cartes sources

Démonstration: Écrire ES6, le déboguer et voir le mappage source en action

Comment fonctionne la carte source ?

À l'heure actuelle, le seul compilateur/mineur JavaScript qui permet de générer des cartes sources est le compilateur Closure. (J'expliquerai comment l'utiliser plus tard.) Une fois que vous avez combiné et réduit votre code JavaScript, il existera avec lui un fichier de mappage source.

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

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

Cela permet aux outils pour les développeurs de mapper les appels à leur emplacement dans les fichiers sources d'origine. Auparavant, le pragma du commentaire était //@. Toutefois, en raison de certains problèmes liés à cela et aux commentaires de compilation conditionnelle d'IE, la décision a été prise pour le remplacer par //#. Actuellement, Chrome Canary, WebKit Nightly et Firefox 24+ sont compatibles avec le nouveau pragma des commentaires. Ce changement de syntaxe affecte également sourceURL.

Si ces commentaires ne vous plaisent pas, vous pouvez définir un en-tête spécial sur votre fichier JavaScript compilé:

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

Comme le commentaire, cela indique au client de la carte source où trouver la carte source associée à un fichier JavaScript. Cet en-tête permet également d'éviter le problème du référencement des cartes sources dans les langues non compatibles avec les commentaires sur une seule ligne.

Exemple de mappages sources activés et désactivés dans WebKit Devtools.

Le fichier de carte source ne sera téléchargé que si vous avez activé les cartes sources et ouvert vos outils de développement. Vous devez également importer vos fichiers originaux afin que les outils de développement puissent les référencer et les afficher si nécessaire.

Comment générer une carte source ?

Vous devez utiliser le compilateur de fermeture pour réduire la taille des fichiers, les concaténer et générer un mappage source pour vos fichiers JavaScript. La commande se présente comme suit:

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 indicateurs de commande importants sont --create_source_map et --source_map_format. C'est obligatoire, car la version par défaut est V2 et nous ne voulons travailler qu'avec V3.

Anatomie d'une carte source

Pour mieux comprendre une carte source, prenons un petit exemple de fichier de mappage source qui serait généré par le compilateur Closure. Nous nous pencherons ensuite plus en détail sur le fonctionnement de la section "Mappages". L'exemple suivant diffère légèrement de l'exemple V3 spec.

{
    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 map source est un littéral d'objet contenant beaucoup d'informations juteuses:

  • Numéro de version sur lequel la carte source est basée
  • Nom de fichier du code généré (votre fichier de production combiné/minifé)
  • sourceRoot vous permet d'ajouter une structure de dossiers aux sources, ce qui permet également de gagner de l'espace.
  • "source" contient tous les noms de fichiers qui ont été combinés
  • contient tous les noms de variables/méthodes figurant dans votre code.
  • Enfin, c'est dans la propriété des mappages que la magie opère à l'aide des valeurs VLQ Base64. C'est ici que vous pouvez véritablement gagner de l'espace.

VLQ Base64 et taille de la carte source réduite

À l'origine, la spécification de la carte source affichait une sortie très détaillée de tous les mappages, ce qui entraînait une taille de la carte source environ 10 fois supérieure à celle du code généré. La version 2 a réduit ce nombre d'environ 50 %, tandis que la version 3 l'a de nouveau réduite de 50 %. Ainsi, pour un fichier de 133 Ko, vous obtenez une carte source d'environ 300 Ko.

Comment a-t-il réduit la taille tout en conservant les mappages complexes ?

VLQ (Variable Length Quantity) est utilisé avec l'encodage de la valeur en base64. La propriété mappings est une chaîne très volumineuse. 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 la ligne. Chacun de ces segments correspond à 1, 4 ou 5 dans des champs de longueur variable. Certaines peuvent sembler plus longues, mais elles 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 lié à ses segments précédents.

Répartition d'un segment dans le fichier JSON de mappage source.

Comme indiqué ci-dessus, chaque segment peut avoir une longueur variable de 1, 4 ou 5. 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 le lieu d'origine.

Les valeurs présentées ci-dessus sont purement des valeurs décodées en base64. Un traitement supplémentaire est nécessaire pour obtenir leurs valeurs réelles. Chaque segment répond généralement aux cinq étapes suivantes:

  • Colonne générée
  • Fichier d'origine dans lequel il apparaît
  • Numéro de ligne d'origine
  • Colonne d'origine
  • Si possible, le nom d'origine

Tous les segments n'ont pas de nom, de nom de méthode ou d'argument, donc les segments basculeront entre quatre et cinq longueurs variables. La valeur g dans le diagramme de segment ci-dessus est ce qu'on appelle un bit de continuation. Cela permet une optimisation supplémentaire lors de l'étape de décodage VLQ en base64. Un bit de continuation vous permet de développer une valeur de segment afin de stocker de grands nombres sans avoir à en stocker un grand. Cette technique très astucieuse d'économie d'espace a été créée dans le format MIDI.

Le diagramme AAgBC ci-dessus, une fois traité, renverrait 0, 0, 32, 16, 1 ; 32 étant le bit de continuation qui permet de créer la valeur suivante de 16. B purement décodé en Base64 correspond à 1. Les valeurs importantes sont donc 0, 0, 16 et 1. Cela nous permet ensuite de savoir que la ligne 1 (les lignes sont conservé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 dans la colonne 1.

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

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

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

Cela renvoie un 1 pour chaque position de bit où il apparaît dans les deux cas. Ainsi, une valeur décodée en base64 de 33 & 32 renvoie 32, car ils ne partagent que l'emplacement 32 bits, comme vous pouvez le voir dans le schéma ci-dessus. Cela augmente ensuite la valeur de décalage de 5 pour chaque bit de continuation précédent. Dans le cas ci-dessus, elle n'a été décalée de 5 qu'une seule fois, donc le décalage gauche de 1 (B) 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 signée VLQ en décalant vers la droite le nombre (32) d'un point.

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

Et voilà: c'est comme ça qu'on passe de 1 à 16. Cela peut sembler un processus trop compliqué, mais une fois que les chiffres commencent à augmenter, cela devient plus logique.

Problèmes XSSI potentiels

La spécification mentionne les problèmes d'inclusion de scripts intersites qui peuvent provenir de l'utilisation d'une carte source. Pour limiter ce problème, nous vous recommandons de remplacer la première ligne de votre mappage source par ")]} " afin d'invalider délibérément 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 segmentés pour vérifier s'ils correspondent à l'erreur de syntaxe indiquée dans la spécification et, le cas échéant, supprimer tous les caractères menant à la première entité de nouvelle ligne (\n).

sourceURL et displayName en action: fonctions d'évaluation et d'anonymat

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

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

Démonstration: voir le code de eval() s'afficher sous forme de script via sourceURL

//# sourceURL=sqrt.coffee
À quoi ressemble le commentaire spécial &quot;sourceURL&quot; dans les outils de développement

L'autre outil d'aide vous permet de nommer des fonctions anonymes en utilisant la propriété displayName disponible dans le contexte actuel de la fonction anonyme. Profilez 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 profilez votre code dans les outils de développement, la propriété displayName s'affiche à la place de (anonymous). Cependant, displayName est presque mort dans l'eau et ne risque pas d'arriver dans Chrome. Mais tous les espoirs ne sont pas perdus, et une bien meilleure proposition a été suggérée : debugName.

Au moment de la rédaction de l'article, la désignation de l'évaluation n'est disponible que dans les navigateurs Firefox et WebKit. La propriété displayName se trouve uniquement dans les nuits WebKit.

Travaillons ensemble

Actuellement, une discussion très longue est en cours sur la prise en charge des cartes sources dans CoffeeScript. Examinez le problème et ajoutez la prise en charge de l'ajout de la génération des cartes sources au compilateur CoffeeScript. Ce sera une grande victoire pour CoffeeScript et ses abonnés dévoués.

UglifyJS présente également un problème de carte source que vous devriez également examiner.

De nombreux tools génèrent des cartes sources, y compris le compilateur CoffeeScript. Je considère que c'est un point moyen maintenant.

Plus nous disposerons d'outils capables de générer des cartes sources, mieux nous nous améliorerons. N'hésitez pas à demander ou à ajouter la prise en charge des cartes sources à votre projet Open Source préféré.

Ce n'est pas parfait

Les expressions de contrôle ne sont pas prises en charge dans les mappages sources pour le moment. Le problème est que le fait d'essayer d'inspecter un nom d'argument ou de variable dans le contexte d'exécution actuel ne renvoie rien, car ils n'existent pas réellement. 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 JavaScript compilé.

Il s'agit bien entendu d'un problème qui peut être résolu. En observant davantage de cartes sources, nous pouvons commencer à découvrir des fonctionnalités incroyables et une meilleure stabilité.

Problèmes

Récemment, jQuery 1.9 est compatible avec les cartes sources diffusées depuis des CDN officiels. Nous avons également signalé un bug particulier lors de l'utilisation de commentaires de compilation conditionnel d'IE (//@cc_on) avant le chargement de jQuery. Depuis, un commit a été mis en place pour limiter ce problème en encapsulant l'URL sourceMappingURL dans un commentaire de plusieurs lignes. La leçon à apprendre n'utilise pas de commentaire conditionnel.

Ce problème a depuis été résolu grâce au remplacement de la syntaxe par //#.

Outils et ressources

Voici d'autres ressources et outils à consulter:

Les cartes sources sont un utilitaire très puissant intégré aux outils des développeurs. Il est très utile de pouvoir maintenir votre application Web épurée, mais facile à déboguer. Il s'agit également d'un outil d'apprentissage très puissant qui permet aux nouveaux développeurs de voir comment les développeurs expérimentés structurent et écrivent leurs applications sans avoir à se préoccuper d'un code illisible en taille réduite.

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