Au fur et à mesure que notre application grandit, nous souhaitons la scinder en plusieurs fichiers, appelés âmodulesâ. Un module contient généralement une classe ou une bibliothèque de fonctions pour une tâche précise.
Pendant longtemps, JavaScript nâavait pas de module. Ce nâétait pas un problème, car au départ les scripts étaient petits et simples, il nâétait donc pas nécessaire.
Mais les scripts sont devenus de plus en plus complexes et la communauté a donc inventé diverses méthodes pour organiser le code en modules, des bibliothèques spéciales pour charger des modules à la demande.
Pour en nommer quelques-uns (pour des raisons historiques) :
- AMD â un des systèmes de modules les plus anciens, initialement mis en Åuvre par la bibliothèque require.js.
- CommonJS â le système de module créé pour Node.js
- UMD â un système de module supplémentaire, proposé comme universel, compatible avec AMD et CommonJS
Maintenant, tous ces éléments deviennent lentement du passé, mais nous pouvons toujours les trouver dans dâanciens scripts.
Le système de modules au niveau du langage est apparu dans la norme en 2015, a progressivement évolué depuis, et est désormais pris en charge par tous les principaux navigateurs et dans Node.js. Nous allons donc étudier les modules JavaScript modernes à partir de maintenant.
Quâest-ce quâun module?
Un module nâest quâun fichier. Un script est un module. Aussi simple que cela.
Les modules peuvent se charger mutuellement et utiliser des directives spéciales, export et import, pour échanger des fonctionnalités, appeler les fonctions dâun module dans un autre:
- Le mot-clé
exportlabelise les variables et les fonctions qui doivent être accessibles depuis lâextérieur du module actuel. importpermet lâimportation de fonctionnalités à partir dâautres modules.
Par exemple, si nous avons un fichier sayHi.js exportant une fonction
// ð sayHi.js
export function sayHi(user) {
alert(`Hello, ${user}!`);
}
â¦Un autre fichier peut lâimporter et lâutiliser:
// ð main.js
import {sayHi} from './sayHi.js';
alert(sayHi); // function...
sayHi('John'); // Hello, John!
La directive import charge le module qui a pour chemin ./sayHi.js par rapport au fichier actuel et affecte la fonction exportée sayHi à la variable correspondante.
Lançons lâexemple dans le navigateur.
Comme les modules prennent en charge des mots-clés et des fonctionnalités spéciales, nous devons indiquer au navigateur quâun script doit être traité comme un module, en utilisant lâattribut <script type="module">.
Comme ça:
export function sayHi(user) {
return `Hello, ${user}!`;
}<!doctype html>
<script type="module">
import {sayHi} from './say.js';
document.body.innerHTML = sayHi('John');
</script>Le navigateur extrait et évalue automatiquement le module importé (et, le cas échéant, ses importations), puis exécute le script.
Si vous essayez dâouvrir une page Web localement, via le protocole file://, vous verrez que les directives import/export ne fonctionnent pas. Utilisez un serveur Web local, tel que static-server ou utilisez la fonctionnalité âlive serverâ de votre éditeur, tel que VS Code Live Server Extension pour tester les modules.
Caractéristiques du module de base
Quâest-ce qui est différent dans les modules par rapport aux scripts ânormauxâ?
Il existe des fonctionnalités de base, valables à la fois pour le navigateur et le JavaScript côté serveur.
Toujours en mode âuse strictâ
Les modules fonctionnent toujours en mode strict. Par exemple. lâaffectation à une variable non déclarée donnera une erreur.
<script type="module">
a = 5; // error
</script>
Portée au niveau du module
Chaque module a sa propre portée globale. En dâautres termes, les variables et les fonctions globales dâun module ne sont pas visibles dans les autres scripts.
Dans lâexemple ci-dessous, deux scripts sont importés et hello.js essaie dâutiliser la variable user déclarée dans user.js. Il échoue, car il sâagit dâun module distinct (vous verrez lâerreur dans la console) :
alert(user); // pas de variable user (chaque module a des variables indépendantes)let user = "John";<!doctype html>
<script type="module" src="user.js"></script>
<script type="module" src="hello.js"></script>Les modules doivent export ce quâils veulent être accessible de lâextérieur et import ce dont ils ont besoin.
user.jsdevrait exporter la variableuser.hello.jsdevrait lâimporter depuis le moduleuser.js.
En dâautres termes, avec les modules, nous utilisons lâimport/export au lieu de nous appuyer sur des variables globales.
Ceci est la bonne variante :
import {user} from './user.js';
document.body.innerHTML = user; // Johnexport let user = "John";<!doctype html>
<script type="module" src="hello.js"></script>Dans le navigateur, si nous parlons de pages HTML, une portée de niveau supérieur indépendante existe également pour chaque <script type="module">.
Voici deux scripts sur la même page, tous deux type="module". Ils ne voient pas les variables de niveau supérieur de lâautre :
<script type="module">
// La variable est uniquement visible dans ce module
let user = "John";
</script>
<script type="module">
alert(user); // Error: user is not defined
</script>
Dans le navigateur, nous pouvons rendre une variable globale au niveau de la fenêtre en lâaffectant explicitement à une propriété window, par exemple window.user = "John".
Ensuite, tous les scripts la verront, Ã la fois avec type="module" et sans.
Cela dit, faire de telles variables globales est mal vu. Veuillez essayer de les éviter.
Un code de module est chargé la première fois lorsquâil est importé
Si le même module est importé dans plusieurs autres modules, son code nâest exécuté quâune seule fois, lors de la première importation. Ensuite, ses exportations sont données à tous les autres importateurs.
Lâévaluation ponctuelle a des conséquences importantes, dont nous devons être conscients.
Voyons quelques exemples.
Premièrement, si exécuter un code de module entraîne des effets secondaires, comme afficher un message, lâimporter plusieurs fois ne le déclenchera quâune seule fois â la première fois:
// ð alert.js
alert("Module is evaluated!");
// Importer le même module à partir de fichiers différents
// ð 1.js
import `./alert.js`; // le module est chargé
// ð 2.js
import `./alert.js`; // (n'affiche rien)
La deuxième importation ne montre rien, car le module a déjà été évalué.
Il y a une règle : le code du module de niveau supérieur doit être utilisé pour lâinitialisation, la création de structures de données internes spécifiques au module. Si nous devons rendre quelque chose appelable plusieurs fois, nous devons lâexporter en tant que fonction, comme nous lâavons fait avec sayHi ci-dessus.
Maintenant, considérons un exemple plus profond.
Disons quâun module exporte un objet:
// ð admin.js
export let admin = {
name: "John"
};
Si ce module est importé à partir de plusieurs fichiers, il nâest chargé que la première fois, un objet admin est créé, puis transmis à tous les autres importateurs.
Tous les importateurs obtiennent exactement le seul et unique objet admin:
// ð 1.js
import {admin} from './admin.js';
admin.name = "Pete";
// ð 2.js
import {admin} from './admin.js';
alert(admin.name); // Pete
// 1.js et 2.js font référence au même objet admin
// Les modifications apportées dans 1.js sont visibles dans 2.js
Comme vous pouvez le voir, lorsque 1.js modifie la propriété name dans le admin importé, alors 2.js peut voir le nouveau admin.name.
Câest précisément parce que le module nâest exécuté quâune seule fois. Les exportations sont générées, puis partagées entre les importateurs, donc si quelque chose change lâobjet admin, les autres modules le verront.
Un tel comportement est en fait très pratique, car il nous permet de configurer des modules.
En dâautres termes, un module peut fournir une fonctionnalité générique qui nécessite une configuration. Par exemple. lâauthentification a besoin dâinformations dâidentification. Ensuite, il peut exporter un objet de configuration en attendant que le code externe lui soit affecté.
Voici le modèle classique :
- Un module exporte certains moyens de configuration, par exemple un objet de configuration.
- Lors de la première importation, nous lâinitialisons, écrivons dans ses propriétés. Le script dâapplication de niveau supérieur peut le faire.
- Dâautres importations utilisent le module.
Par exemple, le module admin.js peut fournir certaines fonctionnalités (par exemple, lâauthentification), mais sâattend à ce que les informations dâidentification entrent dans lâobjet config de lâextérieur :
// ð admin.js
export let config = { };
export function sayHi() {
alert(`Ready to serve, ${config.user}!`);
}
Ici, admin.js exporte lâobjet config (initialement vide, mais peut également avoir des propriétés par défaut).
Ensuite, dans init.js, le premier script de notre application, nous en importons config et définissons config.user :
// ð init.js
import {config} from './admin.js';
config.user = "Pete";
â¦Maintenant, le module admin.js est configuré.
Further importers can call it, and it correctly shows the current user:
// ð another.js
import {sayHi} from './admin.js';
sayHi(); // Prêt à être utilisé, Pete!
import.meta
Lâobjet import.meta contient les informations sur le module actuel.
Son contenu dépend de lâenvironnement. Dans le navigateur, il contient lâURL du script ou une URL de page Web actuelle si elle est en HTML:
<script type="module">
alert(import.meta.url); // URL du script
// pour un script en ligne - l'URL de la page HTML actuelle
</script>
Dans un module, âthisâ nâest pas défini
Câest un peu une caractéristique mineure, mais pour être complet, nous devrions le mentionner.
Dans un module, lâobjet global this est indéfini.
Comparez-le à des scripts sans module, là où il est un object global:
<script>
alert(this); // window
</script>
<script type="module">
alert(this); // undefined
</script>
Fonctionnalités spécifiques au navigateur
Il existe également plusieurs différences de scripts spécifiques au navigateur avec type="module" par rapport aux scripts classiques.
Vous devriez peut-être ignorer cette section pour lâinstant si vous lisez pour la première fois ou si vous nâutilisez pas JavaScript dans un navigateur.
Les modules sont différés
Les modules sont toujours différés, avec le même effet que lâattribut defer (décrit dans le chapitre Les scripts: async, defer), pour les scripts externes et intégrés.
En dâautres termes:
- télécharger des modules externe
<script type="module" src="...">ne bloque pas le traitement HTML, ils se chargent en parallèle avec dâautres ressources. - Les modules attendent que le document HTML soit complètement prêt (même sâils sont minuscules et se chargent plus rapidement que le HTML), puis sâexécutent.
- lâordre relatif des scripts est maintenu : les scripts qui entrent en premier dans le document sont exécutés en premier.
Comme effet secondaire, les modules âvoientâ toujours la page HTML entièrement chargée, y compris les éléments HTML situés en dessous.
Par exemple:
<script type="module">
alert(typeof button); // object: le script peut 'voir' le bouton ci-dessous
// à mesure que les modules sont différés, le script s'exécute après le chargement de la page entière
</script>
Comparez au script habituel ci-dessous:
<script>
alert(typeof button); // button est undefined, le script ne peut pas voir les éléments ci-dessous
// les scripts normaux sont exécutés immédiatement, avant que le reste de la page ne soit traité
</script>
<button id="button">Button</button>
Remarque : le deuxième script fonctionne avant le premier ! Nous verrons donc dâabord undefined, puis object.
Câest parce que les modules sont différés, nous attendons donc que le document soit traité. Les scripts réguliers sâexécutent immédiatement, nous avons donc vu son resultat en premier.
Lorsque nous utilisons des modules, nous devons savoir que la page HTML apparaît lors de son chargement et que les modules JavaScript sâexécutent par la suite, afin que lâutilisateur puisse voir la page avant que lâapplication JavaScript soit prête. Certaines fonctionnalités peuvent ne pas encore fonctionner. Nous devons définir des âindicateurs de chargementâ ou veiller à ce que le visiteur ne soit pas confus par cela.
Async fonctionne sur les scripts en ligne
Pour les scripts non modulaires, lâattribut async ne fonctionne que sur les scripts externes. Les scripts asynchrones sâexécutent immédiatement lorsquâils sont prêts, indépendamment des autres scripts ou du document HTML.
Pour les modules, cela fonctionne sur tous les scripts.
Par exemple, le script ci-dessous est async et nâattend donc personne.
Il effectue lâimportation (récupère ./analytics.js) et sâexécute lorsquâil est prêt, même si le document HTML nâest pas encore terminé ou si dâautres scripts sont toujours en attente.
Câest bon pour une fonctionnalité qui ne dépend de rien, comme des compteurs, des annonces, des écouteurs dâévénements au niveau du document.
<!-- toutes les dépendances sont récupérées (analytics.js) et le script s'exécute -->
<!-- il n'attend pas le document ou d'autres balises <script> -->
<script async type="module">
import {counter} from './analytics.js';
counter.count();
</script>
Scripts externes
Les scripts externes de type="module" se distinguent sous deux aspects:
-
Les scripts externes avec le même
srcne sâexécutent quâune fois:<!-- le script my.js est récupéré et exécuté une seule fois --> <script type="module" src="my.js"></script> <script type="module" src="my.js"></script> -
Les scripts externes extraits dâune autre origine (par exemple, un autre site) nécessitent CORS en-têtes, comme décrit dans le chapitre Fetch: Requêtes Cross-Origin. En dâautres termes, si un module est extrait dâune autre origine, le serveur distant doit fournir un en-tête
Access-Control-Allow-Originpermettant lâextraction.<!-- another-site.com doit fournir Access-Control-Allow-Origin --> <!-- sino, le script ne sera pas exécuté --> <script type="module" src="http://another-site.com/their.js"></script>Cela garantit une meilleure sécurité par défaut.
Aucun module ânuâ autorisé
Dans le navigateur, import doit avoir une URL relative ou absolue. Les modules sans chemin sont appelés modules ânusâ. De tels modules ne sont pas autorisés lors de lâimportation.
Par exemple, cette import nâest pas valide:
import {sayHi} from 'sayHi'; // Error, "bare" module
// le module doit avoir un chemin, par exemple './sayHi.js'
Certains environnements, tels que Node.js ou les outils de bundle autorisent les modules nus, sans chemin dâaccès, car ils disposent de moyens propres de recherche de modules trouver des modules et des hooks pour les ajuster. Mais les navigateurs ne supportent pas encore les modules nus.
Compatibilité, ânomoduleâ
Les anciens navigateurs ne comprennent pas type="module". Les scripts de type inconnu sont simplement ignorés. Pour eux, il est possible de fournir une solution de secours en utilisant lâattribut nomodule :
<script type="module">
alert("Runs in modern browsers");
</script>
<script nomodule>
alert("Modern browsers know both type=module and nomodule, so skip this")
alert("Old browsers ignore script with unknown type=module, but execute this.");
</script>
Construire des outils
Dans la vie réelle, les modules de navigateur sont rarement utilisés sous leur forme âbruteâ. Généralement, nous les regroupons avec un bundle tel que Webpack et les déployons sur le serveur de production.
Lâun des avantages de lâutilisation des bundles est â quâils permettent de mieux contrôler la façon dont les modules sont résolus, permettant ainsi des modules nus et bien plus encore, comme les modules CSS / HTML.
Les outils de construction font ce qui suit:
- Prenons un module âprincipalâ, celui qui est destiné à être placé dans
<script type="module">dans le HTML. - Analyser ses dépendances : importations puis importations dâimportations etc.
- Construire un seul fichier avec tous les modules (ou plusieurs fichiers configurables), en remplaçant les appels
importnatifs par des fonctions dâassemblage, pour que cela fonctionne. Les types de modules âspéciauxâ tels que les modules HTML/CSS sont également pris en charge. - Dans le processus, dâautres transformations et optimisations peuvent être appliquées:
- Le code inaccessible est supprimé.
- Les exportations non utilisées sont supprimées (âtree-shakingâ).
- Les instructions spécifiques au développement telles que
consoleet ledebuggersont supprimées. - La syntaxe JavaScript moderne et ultramoderne peut être transformée en une ancienne version dotée de fonctionnalités similaires avec Babel.
- Le fichier résultant est minifié (espaces supprimés, variables remplacées par des noms plus courts, etc.).
Si nous utilisons des outils dâensemble, alors que les scripts sont regroupés dans un seul fichier (ou quelques fichiers), les instructions import/export contenues dans ces scripts sont remplacées par des fonctions spéciales de regroupeur. Ainsi, le script âfourniâ résultant ne contient aucune import/export, il ne nécessite pas type="module", et nous pouvons le mettre dans un script standard:
<!-- En supposant que nous ayons bundle.js d'un outil tel que Webpack -->
<script src="bundle.js"></script>
Cela dit, les modules natifs sont également utilisables. Nous nâutilisons donc pas Webpack ici: vous pourrez le configurer plus tard.
Résumé
Pour résumer, les concepts de base sont les suivants:
- Un module est un fichier. Pour que
import/exportfonctionne, les navigateurs ont besoin de<script type="module">. Les modules ont plusieurs différences:- Différé par défaut.
- Async fonctionne sur les scripts en ligne.
- Pour charger des scripts externes dâune autre origine (domain/protocol/port), des en-têtes CORS sont nécessaires.
- Les scripts externes en double sont ignorés.
- Les modules ont leur propre portée globale et leurs fonctionnalités dâéchange via
import/export. - Les modules utilisent toujours
use strict. - Le code des modules est exécuté une seule fois. Les exportations sont créées une fois et partagées entre les importateurs
Lorsque nous utilisons des modules, chaque module implémente la fonctionnalité et lâexporte. Nous utilisons ensuite import pour lâimporter directement là où il le faut. Le navigateur charge et exécute les scripts automatiquement.
En production, les gens utilisent souvent des âbundlersâ tels que Webpack qui regroupe des modules pour des raisons de performances ou pour dâautres raisons.
Dans le chapitre suivant, nous verrons plus dâexemples de modules et comment des choses peuvent être importé / exporté.
Commentaires
<code>, pour plusieurs lignes â enveloppez-les avec la balise<pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepenâ¦)