Greboca  

Suport technique et veille technologique

Aujourd’hui, les grandes entreprises et administrations publiques hésitent entre continuer à utiliser des logiciels propriétaires ou basculer vers les Logiciels Libres. Pourtant, la plupart des logiciels libres sont capables de bien traiter les données issues des logiciels propriétaire, et parfois avec une meilleur compatibilité.

C’est alors la barrière de la prise en main qui fait peur, et pourtant...

Les logiciels libres

L’aspect « Logiciel Libre » permet une évolution rapide et une plus grande participation des utilisateurs. Les aides et tutoriels foisonnent sur Internet ou sont directement inclus dans le logiciel lui-même.

Enfin, les concepteurs sont plus proches des utilisateurs, ce qui rend les logiciels libres plus agréable à utiliser et conviviaux.

Grâce à la disponibilité des logiciels libres, vous trouverez facilement des services de support techniques et la licence n’est plus un frein à l’utilisation de ces logiciels par votre personnel.

Notre support technique concerne essentiellement les logiciels libres, que ce soit sous forme de services ponctuels ou de tutoriels.

Bidouilleux d’Web  -  Une plongée illustrée dans les modules ECMAScript

 -  Avril 2018 - 

Cet article est une traduction de ES modules: A cartoon deep-dive, écrit par Lin Clark. Merci à goofy pour la relecture !

Les modules ECMAScript (ou modules ES) sont un système de modules, standard et officiel, pour JavaScript. Ce système est le fruit d’un travail de standardisation qui a duré 10 ans.

Nous pourrons bientôt profiter de tout cela avec l’arrivée des modules dans Firefox 60 en mai (cette version est actuellement en bêta). Les navigateurs principaux prennent tous en charge cette fonctionnalité et le groupe de travail pour les modules Node contribue à l’ajout des modules ES dans Node.js. L’intégration des modules ES dans WebAssembly a également démarré.

De nombreux développeurs JavaScript savent que les modules ES ont été à l’origine de différentes controverses mais peu comprennent réellement le fonctionnement des modules ES.

Voyons ici les problèmes résolus par les modules ES et leurs différences avec les autres systèmes de modules.

Les problèmes résolus par les modules

Quand on y pense, coder en JavaScript revient principalement à gérer des variables. On leur affecte des valeurs, on leur ajoute des nombres, on combine deux variables pour construire une nouvelle variable.

.01_variables_m.png

Étant donné la proportion de code consacrée à la modification des variables, l’organisation de ces variables aura un impact non-négligeable sur la qualité du code… mais aussi sur la facilité à le maintenir.

S’occuper d’un nombre de variables limité permet de simplifier les choses. En JavaScript, on peut s’aider des portées (scope) pour limiter le périmètre des variables qu’on doit gérer. En JavaScript, les portées formées par les fonctions empêchent ces dernières d’accéder à des variables qui seraient définies dans d’autres fonctions.

.02_module_scope_01_m.png

C’est une bonne chose. Cela signifie que lorsqu’on travaille sur une fonction, on peut ne penser qu’à cette fonction. Il n’est pas nécessaire de s’encombrer l’esprit avec l’impact potentiel des autres fonctions sur les variables de la fonction courante.

Toutefois, cela a un prix : il est plus difficile de partager des variables entre différentes fonctions.

Comment partager une variable en dehors de sa portée ? Pour répondre au problème, on utilise généralement une portée plus grande… comme la portée globale.

Peut-être vous souvenez vous de l’époque où jQuery était utilisé. Avant de pouvoir utiliser un plugin jQuery, il fallait s’assurer que jQuery faisait bien partie de la portée globale.

.02_module_scope_02_m.png

On a répondu à ce problème de partage des variables mais cela n’est pas sans conséquence.

Tout d’abord, il faut que les balises de script soient dans le bon ordre. Ensuite, il faut s’assurer que personne ne vienne chambouler cet ordre.

Si cet ordre vient à être chamboulé, l’application pourra causer une belle erreur au plein milieu de l’exécution. Lorsque la fonction qui va chercher jQuery où elle s’attend à l’avoir (c’est-à-dire dans la portée globale) et ne le trouve pas, elle déclenchera une erreur et arrêtera son exécution.

.02_module_scope_03_m.png

La maintenance du code devient plus épineuse. Retirer du vieux code ou des balises <script></code> s’apparente à un pari risqué. Personne ne peut savoir ce qui va être cassé. Les dépendances entre les différentes parties du code sont implicites. N’importe quelle fonction peut manipuler n’importe quoi appartenant à la portée globale et on ne sait plus de quels scripts dépendent ces fonctions.</p>

<p>Ce n’est pas tout, les variables qui sont dans la portée globale peuvent très bien être modifiées par n’importe quel code situé au sein de cette portée. Du code malveillant pourrait modifier cette variable globale à dessein afin de détourner l’exécution. Du code <em>a priori</em> innocent pourrait également, accidentellement, modifier la variable.</p>

<h2>Qu’apportent les modules ?</h2>

<p>Les modules permettent de mieux organiser les variables et les fonctions. Avec les modules, on peut regrouper les variables et les fonctions de façon pertinente.</p>

