Modules — import et export

export rend publiques les valeurs d'un fichier, import les charge : exports nommés ou par défaut, type="module", ESM ou CommonJS dans Node.js.

Si tu mets tout le code d'un écran de commande dans un seul fichier, le calcul de la TVA, le panier et le paiement finissent mélangés sur des centaines de lignes, et retrouver une fonction prend du temps. Même en découpant le code en plusieurs fichiers et en alignant des balises <script> (la balise qui charge un fichier JavaScript) dans le HTML, le code qui utilise une fonction ne dit pas dans quel fichier elle se trouve.

Cet article présente export et import, qui font circuler des valeurs entre fichiers.

import ne fonctionne pas dans la console

La console des exercices exécute ton code en le plaçant dans une seule fonction. import et export ne peuvent s'écrire qu'au niveau supérieur d'un fichier : ils y provoquent donc une SyntaxError. Le code de cet article est prévu pour s'exécuter dans Node.js ou dans le navigateur, réparti en plusieurs fichiers, et les résultats sont indiqués en commentaire en fin de ligne.

Partager des fonctions entre fichiers — les exports nommés

cart-page.js (l'écran du panier) et checkout.js (l'écran de paiement) ont tous deux besoin du même calcul de prix TTC. Si tu recopies la fonction dans les deux fichiers, un changement de taux t'oblige à corriger deux endroits, et si tu en oublies un, les deux écrans affichent des montants différents.

