Question 1Que se passe-t-il si tu charges l'export par défaut Cart avec import { Cart } from "./cart.js" ?
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'
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'
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'import | Ce que tu reçois | Nom côté import |
|---|---|---|
| import { withTax } from … | Export nommé | Le même que l'export |
| import Cart from … | Export par défaut | Au 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
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 en
.jssont chargés en ESM
- Peut importer avec
import stock from "./stock.cjs" - Écrire
requirelève uneReferenceError
stock.cjs— rend les valeurs publiques avecmodule.exportsreport.cjs— chargestock.cjsavecrequire
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ément | ESM | CommonJS |
|---|---|---|
| Charger | import { reserve } from "./stock.js" | const { reserve } = require("./stock.js") |
| Rendre public | export function reserve() { ... } | module.exports = { reserve: reserve } |
| Extension | Obligatoire 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) |
| Navigateur | Chargé avec type="module" | Pas pris en charge tel quel |
Vérification des connaissances
Répondez à chaque question une par une.
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" ?