<p>Ainsi, ces fonctions et variables font partie de la portée d’un module et cette portée peut être utilisée afin de partager des variables entre les fonctions du module.</p>

<p>Cependant, à la différence des portées créées par les fonctions, les portées des modules permettent de rendre leurs variables disponibles pour d’autres modules. On peut indiquer, explicitement, les variables, les classes ou les fonctions qui seront disponibles pour être utilisées ailleurs.</p>

<p>Quelque chose qui est rendu disponible pour un autre module est appelé un <strong>export</strong>. Lorsqu’on dispose d’un export, les autres modules peuvent, de façon explicite, indiquer qu’ils dépendent de cette variable, classe ou fonction.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/02_module_scope_04.png" title="Deux portées de modules avec la deuxième utilisant un export du premier"><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.02_module_scope_04_m.png" style="margin: 0 auto; display: block;" title="Deux portées de modules avec la deuxième utilisant un export du premier"/></a></p>

<p>Cette relation étant explicite, on est cette fois en mesure de déterminer ce qui va casser si on retire un des modules.</p>

<p>Une fois capable d’exporter et d’importer des variables entre les modules, on peut diviser le code en petits fragments qui peuvent fonctionner de façon indépendante. On peut alors combiner et recombiner ces fragments, tels des briques de Lego, afin de créer différentes applications à partir d’un même ensemble de modules.</p>

<p>L’organisation en module apportant de nombreux bénéfices, plusieurs tentatives ont été lancées afin d’incorporer ces modules dans JavaScript. Il existe aujourd’hui deux systèmes de module activement utilisés :
* CommonJS (CJS), utilisé par Node.js jusqu’à présent.
* ESM (<em>ECMAScript modules</em>), un système plus récent qui a été ajouté à la spécification JavaScript.</p>

<p>Les navigateurs prennent déjà en charge les modules ES et Node travaille à leur implémentation.</p>

<p>Voyons ensuite le détail du fonctionnement de ce nouveau système de modules.</p>

<h2>Le fonctionnement des modules ES</h2>

<p>Lorsqu’on développe en utilisant des modules, on construit un graphe de dépendances. Les liens entre ces dépendances proviennent des instructions <code>import</code> utilisées.</p>

<p>Ces instructions <code>import</code> indiquent, au moteur du navigateur ou à celui de Node, les fragments de code à charger. Il suffit de donner le nom d’un fichier comme point d’entrée pour le graphe puis le moteur suivra chacune des instructions <code>import</code> afin de récupérer l’ensemble du code nécessaire.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/04_import_graph.png" title="Un module avec deux dépendances. Le module du haut est le point d'entrée et les deux autres sont liés par la présence d'instructions import"><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.04_import_graph_m.png" style="margin: 0 auto; display: block;" title="Un module avec deux dépendances. Le module du haut est le point d'entrée et les deux autres sont liés par la présence d'instructions import"/></a></p>

<p>Mais le navigateur ne peut pas utiliser les fichiers directement. Il faut qu’il analyse ces fichiers afin de les transformer en structures de données appelées <em>Module Records</em>. Ainsi, il peut savoir ce qui se passe dans chaque fichier.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/05_module_record.png" title="Un module record avec différents champs et notamment RequestedModules et ImportEntries"><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.05_module_record_m.png" style="margin: 0 auto; display: block;" title="Un module record avec différents champs et notamment RequestedModules et ImportEntries"/></a></p>

<p>Après cette analyse, le <em>module record</em> doit être converti en une <em>instance de module</em>. Une instance représente deux choses : le code et un état.</p>

<p>Le code est, pour résumer, un ensemble d’instructions (à la manière d’une recette). Mais le code seul ne permet pas de faire grand-chose, il lui faut de la matière première à traiter avec ces instructions.</p>

<p>L’état est cette matière première. L’état permet de stocker les valeurs des variables à un instant donné. Quant aux variables, ce ne sont en réalité que des alias pour les espaces mémoire qui contiennent ces valeurs.</p>

<p>Reformulons : l’instance d’un module combine le code (la liste des instructions) avec un état (l’ensemble des valeurs des variables).</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/06_module_instance.png" title="Une instance de module combinant du code et un état"><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.06_module_instance_m.png" style="margin: 0 auto; display: block;" title="Une instance de module combinant du code et un état"/></a></p>

<p>Il nous faut donc une instance de module pour chaque module. Le processus de chargement d’un module part du point d’entrée fourni par le fichier jusqu’à avoir un graphe complet avec l’ensemble des instances de module nécessaires.</p>

<p>Pour les modules ES, ce processus se décompose en trois étapes :</p>

