JavaScript : Import et export dans les modules ES6
Pour faire les exemples de ce tutoriel, vous avez besoin d'un serveur que ce soit un serveur distant ou un serveur local tel que le Wamp.
#1 Des modules pour quoi faire ?
On va parler ici des modules pour le code JavaScript qui s'exécute du côté du navigateur. Ce sont les modules décrits dans la norme ECMAScript. Du côté serveur, Nodejs a son propre système de modularisation du code. Un système qui n'est donc pas dans la norme.
Au départ, JavaScript nous a permis de rendre nos documents interactifs. Ensuite, on s'en est servi pour que, sans recharger le document, on puisse communiquer avec des serveurs. Les scripts sont devenus de plus en plus gros, ils sont devenus de véritables applications et aussi des librairies. On peut légitimement penser que les développeurs ont ressenti le besoin de structurer leur code. Pour faire cela, un développeur commence par découper son application en plusieurs fichiers. Les modules JavaScript ES6 ont adopté le principe d'un module par fichier. On va voir ce que les modules apportent de plus :
- Le mode strict est activé par défaut dans un module.
- Un module va avoir sa propre portée. Ca c'est capital. Ca veut dire que tout ce que vous allez déclarer dans le module sera par défaut local au module. Pour rendre visible une variable, une fonction ou une classe il faudra le dire, il faudra l'exporter. Cacher des informations dans un logiciel a toujours été nécessaire. Ca permet d'isoler les couches logicielles les unes des autres. Jusqu'à ES6, en JavaScript on utilisait pour faire cela des closures qui étaient en fait un moyen détourné, mais efficace.
- Un module vous permet de créer un espace de nommage. Lorsque vous importez des librairies ou lorsque vous travaillez à plusieurs développeurs sur une même application, il arrive que vous rentriez en conflit avec une autre variable, fonction ou classe qui porte un nom que vous-même, vous utilisez. Avec les modules, vous avez plusieurs façons de solutionner ce problème.
- Les modules vous simplifient le chargement de l'application. Généralement, il vous suffira d'implanter un seul module "principal" au moyen d'une seule balise script dans votre html. Ce module "principal" ira chercher les autres modules et chargera l'ensemble de l'application.
- Vous pouvez charger un module de manière dynamique. Le module est chargé uniquement si une ressource du module est utilisée au moment de l'exécution de votre application. Cette possibilité pourra vous éviter de charger la totalité d'un gros code au premier chargement de votre page web.
- Vous pouvez charger un module de manière asynchrone.
#2 Un exemple pour expliquer le principe des modules
Le principe est basé sur l'utilisation de deux mots réservés :
- Dans le Module A, il faut utiliser le mot réservé
export
, pour exporter une information. Ici x peut-être une variable, une fonction ou une classe. - Dans le Module B, il faut utiliser le mot réservé
import
, pour importer l'information qui a été exportée dans A.
Nous allons écrire un petit exemple à trois modules :
tirage.js
: il contient une fonctionfaireTirage(tab)
qui prend en argument un tableau. La fonction est exportée. Pour cela, il suffit d'écrireexport
devantfunction
. Dans la fonction, le tableau est parcouru et on procède à un tirage aléatoire pour affecter chaque valeur du tableau, soit àtrue
, soit àfalse
.affichage.js
: il contient une fonctionafficherResultat(tab)
qui prend en argument un tableau. Ce tableau est affiché par unconsole.log()
. La fonction est elle aussi exportée.main.js
: dans ce module, on importe les deux fonctions puis on déclare le tableau sur lequel on va travailler. Ensuite, on appelle les deux fonctions en passant le tableau en argument. Pour importer les fonctions, il faut écrireimport
suivi du nom de la fonction que l'on place entre une paire d'accolades. Ensuite, on écritfrom
et on fait suivre par une chaîne de caractères qui contient le nom du fichier écrit en relatif. Commencez toujours vos chemins en relatif en citant le répertoire local avec./
. Notez qu'il n'est pas nécessaire d'exporter le tableau puisqu'il est passé en argument des fonctions.- Pour finir, on prend une structure minimale html pour implanter notre script. Notez que l'on se contente d'implanter
main.js
. C'est lui qui ira chercher les modules dont il a besoin pour les charger. Notez que c'est l'attributtype='module'
qui informe le navigateur qu'il est en train de charger le code d'un module JavaScript. En effet, dans un fichier d'extension.js
rien ne spécifie que le fichier contient le code d'un module.
Ci-dessous, voyons ce qui se passe lorsque l'on exécute le script. Avec le debugger, on met un point d'arrêt dans chacune des fonctions des modules pour examiner le scope de l'application, lorsque qu'elle est sur un point d'arrêt du module tirage
, puis lorsqu'elle est sur un point d'arrêt du module affichage
. Vous constatez que chaque module a son propre scope (* sa propre unité locale de portée).
#3 Export d'une fonction
Vous avez deux syntaxes à disposition pour exporter une fonction :
- Soit, vous écrivez le mot réservé
export
devant la déclaration de la fonction. - Soit, vous écrivez
export
suivi du nom de la fonction placé entre une paire d'accolades. - La syntaxe ne changera rien à la manière d'importer.
#4 Exports et imports groupés
Vous pouvez faire toutes vos exportations de manière groupée en fin de fichier. C'est ce que je vous conseille. La démarche est logique. Une fois que vous avez terminé votre module, vous décidez d'exporter ce que vous voulez.
Dans la même logique, vous pouvez grouper vos importations en début de fichier selon les besoins du module que vous allez coder.
N'oubliez pas que l'adresse du fichier ici './mod/module.js'
est une adresse relative exprimée depuis l'emplacement du module main.js
. Autrement dit, c'est le chemin que main.js
doit faire pour trouver module.js
. Ici, j'ai mis le fichier module.js
dans un répertoire que j'ai appelé mod
. Le chemin depuis main.js
est donc './mod/module.js'
.
#5 Export et import par défaut.
Vous avez droit à une et une seule exportation par défaut. Ca peut vous servir par exemple pour l'export d'une fonction anonyme.
#6 Renommage des imports et des exports
Vous pouvez avoir besoin de renommer pour résoudre des conflits de nommage. Ces conflits apparaissent le plus souvent lorsque vous utilisez une librairie ou lorsque vous travaillez à plusieurs développeurs sur un même projet.
Pour procéder à un renommage des imports, vous n'avez pas besoin d'avoir le contrôle sur ce que vous importez. Je veux dire que c'est votre code que vous allez modifié par celui que vous importez. Ce qui fait que c'est la méthode la plus utilisée.
Je reprends l'exemple que j'avais utilisé pour expliquer le principe de l'import / export dans les modules au début du tutoriel. Je vais changer au moment de l'import le nom de la fonction faireTirage
par celui de tirer
. Pour cela, j'écris import {faireTirage as tirer}
. Je le fais dans main.js
le fichier sur lequel j'ai la main. Je renomme également afficherResultat
en afficher
. Je ne touche ni à tirage.js
ni à affichage.js
. Donc si je n'avais pas la main sur tirage.js
et affichage.js
le renommage à l'import serait une solution en cas de conflit de nommage.
Pour procéder à un renommage des exports, il faut que vous ayez la main sur le code dans lequel vous voulez justement placer vos exports. Ce n'est pas toujours le cas. Aussi, vous serez souvent amené à plutôt renommer les imports avec la méthode que nous venons de voir. Toutefois ci-dessous je vous montre comment renommer des exports.
- Dans le fichier
affichage.js
, j'ai placé une instructionexport {afficherResultat as afficher}
. Je renomme la fonctionafficherResultat
enafficher
et j'exporteafficher
. - Dans le fichier
tirage.js
, j'ai placé une instructionexport {faireTirage as tirer}
. Je renomme la fonctionfaireTirage
entirer
et j'exportetirer
. - Maintenant, il me reste à importer dans
main.js
les deux fonctions en utilisant leurs nouveaux noms,tirer
etafficher
.
#7 Importer le module dans un objet
Avec cette syntaxe, tous les exports sont récupérés et transformés soit en propriétés, soit en méthodes qui doivent être préfixées par le nom du module. Ci-dessous, j'ai exporté tout le contenu du fichier tirage.js
dans un objet que j'ai appelé Tirage
. J'en ai fait de même, avec affichage.js
dans un objet que j'ai appelé Affichage
. Pour cela, j'ai utilisé cette syntaxe import * as Tirage from './tirage.js'
. Pareil pour affichage. Ensuite, j'appelle mes fonctions en les préfixant avec le nom du module. Pour la fonction faireTirage
ça donne Tirage.faireTirage
. Je vérifie à la fin que Tirage
est bien un objet avec un typeof
de Tirage
.
#8 Charger un module dynamiquement
Je vais ajouter un module supplémentaire que je vais charger en dernier et je vais le charger dynamiquement, c'est-à-dire en cours d'exécution. Vous avez le code de ce module ci-dessous. Il fait juste un console.log
d'une ligne de traits pointillés.
Je vais importer ce fichier dynamiquement dans main.js
. Pour cela je mets en place une promesse.
Avec le debugger, je mets en place un point d'arrêt sur le then
de la promesse à la ligne 12 de main.js
. Je lance l'exécution. Je vais ensuite dans l'onglet Network de la console, je filtre et je coche JS. Vous pouvez constater sur l'image ci-dessous que seuls les fichiers tirage.js
et affichage.js
sont chargés. Le fichier soulignage.js
lui n'est pas chargé.
Ensuite, je reprends l'exécution et je passe le point d'arrêt et vous pouvez constater sur l'image ci-dessous, que maintenant le fichier soulignage.js
est chargé. Vous profitez en fait de l'effet asynchrone de la promesse.