Les variables et fonctions déclarées dans un module (un fichier JavaScript qui échange des valeurs avec d'autres fichiers via import et export ; on verra plus loin comment le charger) ne sont pas visibles depuis les autres fichiers. Placer export devant une déclaration la rend publique : c'est ce qu'on appelle un export nommé. Le fichier qui l'utilise écrit le même nom entre accolades : import { withTax } from "./price.js";.

// ---- price.js ----
const TAX_RATE = 0.1;                              // Sans export : utilisé seulement dans price.js

export function withTax(price) {                   // Avec export : les autres fichiers peuvent l'importer
  return Math.floor(price * (1 + TAX_RATE));
}
export function formatYen(price) {
  return `${price} yens`;
}

// ---- cart-page.js ----
import { withTax, formatYen } from "./price.js";   // Liste entre accolades les noms que tu utilises
console.log(formatYen(withTax(4800)));             // 5280 yens

// Si tu écris dans l'import un nom qui n'est pas exporté
// import { TAX_RATE } from "./price.js";
// SyntaxError: The requested module './price.js' does not provide an export named 'TAX_RATE'
Les noms que tu peux importer ou non
import { withTax,formatYen }price.jsles exporteLes deux fonctionssont chargéesAffiche5280 yensimport{ TAX_RATE }TAX_RATEn'est pas exportéÉchoue avecune SyntaxErrorAucune ligne decart-page.js nes'exécute
Les deux noms de la rangée du haut sont exportés, mais pas TAX_RATE dans celle du bas. Tu ne peux importer que des noms exportés.

C'est withTax, à l'intérieur de price.js, qui lit TAX_RATE : cart-page.js obtient donc le prix TTC sans connaître le taux. Si le taux change, tu ne corriges que la ligne TAX_RATE de price.js, et tous les fichiers qui l'importent en tiennent compte.

Rendre publique une seule classe — l'export par défaut

Si cart.js contient la classe du panier, il exporte avant tout une seule classe, Cart. Tu verras souvent import Cart from "./cart.js"; dans le code des autres, mais si tu ajoutes des accolades comme à la section précédente, tu obtiens une SyntaxError.

Un export par défaut (un seul par fichier au maximum ; le fichier qui l'importe n'a pas besoin de reprendre son nom) s'écrit export default class Cart { ... }. Le fichier qui l'importe l'écrit sans accolades et peut lui donner le nom de son choix à la place de Cart.

// ---- cart.js ----
import { withTax } from "./price.js";
export const MAX_ITEMS = 20;                       // Des exports nommés peuvent cohabiter dans le même fichier
export default class Cart {                        // Un seul export par défaut par fichier
  #prices = [];
  add(price) { this.#prices.push(price); }
  get total() { return withTax(this.#prices.reduce((sum, price) => sum + price, 0)); }
}

// ---- checkout.js ----
import Cart, { MAX_ITEMS } from "./cart.js";       // Le défaut hors des accolades, les nommés dedans
import ShoppingCart from "./cart.js";              // Un export par défaut peut être importé sous un autre nom
const cart = new ShoppingCart();
cart.add(1200); cart.add(3600);
console.log(cart.total, MAX_ITEMS);                // 5280 20
console.log(Cart === ShoppingCart);                // true (c'est la même classe)

// Avec des accolades, il cherche un export nommé Cart
// import { Cart } from "./cart.js";
// SyntaxError: The requested module './cart.js' does not provide an export named 'Cart'
Les accolades déterminent l'export recherché
import Cartfrom "./cart.js"Sans accolades :export par défautexport defaultclass Cart existenew Cart()fonctionneimport { Cart }from "./cart.js"Avec accolades :export nomméAucun exportnommé CartÉchoue avecune SyntaxError
La rangée du bas cherche un export nommé Cart et échoue. Un import sans accolades reçoit l'export par défaut, pas un export nommé.

Le 'Cart' à la fin du message d'erreur est le nom de l'export nommé cherché et introuvable. Le message ne signale pas qu'un Cart par défaut existe : vérifie donc si tu as mis des accolades. Le tableau ci-dessous résume les formes d'import utilisées dans cet article.

Forme d'importCe que tu reçoisNom côté import
import { withTax } from …Export nomméLe même que l'export
import Cart from …Export par défautAu choix
import Cart, { MAX_ITEMS } from …Défaut et nomméCart et MAX_ITEMS (défaut en 1er)

Laisser le navigateur suivre les imports — type="module"

Le HTML de la page de paiement doit maintenant charger checkout.js. Avec un simple <script src="./checkout.js"></script>, Chrome signale SyntaxError: Cannot use import statement outside a module sur l'import de la ligne 1, et rien dans le fichier ne s'exécute.

Avec type="module" (un attribut de <script> qui exécute le fichier chargé comme un module), le navigateur suit les imports du fichier d'entrée et récupère aussi les autres fichiers. Ces liens d'import forment ce qu'on appelle le graphe de dépendances (quels fichiers importent quels fichiers).

// ---- index.html : n'indique que le fichier d'entrée, checkout.js ----
// <script type="module" src="./checkout.js"></script>

// ---- price.js ----
console.log("Exécution de price.js");              // Affiché une seule fois, même importé par 2 fichiers
const TAX_RATE = 0.1;
export function withTax(price) { return Math.floor(price * (1 + TAX_RATE)); }

// ---- cart.js ----
import { withTax } from "./price.js";
console.log("Exécution de cart.js");
export const cartTotal = withTax(4800);

// ---- checkout.js ----
import { cartTotal } from "./cart.js";
import { withTax } from "./price.js";              // Charge le même price.js que cart.js
console.log(`Total à payer : ${cartTotal + withTax(500)} yens`);

// Ordre dans la console : Exécution de price.js → Exécution de cart.js → Total à payer : 5830 yens
Le graphe de dépendances à partir de checkout.js
index.htmltype="module"checkout.js③ en derniercart.js② ensuiteprice.js① en 1er, une fois
Sauf celle qui part d'index.html, chaque flèche indique le sens d'un import. L'exécution commence par price.js, qui ne s'exécute qu'une fois alors que deux flèches y mènent.

Un module exécute les fichiers qu'il importe avant de s'exécuter lui-même. Le fichier d'entrée, checkout.js, attend que cart.js et price.js, qu'il importe, aient terminé : il s'exécute donc en dernier. Comme l'ordre est fixé par les imports, tu n'as pas à réfléchir à l'ordre des fichiers dans le HTML.

Ça ne fonctionne pas dans un HTML ouvert via file://

Si tu ouvres un fichier HTML par double-clic, en file://, Chrome considère que même les fichiers du même dossier viennent d'une autre origine (la source d'où un fichier est récupéré), et CORS (le mécanisme qui restreint les chargements depuis d'autres origines) bloque le chargement en type="module". Lance un serveur web, par exemple avec l'extension Live Server de VS Code, et ouvre la page depuis celui-ci.

Distinguer les formats dans Node.js — ESM et CommonJS

Dans du code écrit pour Node.js, tu croiseras des fichiers qui utilisent const { reserve } = require("./stock"); au lieu d'import. Si tu recopies une ligne de ce genre dans un fichier qui utilise import, une ReferenceError est levée à l'exécution.

À côté d'ESM (abréviation d'ES Modules, le format standard qui utilise import et export), il existe CommonJS (un format propre à Node.js qui charge avec require et rend les valeurs publiques avec module.exports). Node.js détermine le format d'un fichier d'après son extension et le champ "type" du package.json (le fichier de configuration du projet).

// ---- shop/package.json ----
// { "type": "module" }

// ---- shop/stock.cjs : son extension est .cjs, il est donc chargé en CommonJS ----
function reserve(count) { return `Quantité réservée : ${count}`; }
module.exports = { reserve: reserve, LIMIT: 3 };   // Toutes les valeurs publiques dans un seul objet

// ---- shop/report.cjs : un fichier CommonJS charge stock.cjs avec require ----
const { reserve, LIMIT } = require("./stock.cjs");
console.log(reserve(2), LIMIT);                    // Quantité réservée : 2 3

// ---- shop/checkout.js : "type": "module", il est donc chargé en ESM ----
import stock from "./stock.cjs";                   // Sans accolades : reçoit la valeur de module.exports
console.log(stock.reserve(1));                     // Quantité réservée : 1
const { LIMIT } = require("./stock.cjs");
// ReferenceError: require is not defined in ES module scope, you can use import instead
Les fichiers concernés par "type": "module"
Dossier shop — "type": "module" dans package.json
  • Les fichiers en .js sont chargés en ESM
checkout.js — ESM
  • Peut importer avec import stock from "./stock.cjs"
  • Écrire require lève une ReferenceError
Extension .cjs — CommonJS
  • stock.cjs — rend les valeurs publiques avec module.exports
  • report.cjs — charge stock.cjs avec require
Avec "type": "module", les fichiers .js du dossier sont chargés en ESM. Un fichier .cjs reste en CommonJS, même dans le même dossier.

import stock n'a pas d'accolades : il fonctionne donc comme l'import d'un export par défaut et reçoit l'objet module.exports. Ne mélange pas import et require dans un même fichier : tiens-t'en à l'un ou à l'autre. Le tableau ci-dessous montre comment écrire chaque format et les règles qui fixent le format d'un fichier.

ÉlémentESMCommonJS
Chargerimport { reserve } from "./stock.js"const { reserve } = require("./stock.js")
Rendre publicexport function reserve() { ... }module.exports = { reserve: reserve }
ExtensionObligatoire dans Node.js et le navigateur ("./stock" introuvable)Facultative ("./stock" trouve stock.js)
Fichiers chargés dans ce format.mjs, et .js sous "type": "module".cjs, et .js sans "type": "module" (sans "type", un .js avec import passe en ESM)
NavigateurChargé avec type="module"Pas pris en charge tel quel
QUIZ

Vérification des connaissances

Répondez à chaque question une par une.

Question 1Que se passe-t-il si tu charges l'export par défaut Cart avec import { Cart } from "./cart.js" ?

Question 2price.js est importé depuis deux endroits. Combien de fois le console.log placé au début du fichier s'affiche-t-il ?

Question 3Que se passe-t-il si tu appelles require dans checkout.js sous "type": "module" ?