<ol>
<li>Construction  — le moteur trouve, télécharge et analyse l’ensemble des fichiers pour le convertir en <em>module records</em>.</li>
<li>Instanciation — le moteur détermine les zones mémoire dans lesquelles il va placer les valeurs exportées (il ne remplit pas ces zones pour l’instant). Ainsi, les exports et les imports peuvent pointer vers ces zones mémoire. C’est ce qu’on appelle aussi l’édition de lien ou <em>linking</em>.</li>
<li>Évaluation — le code est exécuté afin de remplir ces espaces mémoire avec les valeurs réelles des variables.</li>
</ol>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/07_3_phases.png" title="Une illustration des trois phases : la construction qui part d'un fichier JS afin de construire plusieurs module records ; l'instanciation qui relie ces records et l'évaluation qui exécute le code."><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.07_3_phases_m.png" style="margin: 0 auto; display: block;" title="Une illustration des trois phases : la construction qui part d'un fichier JS afin de construire plusieurs module records ; l'instanciation qui relie ces records et l'évaluation qui exécute le code."/></a></p>

<p>On peut entendre dire que les modules ES sont <em>asynchrones</em>. En fait, cette asynchronicité vient du fait que chacune de ces trois étapes peut être exécutée séparément.</p>

<p>Cela signifie que la spécification introduit un niveau d’asynchronicité qui n’existait pas avec les modules CommonJS. Nous y reviendrons ensuite, mais il faut retenir qu’avec CJS, un module et ses dépendances sont chargées, instanciées et évaluées d’un coup, sans interruption entre ces étapes.</p>

<p>Ceci étant dit, les étapes individuelles ne sont pas nécessairement asynchrones et elles peuvent isolément être exécutées de façon synchrone. Cela dépend du chargement. En fait, la spécification pour les modules ES ne décrit pas l’intégralité du processus. Celui-ci se découpe en deux moitiés dont chacune est décrite par une spécification différente.</p>

<p><a href="https://tc39.github.io/ecma262/#sec-modules">La spécification pour les modules ES</a> indique comment analyser les fichiers pour les convertir en <em>module records</em> et comment instancier puis évaluer le module. Toutefois, elle ne dit rien de la façon dont on doit récupérer les fichiers pour commencer.</p>

<p>C’est au chargeur (<em>loader</em>) de récupérer les fichiers. Or, le comportement de ce chargeur est décrit dans une autre spécification. Pour les navigateurs, la spécification utilisée est <a href="https://html.spec.whatwg.org/#fetch-a-module-script-tree">la spécification HTML</a>. Pour d’autres plates-formes, on pourrait tout à fait avoir d’autres chargeurs qui devraient également être spécifiés.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/07_loader_vs_es.png" title="Deux personnages : celui de gauche représente la spécification qui définit comment charger des modules (c'est-à-dire la spécification HTML) et celui de droite représente la spécification des modules ES."><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.07_loader_vs_es_m.png" style="margin: 0 auto; display: block;" title="Deux personnages : celui de gauche représente la spécification qui définit comment charger des modules (c'est-à-dire la spécification HTML) et celui de droite représente la spécification des modules ES."/></a></p>

<p>Le chargeur contrôle exactement la façon dont les modules sont chargés. Il appelle les méthodes associées aux modules ES : <code> ParseModule</code>, <code>Module.Instantiate</code> et <code>Module.Evaluate</code>. On pourrait le comparer à un marionnettiste qui tire les ficelles du moteur JavaScript.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/08_loader_as_puppeteer.png" title="Le personnage du chargeur agit comme un marionnettiste tirant les ficelles du personnage représentant la spécification des modules ES."><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.08_loader_as_puppeteer_m.png" style="margin: 0 auto; display: block;" title="Le personnage du chargeur agit comme un marionnettiste tirant les ficelles du personnage représentant la spécification des modules ES."/></a></p>

<p>Étudions maintenant en détail chacune de ces trois étapes.</p>

<h3>Construction</h3>

<p>La phase de construction se décompose en trois sous-étapes :</p>

<ol>
<li>Déterminer où télécharger le fichier contenant le module (aussi appelée la résolution)</li>
<li>Récupérer le fichier (en le téléchargeant via une URL ou en le chargeant depuis le système de fichier)</li>
<li>Analyser le fichier et le convertir en un <em>module record</em></li>
</ol>

<h4>Trouver et récupérer le fichier</h4>

<p>C’est le chargeur qui va déterminer l’emplacement du fichier et le télécharger. Pour commencer, il lui faut un point d’entrée. En HTML, ce point d’entrée est fourni par le document indiqué via un élément <code><script></code>.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/08_script_entry.png" title="Un élément \<script\> avec un attribut type=module et un attribut src contenant une URL. Cette URL pointe vers le fichier qui est le point d'entrée"><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.08_script_entry_m.png" style="margin: 0 auto; display: block;" title="Un élément \<script\> avec un attribut type=module et un attribut src contenant une URL. Cette URL pointe vers le fichier qui est le point d'entrée"/></a></p>

<p>Mais comment faire pour trouver les modules suivants, ceux dont dépend <code>main.js</code> ?</p>

<p>C’est ici qu’intervient l’instruction <code>import</code>. Une partie de cette instruction est l’indicateur du module (<em>module specifier</em>). Cette partie permet au chargeur de connaître l’emplacement du prochain module.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/09_module_specifier.png" title="Une instruction import avec une URL en fin de ligne qui est identifiée comme indicateur du module"><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.09_module_specifier_m.png" style="margin: 0 auto; display: block;" title="Une instruction import avec une URL en fin de ligne qui est identifiée comme indicateur du module"/></a></p>

