Deux fichiers JavaScript posés dans une même page partagent tout : chacun voit les variables de l'autre, et une variable total déclarée dans le premier écrase celle du second. Cette absence de frontière a longtemps été la première source de pannes d'un projet un peu gros.
Les modules ES ont réglé le problème en donnant à chaque fichier sa propre portée, et une façon officielle de dire ce qu'il accepte de partager.
Définition
Un module ES est un fichier JavaScript dont les valeurs restent privées tant qu'il ne les publie pas avec export, et que les autres fichiers récupèrent avec import. C'est le système de modules officiel du langage, compris aussi bien par les navigateurs que par Node.js.
// compteur.js
export let vues = 0;
export function ajouter() {
vues += 1;
}
// app.js
import { vues, ajouter } from './compteur.js';
console.log(vues); // 0
ajouter();
console.log(vues); // 1La deuxième lecture affiche 1, ce qui surprend souvent : un import n'est pas une copie de la valeur, c'est un lien permanent vers la variable d'origine.
Ce qui sépare un module d'un simple script
- Une portée à lui : ce qui n'est pas exporté reste invisible de l'extérieur, sans devoir emballer le fichier dans une fonction.
- Le mode strict, toujours : pas besoin d'écrire
"use strict", un module l'applique d'office. - Une analyse avant exécution : le moteur lit tous les imports et exports avant de lancer la moindre ligne, ce qui lui permet de charger les dépendances en amont.
- Une seule évaluation : un module importé par dix fichiers n'est exécuté qu'une fois, et les dix reçoivent le même exemplaire.
- Un chargement différé : dans le navigateur, un module attend que la page soit analysée, comme le ferait l'attribut
defer.
Au niveau supérieur d'un module, this vaut undefined et non l'objet global. Du code ancien qui s'appuyait sur cette valeur pour installer une variable globale cesse donc de fonctionner une fois passé en module.
Activer les modules dans un projet
Dans une page web, l'attribut suffit : <script type="module" src="app.js"></script>. Le fichier et tout ce qu'il importe sont alors traités comme des modules.
Côté serveur, deux chemins mènent au même résultat : déclarer "type": "module" dans le fichier package.json, ce qui vaut pour tous les .js du projet, ou nommer le fichier avec l'extension .mjs, qui force le traitement en module quel que soit le reste.
Questions fréquentes
Pourquoi mon module échoue-t-il quand j'ouvre le fichier HTML à la main ?
Parce qu'un double-clic ouvre la page avec l'adresse file://, et que les modules obéissent aux règles de sécurité du chargement réseau, qui rejettent ce protocole. Il faut servir le dossier par un vrai serveur, ne serait-ce que celui d'un outil de développement, et l'erreur disparaît.
Peut-on mélanger modules ES et CommonJS ?
Un module ES peut importer un fichier CommonJS, avec quelques limites sur les exports nommés. L'inverse est plus délicat : un fichier CommonJS ne peut pas toujours réclamer un module ES de façon synchrone, et la solution universelle reste l'import() (import dynamique), qui fonctionne dans les deux sens.
Faut-il un outil de build pour utiliser des modules ES ?
Non : tous les navigateurs actuels les comprennent nativement, et une petite application peut s'en passer complètement. Un Bundler (empaqueteur) redevient utile quand le nombre de fichiers grimpe, car chaque import déclenche une requête réseau, ou quand vous voulez profiter du Tree shaking (élagage) et de la Minification.