<p>On notera ici qu’il y a une différence entre Node et les navigateurs pour ces indicateurs. Chaque environnement hôte dispose de sa propre méthode pour interpréter la chaîne de caractères fournissant l’indicateur. Pour cela, chaque plateforme utilise un algorithme de résolution qui lui est propre. À l’heure actuelle, certains indicateurs de module qui fonctionnent avec Node ne fonctionneront pas dans le navigateur (<a href="https://github.com/domenic/package-name-maps">des pistes sont à l’étude pour résoudre ce problème</a>).</p>

<p>Jusqu’à ce que ce problème soit résolu, les navigateurs acceptent uniquement des URL comme indicateurs de module. C’est le fichier vers lequel pointe cette URL qui sera chargé.</p>

<p>Toutefois, la récupération des différents modules ne se fait pas simultanément. On ne peut pas savoir les dépendances dont on a besoin tant qu’on a pas analysé le contenu du module et on ne peut pas analyser le fichier tant qu’on ne l’a pas récupéré.</p>

<p>Cela signifie qu’il faut progresser niveau par niveau : analyser le premier fichier, déterminer ses dépendances, trouver ces dépendances, les charger et ainsi de suite.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/10_construction.png" title="Un diagramme qui illustre un fichier récupéré puis analysé puis deux dépendances récupérées et analysées"><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.10_construction_m.png" style="margin: 0 auto; display: block;" title="Un diagramme qui illustre un fichier récupéré puis analysé puis deux dépendances récupérées et analysées"/></a></p>

<p>S’il fallait que le <em>thread</em> principal patiente le temps que chacun de ces fichiers soit téléchargé, de nombreuses tâches s’ajouteraient à la queue.</p>

<p>En effet, lorsqu’on travaille dans un navigateur, le téléchargement dure longtemps.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/11_latency.png" title="Un graphe illustrant les durées d'accès et de téléchargement si un cycle processeur durait 1 seconde : l'accès à la mémoire principale prendrait 6 minutes et la récupération d'un fichier provenant d'un serveur à l'autre extrémité des États-Unis prendrait 4 ans."><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.11_latency_m.png" style="margin: 0 auto; display: block;" title="Un graphe illustrant les durées d'accès et de téléchargement si un cycle processeur durait 1 seconde : l'accès à la mémoire principale prendrait 6 minutes et la récupération d'un fichier provenant d'un serveur à l'autre extrémité des États-Unis prendrait 4 ans."/></a></p>

<p><em>Illustration basée sur <a href="https://twitter.com/srigi/status/917998817051541504">ce graphique</a>.</em></p>

<p>Bloquer le <em>thread</em> principal de cette façon ralentirait considérablement une application qui utilise les modules. C’est pour cette raison que la spécification pour les modules ES découpe l’algorithme en plusieurs phases. Placer la construction à part permet aux navigateurs de récupérer les fichiers et de construire le graphe des modules avant de s’attaquer à la tâche, synchrone, d’instanciation.</p>

<p>Cette approche (de découpe d’algorithme) est une des différences fondamentales entre les modules ES et les modules CommonJS.</p>

<p>CommonJS peut gérer les choses différemment, car les fichiers proviennent du système de fichier et l’accès à ces ressources prend moins de temps que de les télécharger via Internet. Cela signifie que Node peut bloquer le <em>thread</em> principal pendant qu’il charge le fichier. Lorsque le fichier est chargé, on peut directement l’instancier et l’évaluer (encore une fois, ces phases ne sont pas distinctes avec CommonJS). Cela signifie également qu’on parcourt tout l’arbre des dépendances, qu’on les charge, qu’on les instancie et qu’on les évalue avant de renvoyer l’instance du module.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/12_cjs_require.png" title="Un diagramme illustrant l'évaluation d'un module Node jusqu'à l'instruction require puis suivant le chargement synchrone et l'évaluation du module et de ses dépendances"><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.12_cjs_require_m.png" style="margin: 0 auto; display: block;" title="Un diagramme illustrant l'évaluation d'un module Node jusqu'à l'instruction require puis suivant le chargement synchrone et l'évaluation du module et de ses dépendances"/></a></p>

<p>L’approche adoptée par CommonJS implique différentes choses que nous verrons ensuite. Entre autres, cela signifie qu’avec Node et les modules CommonJS, on peut utiliser des variables dans l’indicateur du module. On exécute l’ensemble du code du module (y compris l’instruction <code>require</code>) avant d’aller consulter le prochain module. Ainsi, la variable utilisée aura une valeur lorsqu’on passera à la résolution de module.</p>

<p>En revanche, avec les modules ES, tout le graphe des dépendances doit être construit avant l’évaluation. Aussi, on ne peut pas avoir de variable au sein des identificateurs, car ces variables n’ont pas encore de valeurs.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/13_static_import.png" title="Une instruction require utilisant une variable ne pose pas de problème. Par contre, on ne peut pas utiliser de variable dans une instruction import."><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.13_static_import_m.png" style="margin: 0 auto; display: block;" title="Une instruction require utilisant une variable ne pose pas de problème. Par contre, on ne peut pas utiliser de variable dans une instruction import."/></a></p>

<p>Cependant, il est parfois utile d’utiliser des variables pour les chemins des modules. On peut, par exemple, vouloir charger un module ou un autre en fonction de l’état du code ou de l’environnement dans lequel on exécute le code.</p>

<p>Une proposition, <a href="https://github.com/tc39/proposal-dynamic-import"><em>dynamic import</em></a>, vise à rendre cela possible et permettrait d’écrire une instruction <code>import</code> de la forme</p>

<pre><code>import(`${chemin}/toto.js`).
</code></pre>

<p>Pour cela, chaque fichier chargé grâce à <code>import()</code> est considéré comme un point d’entrée pour un autre graphe. Le module importé dynamiquement devient ainsi une racine d’un nouveau graphe, traité séparément.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/14dynamic_import_graph.png" title="Les graphes de deux modules avec une dépendance entre eux, étiquetée avec une instruction import dynamique."><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.14dynamic_import_graph_m.png" style="margin: 0 auto; display: block;" title="Les graphes de deux modules avec une dépendance entre eux, étiquetée avec une instruction import dynamique."/></a></p>

<p>Pour un graphe donné, on aura un cache contenant les instances de ces modules. Aussi, si un module A est présent dans deux graphes, on aura deux caches avec deux instances. Pour chaque module d’une portée globale, il n’y aura qu’une seule instance du module.</p>

<p>Charger une seule instance pour chaque module minimise le travail du moteur JavaScript : le module n’est récupéré qu’une seule fois même lorsque plusieurs modules dépendent de ce premier (une des raisons pour la mise en cache des modules, nous en verrons une autre dans le détail consacré à la phase d’évaluation).</p>

<p>Le chargeur gère ce cache avec ce qu’on appelle <a href="https://html.spec.whatwg.org/multipage/webappapis.html#module-map">la carte des modules (<em>module map</em>)</a>. Chaque portée globale maintient le registre de ses modules dans une carte distincte.</p>

<p>Lorsque le chargeur récupère une URL, il place cette URL dans la carte des modules et note qu’il est en train de récupérer le fichier. Il envoie ensuite la requête puis démarre la récupération du prochain fichier.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/15_module_map.png" title="Le personnage du chargeur qui renseigne le tableau de la carte des modules avec l'URL et avec le mot "fetching" pour indiquer qu'il la récupère."><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.15_module_map_m.png" style="margin: 0 auto; display: block;" title="Le personnage du chargeur qui renseigne le tableau de la carte des modules avec l'URL et avec le mot "fetching" pour indiquer qu'il la récupère."/></a></p>

<p>Que se passe-t-il lorsqu’un autre module dépend du même fichier ? Le chargeur vérifiera la présence de l’URL dans la carte. Si celle-ci est déjà en cours de récupération, il passera à l’URL suivante.</p>

<p>La carte des modules ne sert pas uniquement à indiquer les modules qui sont en train d’être récupérés, elle joue également le rôle de cache pour les modules comme nous le verrons par la suite.</p>

<h4>L’analyse (<em>parsing</em>)</h4>

<p>Nous avons désormais récupéré ce fichier et il faut le convertir en <em>module record</em> afin que le navigateur comprenne les différentes partie du module.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/25_file_to_module_record.png" title="Un diagramme illustrant l'analyse du fichier main.js pour le convertir en module record"><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.25_file_to_module_record_m.png" style="margin: 0 auto; display: block;" title="Un diagramme illustrant l'analyse du fichier main.js pour le convertir en module record"/></a></p>

<p>Une fois le <em>module record</em> créé, il est placé dans la carte des modules. De cette façon, dès que le module est demandé, le chargeur peut le récupérer directement depuis la carte.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/25_module_map.png" title="Les cellules de la carte des modules, auparavant marquées avec "fetching", sont progressivement remplies avec des module records"><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.25_module_map_m.png" style="margin: 0 auto; display: block;" title="Les cellules de la carte des modules, auparavant marquées avec "fetching", sont progressivement remplies avec des module records"/></a></p>

<p>Précisons un léger détail qui n’est peut-être pas si léger. Tous les modules sont analysés comme s’ils utilisaient <code>"use strict"</code> au début. D’autres différences (par rapport à l’analyse des scripts « classiques ») sont également présentes : le mot-clé <code>await</code> est réservé au niveau le plus haut d’un module ; la valeur de <code>this</code> vaut <code>undefined</code>.</p>

<p>Cette différence d’analyse est aussi appelée <em>parse goal</em>. Si on analyse le même fichier pour un autre usage, on pourrait obtenir un résultat différent. Aussi, avant l’analyse, il est nécessaire de savoir si on analyse un module ou non.</p>

<p>Pour les navigateurs, c’est plutôt facile. Il suffit de placer l’attribut <code>type="module"</code> dans la balise <code>script</code>. Le navigateur pourra alors savoir que le fichier doit être analysé comme un module. Au sein des scripts, seuls les modules peuvent être importés, le navigateur sait donc que tout ce qui est importé doit être analysé comme un module.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/26_parse_goal.png" title="Le navigateur détermine que main.js est un module, car la valeur de l'attribut type de la balise script l'indique. Pour counter.js, c'est aussi un module car il est importé"><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.26_parse_goal_m.png" style="margin: 0 auto; display: block;" title="Le navigateur détermine que main.js est un module, car la valeur de l'attribut type de la balise script l'indique. Pour counter.js, c'est aussi un module car il est importé"/></a></p>

<p>Pour Node, c’est une autre histoire. Il n’y a pas d’éléments HTML et on ne peut donc pas utiliser un attribut <code>type</code>. Une des solutions utilisées par la communauté a été d’utiliser l’extension <code>.mjs</code>. Avec cette extension, on indique à Node que le fichier est un module. Vous pourrez lire certaines discussions où cette extension est un <em>signal</em> pour le <em>parse goal</em>. La discussion à ce sujet est toujours en cours et le <em>signal</em> qui sera utilisé par la communauté Node n’est pas encore fixé.</p>

<p>Dans tous les cas, le chargeur déterminera s’il faut analyser le fichier comme un module ou non. Si c’est un module et que celui-ci a des imports, le chargeur recommencera le processus jusqu’à ce que l’ensemble des fichiers ait été récupéré et analysé.</p>

<p>Et voilà ! Au début de cette étape, nous avions un fichier comme point d’entrée et maintenant nous avons un ensemble de <em>module records</em>.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/27_construction.png" title="Un fichier JS à gauche avec trois module records analysés sur la droite pour modéliser le résultat de la phase de construction"><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.27_construction_m.png" style="margin: 0 auto; display: block;" title="Un fichier JS à gauche avec trois module records analysés sur la droite pour modéliser le résultat de la phase de construction"/></a></p>

<p>La prochaine étape consiste à instancier ce module et à lier toutes ces instances ensembles.</p>

<h3>L’instanciation</h3>

<p>Comme évoqué plus haut, une instance combine du code avec un état. Cet état est en mémoire. Aussi, l’étape d’instanciation concernera principalement la mémoire.</p>

<p>Pour commencer, le moteur JavaScript crée un environnement pour le module. Cet environnement va gérer les variables pour le <em>module record</em>. Ensuite, il trouve les zones mémoire où placer l’ensemble des exports. C’est l’environnement du module qui maintiendra le registre d’association entre les zones mémoire et les exports.</p>

<p>Ces zones mémoire n’ont pas encore de valeur à l’intérieur. Ce n’est qu’après l’évaluation que les valeurs seront renseignées. Seule une exception échappe à cette règle : les déclarations de fonction sont initialisées pendant cette phase afin de faciliter la phase d’évaluation.</p>

<p>Pour instancier le graphe des modules, le moteur effectue un parcours postfixe (en profondeur) de l’arbre (<em>depth first post-order traversal</em>). Cela signifie qu’il commence par parcourir le bas de l’arbre (les dépendances qui n’ont aucune dépendance) et traite leurs exports.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/30_live_bindings_01.png" title="Illustration d'une colonne de mémoire vie au milieu. Les module records pour l'affichage et le comptage sont câblés à des zones mémoire sur cette colonne"><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.30_live_bindings_01_m.png" style="margin: 0 auto; display: block;" title="Illustration d'une colonne de mémoire vie au milieu. Les module records pour l'affichage et le comptage sont câblés à des zones mémoire sur cette colonne"/></a>.</p>

<p>Une fois que le moteur a fini de câbler l’ensemble des exports du niveau inférieur, il monte d’un niveau pour câbler les imports du module correspondant.</p>

<p>On notera qu’un export et un import pointent vers le même emplacement en mémoire. Commencer par câbler les exports garantit la capacité à connecter les imports aux exports associés.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/30_live_bindings_02.png" title="Le même diagramme que précédemment mais sur lequel les imports de main.js sont désormais câblés sur les zones mémoire où sont aussi câblés les exports"><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.30_live_bindings_02_m.png" style="margin: 0 auto; display: block;" title="Le même diagramme que précédemment mais sur lequel les imports de main.js sont désormais câblés sur les zones mémoire où sont aussi câblés les exports"/></a></p>

<p>Il y a une ici une différence avec les modules CommonJS. En CommonJS, tout l’objet exporté est copié à l’export. Cela signifie que les valeurs (les nombres par exemple) sont exportées sous forme de copies.</p>

<p>Cela signifie que si le module qui exporte modifie une valeur plus tard, le module qui l’import ne verra pas ce changement.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/31_cjs_variable.png" title="Le diagramme avec la colonne mémoire au milieu, à gauche l'export CommonJS qui est câblé sur un espace contenant la valeur 5 et à droite l'import qui est câble sur un autre espace contenant la copie de la valeur 5"><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.31_cjs_variable_m.png" style="margin: 0 auto; display: block;" title="Le diagramme avec la colonne mémoire au milieu, à gauche l'export CommonJS qui est câblé sur un espace contenant la valeur 5 et à droite l'import qui est câble sur un autre espace contenant la copie de la valeur 5"/></a></p>

<p>Les modules ES utilisent à la place des liaisons dynamiques (<em>live bindings</em>). Les deux modules (l’export et l’import) pointent vers le même emplacement en mémoire. Cela signifie que lorsque le module exportant modifie une valeur, cette modification est également présente dans le module important.</p>

<p>Les modules qui exportent des valeurs peuvent modifier ces valeurs à tout moment mais les modules qui importent des valeurs ne peuvent pas modifier les valeurs importées. Ceci étant dit, si un module importe un objet, il peut modifier les valeurs des propriétés de cet objet.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/30_live_bindings_04.png" title="Le module exportant peut modifier la valeur en mémoire mais le module qui importe la valeur échoue s'il essaie."><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.30_live_bindings_04_m.png" style="margin: 0 auto; display: block;" title="Le module exportant peut modifier la valeur en mémoire mais le module qui importe la valeur échoue s'il essaie."/></a></p>

<p>Avec ces liaisons dynamiques, il est possible de câbler tous les modules sans exécuter aucun code. Comme on le verra ensuite, cela facilite l’évaluation lorsqu’on a des dépendances cycliques.</p>

<p>À la fin de cette étape, toutes les instances de modules pour les imports et les exports sont câblées aux zones mémoire.</p>

<p>Nous voilà alors capables d’évaluer le code et de remplir ces zones mémoire avec leurs valeurs.</p>

<h3>L’évaluation</h3>

<p>L’étape finale consiste à remplir ces zones mémoire. Le moteur JS s’attaque à cette tâche en exécutant le code « de plus haut niveau » (<em>top level</em>), c’est-à-dire le code qui est situé en dehors des fonctions.</p>

<p>En plus de remplir ces espaces en mémoire, l’évaluation du code peut déclencher certains effets de bord. Un module peut, par exemple, communiquer avec un serveur.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/40_top_level_code.png" title="Un module avec du code situé en dehors des fonctions qui suivent. Ce code est marqué comme étant au plus haut niveau"><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.40_top_level_code_m.png" style="margin: 0 auto; display: block;" title="Un module avec du code situé en dehors des fonctions qui suivent. Ce code est marqué comme étant au plus haut niveau"/></a></p>

<p>À cause de ces effets de bord, il peut être préférable de n’évaluer le module qu’une seule fois. Contrairement à l’édition de lien qui se produit pendant l’instanciation et qui peut être effectuée plusieurs fois en produisant toujours le même résultat, l’évaluation pourra produire des résultats différents selon le nombre de fois qu’on l’a appliquée.</p>

<p>C’est l’une des raisons à l’origine de la carte des modules. La carte des modules met en cache les modules et expose une URL canonique pour chacun des modules. Ainsi, on s’assure que chaque module est exécuté une seule fois. Comme avec l’instanciation, l’exécution est effectuée en suivant un parcours postfixe (en profondeur) de l’arbre.</p>

<p>Mais que se passe-t-il avec les cycles que nous avons évoqués plus tôt ?</p>

<p>Si on a une dépendance cyclique, on aura une boucle dans le graphe. Généralement, cette boucle est plutôt longue. Toutefois, afin de pouvoir expliquer le problème ici, nous allons utiliser un exemple simplifié avec une boucle plus courte.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/41_cyclic_graph.png" title="Un graphe de module complexe avec un cycle de 4 modules à gauche. Sur la droite, un cycle avec 2 modules."><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.41_cyclic_graph_m.png" style="margin: 0 auto; display: block;" title="Un graphe de module complexe avec un cycle de 4 modules à gauche. Sur la droite, un cycle avec 2 modules."/></a></p>

<p>Voyons ce qui se serait passé avec des modules CommonJS. Tout d’abord, le module principal est exécuté jusqu’à l’instruction <code>require</code>. Ensuite, on charge le module <code>counter</code>.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/41_cjs_cycle.png" title="Un module CommonJS avec une variable exportée depuis main.js après une instruction require pour charger counter.js (qui dépend de l'import)."><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.41_cjs_cycle_m.png" style="margin: 0 auto; display: block;" title="Un module CommonJS avec une variable exportée depuis main.js après une instruction require pour charger counter.js (qui dépend de l'import)."/></a></p>

<p>Le module <code>counter</code> essaie alors d’accéder à la variable <code>message</code> provenant de l’object d’export. Cette variable n’ayant pas encore été initialisée dans le module principal, elle renvoie <code>undefined</code>. Le moteur JavaScript alloue de la mémoire pour la variable locale et indique <code>undefined</code> comme valeur.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/42_cjs_variable_2.png" title="La colonne représentant la zone mémoire au milieu : aucun lien entre main.js et la mémoire mais un lien d'import entre counter.js et un emplacement mémoire qui contient la valeur undefined"><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.42_cjs_variable_2_m.png" style="margin: 0 auto; display: block;" title="La colonne représentant la zone mémoire au milieu : aucun lien entre main.js et la mémoire mais un lien d'import entre counter.js et un emplacement mémoire qui contient la valeur undefined"/></a></p>

<p>L’évaluation continue jusqu’à la fin du code de plus haut niveau pour le module <code>counter</code>. On veut pouvoir voir si la valeur correcte de <code>message</code> y parvient (après que main.js a été évalué) et on utilise donc <code>setTimeout</code> pour patienter. L’évaluation reprend alors du côté de <code>main.js</code>.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/43_cjs_cycle.png" title="counter.js rend le contrôle à main.js, qui peut finir son évaluation"><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.43_cjs_cycle_m.png" style="margin: 0 auto; display: block;" title="counter.js rend le contrôle à main.js, qui peut finir son évaluation"/></a></p>

<p>La variable <code>message</code> est initialisée et ajoutée à un emplacement mémoire mais aucun lien n’est créé entre cet emplacement et celui utilisé précédemment. Aussi, la valeur utilisée dans <code>counter</code> restera (indéfiniment) <code>undefined</code>.</p>

<p><a href="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/44_cjs_variable_2.png" title="main.js envoie son export en mémoire et crée une connexion : la bonne valeur est bien inscrite en mémoire. Toutefois, counter.js continue de pointer vers le premier emplacement qui contient toujours undefined."><img src="https://tech.mozfr.org/dotclear/public/bidouilleux/ES_Modules/.44_cjs_variable_2_m.png" style="margin: 0 auto; display: block;" title="main.js envoie son export en mémoire et crée une connexion : la bonne valeur est bien inscrite en mémoire. Toutefois, counter.js continue de pointer vers le premier emplacement qui contient toujours undefined."/></a></p>

<p>Si les exports avaient été créés avec des liaisons dynamiques, le module <code>counter</code> aurait fini par voir arriver la bonne valeur pour <code>message</code>. En effet, après que l’évaluation de <code>main.js</code> a eu lieu et remplit la valeur, une fois que le <code>setTimeout</code> se déclenche, il utilise la bonne valeur.</p>

<p>La prise en charge de ces cycles a été une composante majeure lors de la conception des modules ES. C’est ce découpage en trois phases qui permet de gérer les dépendances cycliques.</p>

<h2>Quelle est la prise en charge actuelle des modules ES ?</h2>

<p>Avec la sortie de Firefox 60 en mai, l’ensemble des principaux navigateurs prendra en charge les modules ES par défaut. Node travaille également sur le sujet et un groupe de travail dédié se concentre sur les problèmes de compatibilité entre les modules CommonJS et les modules ES.</p>

<p>Cela signifie qu’on pourra utiliser la balise <code>script</code> avec <code>type=module</code> ainsi que des imports et des exports. D’autres fonctionnalités relatives aux modules sont dans les tuyaux. <a href="https://github.com/tc39/proposal-dynamic-import">La proposition concernant l’import dynamique</a> est au niveau 3 du processus de spécification. Il en va de même avec <a href="https://github.com/tc39/proposal-import-meta">import.meta</a> qui aidera à gérer certains cas d’utilisation pour Node.js. Enfin, <a href="https://github.com/domenic/package-name-maps">la proposition sur la résolution des modules</a> aidera à atténuer les différences entre les navigateurs et Node.js. En bref, le meilleur reste à venir.</p>

<h2>Remerciements</h2>

<p>Merci aux différentes personnes qui ont fourni leurs retours sur ce billet ou dont les écrits ou discussions ont aidé à sa rédaction : Axel Rauschmayer, Bradley Farias, Dave Herman, Domenic Denicola, Havi Hoffman, Jason Weathersby, JF Bastien, Jon Coppeard, Luke Wagner, Myles Borins, Till Schneidereit, Tobias Koppers, Yehuda Katz, les membres du groupe communautaire WebAssembly, les membres du groupe de travail pour les modules Node ainsi que les membres du TC39.</p>

<h2>À propos de <a href="http://code-cartoons.com/">Lin Clark</a></h2>

<p>Lin est ingénieure au sein de l’équipe Mozilla Developer Relations. Elle bricole avec JavaScript, WebAssembly, Rust, Servo et dessine des bandes dessinées à propos du code.</p>

par sphinx

Bidouilleux d’Web

Mise à jour à propos de la stratégie de localisation de MDN

 -  Décembre 2020 - 

Cet article est une traduction d’An update on MDN Web Docs’ localization strategy, écrit par Chris Mills. Si vous souhaitez contribuer aux (...)


Détails techniques sur la panne des modules complémentaires de Firefox

 -  Mai 2019 - 

Cet article est une traduction de Technical Details on the Recent Firefox Add-on Outage, écrit par Eric Rescorla, directeur technique de l’équipe (...)


Retrait de la confiance envers les certificats TLS de Symantec

 -  Août 2018 - 

Sites avec certificat émis par Symantecdans Chrome Canary à gaucheet Firefox Nightly à droiteLa plupart des grands éditeurs de navigateurs ont (...)


Le projet Quantum de Mozilla, des ambitions et des précisions

 -  Décembre 2017 - 

Après un premier billet qui traçait les grandes lignes du projet et sa philosophie (Un saut quantique pour le Web) nous disposons maintenant d’un (...)


Bienvenue au MozFest de Londres

 -  Juillet 2017 - 

Le MozFest, pour Mozilla Festival, tiendra sa 8ᵉ édition à Londres cet automne. Le mouvement du Web ouvert s’y retrouvera du vendredi 27 au dimanche (...)