Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 1138 minDébutant

Modules ES et classes : organiser le code

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • découper un projet en modules avec export et import ;
  • charger un module dans une page et éviter les erreurs de chargement ;
  • organiser une application en couches : règles métier, affichage, données ;
  • lire et écrire une classe JavaScript : constructeur, méthodes, champs privés.

4 exercices corrigés1 schéma

Un module ES (ECMAScript) est un fichier JavaScript qui choisit ce qu'il partage avec export et ce qu'il utilise des autres fichiers avec import. Découper une application en modules donne à chaque fichier une seule responsabilité : les règles métier, l'affichage, le stockage, les échanges avec le serveur. Ce chapitre montre la syntaxe des modules, leur chargement dans une page, l'organisation complète de Gestock, l'utilisation de bibliothèques avec npm et Vite, puis la syntaxe des classes JavaScript.

export et import

Chaque module a sa propre portée : ce qu'il déclare reste privé, sauf ce qu'il exporte.

js
// js/stock.js
export const REF_VALIDE = /^[A-Z]{3}-\d{2,3}$/;

export function estEnAlerte(piece) {
  return piece.quantite <= piece.seuil;
}

export function valeurStock(pieces) {
  return pieces.reduce((total, p) => total + p.quantite * p.prix, 0);
}

function outilInterne() {
  // pas exportée : invisible depuis les autres fichiers
}
js
// js/vue.js
import { estEnAlerte, valeurStock } from './stock.js';
ÉcritureRôle
export function f() {}, export const X = …Export nommé : on peut en avoir autant qu'on veut
export { f, g }Exporter en fin de fichier des noms déjà déclarés
export default function () {}Export par défaut : un seul par module
import { f, g } from './a.js'Importer des exports nommés, avec leur nom exact
import { f as filtre } from './a.js'Importer en renommant
import * as stock from './stock.js'Tout importer dans un objet : stock.valeurStock(…)
import maFonction from './a.js'Importer l'export par défaut, sous le nom de ton choix

Préfère les exports nommés : l'éditeur les propose à l'autocomplétion, et un renommage se propage partout. L'export par défaut est surtout une convention de certains outils (un composant React par fichier, par exemple).

Charger un module dans la page

La page ne charge que le point d'entrée, avec type="module" ; le navigateur suit lui-même les import et télécharge les autres fichiers.

html
<script type="module" src="js/main.js"></script>

Un script de type module se comporte différemment d'un script classique :

PropriétéConséquence
Différé comme deferIl s'exécute après la lecture du HTML : querySelector trouve les éléments
Mode strict automatiqueLes erreurs silencieuses deviennent des erreurs visibles (variable non déclarée, this à undefined)
Portée propreSes variables ne sont pas globales : une console ne les voit pas directement
Exécuté une seule foisImporté par dix fichiers, un module ne s'exécute qu'une fois
Chargé selon les règles CORSOuvert en file://, il est bloqué : il faut un serveur (chapitre 1)
await autorisé au premier niveauconst pieces = await chargerPiecesApi(…) directement dans le module

Les erreurs de chargement que tu rencontreras :

Message dans la consoleCause
Cannot use import statement outside a moduleLa balise <script> n'a pas type="module"
Access to script at 'file:///…' from origin 'null' has been blocked by CORS policyLa page est ouverte par double-clic : sers-la par HTTP
Une ligne rouge GET http://…/js/Stock.js 404 (Not Found), et rien ne s'exécuteLe chemin d'un import est faux (ici, la casse) : le fichier n'existe pas
Failed to load module script: Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of "text/html"Le serveur a renvoyé une page HTML au lieu du fichier : chemin faux sur un hébergement qui redirige toute adresse inconnue vers index.html
The requested module './stock.js' does not provide an export named 'valeurStok'Faute de frappe dans le nom importé, ou fonction non exportée

Organiser Gestock en modules

Gestock est découpé en cinq fichiers, chacun avec une responsabilité :

text
gestock/
├── index.html             la page, donnée en entier plus bas
├── package.json           créé par Vite : scripts dev, build et preview
├── public/
│   └── data/
│       └── pieces.json    le stock de départ, copié tel quel dans dist/
└── js/
    ├── main.js            point d'entrée : état, écouteurs, démarrage
    ├── stock.js           règles métier, aucun accès au DOM
    ├── vue.js             affichage : construit les éléments du DOM
    ├── stockage.js        localStorage et JSON
    ├── api.js             fetch, statut HTTP, délai maximal
    └── stock.test.js      tests de stock.js (chapitre 14)
index.html<script type="module">main.jsétat, écouteurs, démarrageimportapi.jsfetch, statut HTTP,délai maximalstockage.jslocalStorageet JSONvue.jsle DOM :affichagestock.jsrègles métier,aucun DOMstock.test.jstests VitestChaque flèche est un import.stock.js n'utilise ni le DOM ni le navigateur :les tests le vérifient dans Node.
Chaque fichier a une responsabilité ; les règles métier ne dépendent de rien, ce qui les rend testables

Le principe : les dépendances vont dans un seul sens. stock.js ne connaît ni la page ni le navigateur ; vue.js sait afficher mais ne décide de rien ; main.js relie le tout. Si demain Réparinfo veut une application mobile, stock.js se réutilise tel quel ; si le design change, seul vue.js bouge. Tu retrouveras cette séparation des responsabilités, à l'échelle d'une application entière, dans le cours Architecture MVC.

Voici la page index.html complète. Chaque élément que main.js va chercher avec querySelector y a son id, et chaque champ du formulaire a son label, son attribut name (lu par FormData) et son paragraphe d'erreur relié par aria-describedby :

html
<!doctype html>
<html lang="fr">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Gestock : le stock de Réparinfo</title>
  <style>
    tr.alerte { background: #fdecee; font-weight: 600; }
    .erreur { color: #b00020; margin: 0 0 0.75rem; }
    input[aria-invalid="true"] { border-color: #b00020; }
  </style>
</head>
<body>
  <header>
    <h1>Gestock</h1>
    <p id="resume" aria-live="polite"></p>
  </header>

  <main>
    <section aria-labelledby="titre-liste">
      <h2 id="titre-liste">Pièces en stock</h2>
      <label for="recherche">Rechercher une pièce</label>
      <input type="search" id="recherche" autocomplete="off">
      <input type="checkbox" id="alertes-seules">
      <label for="alertes-seules">Seulement les pièces en alerte</label>

      <table>
        <thead>
          <tr>
            <th scope="col">Référence</th>
            <th scope="col">Nom</th>
            <th scope="col">Quantité</th>
            <th scope="col">Prix</th>
            <th scope="col">Actions</th>
          </tr>
        </thead>
        <tbody id="liste-pieces"></tbody>
      </table>
    </section>

    <section aria-labelledby="titre-ajout">
      <h2 id="titre-ajout">Ajouter une pièce</h2>
      <form id="form-piece" novalidate>
        <label for="ref">Référence</label>
        <input id="ref" name="ref" required aria-describedby="erreur-ref">
        <p id="erreur-ref" class="erreur"></p>

        <label for="nom">Nom</label>
        <input id="nom" name="nom" required maxlength="60" aria-describedby="erreur-nom">
        <p id="erreur-nom" class="erreur"></p>

        <label for="categorie">Catégorie</label>
        <select id="categorie" name="categorie" aria-describedby="erreur-categorie">
          <option>Stockage</option>
          <option>Mémoire</option>
          <option>Alimentation</option>
          <option>Écran</option>
          <option>Consommable</option>
        </select>
        <p id="erreur-categorie" class="erreur"></p>

        <label for="quantite">Quantité</label>
        <input id="quantite" name="quantite" inputmode="numeric" required aria-describedby="erreur-quantite">
        <p id="erreur-quantite" class="erreur"></p>

        <label for="seuil">Seuil d'alerte</label>
        <input id="seuil" name="seuil" inputmode="numeric" required aria-describedby="erreur-seuil">
        <p id="erreur-seuil" class="erreur"></p>

        <label for="prix">Prix unitaire (€)</label>
        <input id="prix" name="prix" inputmode="decimal" required aria-describedby="erreur-prix">
        <p id="erreur-prix" class="erreur"></p>

        <button type="submit">Ajouter</button>
      </form>
      <p id="message" role="status"></p>
    </section>
  </main>

  <script type="module" src="js/main.js"></script>
</body>
</html>

Trois choix à remarquer. La recherche et la case à cocher sont hors du formulaire : FormData ne les lit pas, et Entrée dans la recherche n'envoie rien. La catégorie est un <select> sans option vide : elle a toujours une valeur, que creerPiece recopie. Enfin, #resume et #message existent, vides, dès le chargement : c'est la condition pour que les lecteurs d'écran annoncent leurs changements (chapitre 13). Le CSS tient ici en trois règles ; dans un vrai projet, range-le dans un fichier style.css.

Le fichier stock.js complet rassemble les règles métier écrites aux chapitres 5 et 8. Aucune de ses fonctions ne touche au DOM ni au navigateur :

js
// Les règles métier de Gestock : aucun accès au DOM ni au navigateur.

export const REF_VALIDE = /^[A-Z]{3}-\d{2,3}$/;

// Transforme « 42,9 » ou « 42.9 » en nombre ; NaN si la saisie n'est pas un nombre.
export function lireNombre(saisie) {
  const texte = String(saisie).trim().replace(',', '.');
  return texte === '' ? NaN : Number(texte);
}

export function estEnAlerte(piece) {
  return piece.quantite <= piece.seuil;
}

export function valeurStock(pieces) {
  return pieces.reduce((total, p) => total + p.quantite * p.prix, 0);
}

export function filtrerPieces(pieces, texte = '', alertesSeules = false) {
  const recherche = texte.trim().toLowerCase();
  return pieces.filter((p) => {
    const correspond =
      p.nom.toLowerCase().includes(recherche) || p.ref.toLowerCase().includes(recherche);
    return correspond && (!alertesSeules || estEnAlerte(p));
  });
}

export function ajusterQuantite(pieces, ref, delta) {
  return pieces.map((p) =>
    p.ref === ref ? { ...p, quantite: Math.max(0, p.quantite + delta) } : p,
  );
}

// Renvoie un objet { champ: message } : vide si la saisie est valide.
export function validerPiece(saisie, pieces) {
  const erreurs = {};
  const ref = saisie.ref.trim().toUpperCase();
  if (!REF_VALIDE.test(ref)) {
    erreurs.ref = 'Format attendu : trois lettres, un tiret, 2 ou 3 chiffres (ex. SSD-512).';
  } else if (pieces.some((p) => p.ref === ref)) {
    erreurs.ref = `La référence ${ref} existe déjà.`;
  }
  const nom = saisie.nom.trim();
  if (nom.length < 2 || nom.length > 60) {
    erreurs.nom = 'Le nom doit faire entre 2 et 60 caractères.';
  }
  const quantite = lireNombre(saisie.quantite);
  if (!Number.isInteger(quantite) || quantite < 0) {
    erreurs.quantite = 'La quantité est un nombre entier positif ou nul.';
  }
  const seuil = lireNombre(saisie.seuil);
  if (!Number.isInteger(seuil) || seuil < 0) {
    erreurs.seuil = "Le seuil d'alerte est un nombre entier positif ou nul.";
  }
  const prix = lireNombre(saisie.prix);
  if (!(prix > 0)) {
    erreurs.prix = 'Le prix est un nombre strictement positif.';
  }
  return erreurs;
}

export function creerPiece(saisie) {
  return {
    ref: saisie.ref.trim().toUpperCase(),
    nom: saisie.nom.trim(),
    categorie: saisie.categorie,
    quantite: lireNombre(saisie.quantite),
    seuil: lireNombre(saisie.seuil),
    prix: lireNombre(saisie.prix),
  };
}

Le stock de départ, public/data/pieces.json, est du JSON strict : guillemets doubles partout, pas de virgule après le dernier élément (chapitre 6).

json
[
  { "ref": "SSD-512", "nom": "SSD NVMe 512 Go", "categorie": "Stockage", "quantite": 12, "seuil": 5, "prix": 42.9 },
  { "ref": "RAM-16", "nom": "Barrette DDR4 16 Go", "categorie": "Mémoire", "quantite": 3, "seuil": 4, "prix": 38.5 },
  { "ref": "CHG-65", "nom": "Chargeur USB-C 65 W", "categorie": "Alimentation", "quantite": 0, "seuil": 2, "prix": 24 },
  { "ref": "ECR-156", "nom": "Dalle 15,6 pouces", "categorie": "Écran", "quantite": 2, "seuil": 1, "prix": 89 },
  { "ref": "PAT-01", "nom": "Pâte thermique", "categorie": "Consommable", "quantite": 25, "seuil": 10, "prix": 6.5 }
]

Les deux derniers modules sont déjà complets : stockage.js au chapitre 9 et api.js, avec sa fonction chargerPiecesApi, au chapitre 10.

Voici main.js dans sa version finale, qui rassemble tout ce que les chapitres 7 à 10 ont construit :

js
// Point d'entrée : l'état de l'application, les écouteurs, le démarrage.
import { filtrerPieces, ajusterQuantite, validerPiece, creerPiece } from './stock.js';
import { afficherPieces, afficherResume, afficherErreurs } from './vue.js';
import { chargerPieces, sauvegarderPieces } from './stockage.js';
import { chargerPiecesApi } from './api.js';

const etat = { pieces: [], recherche: '', alertesSeules: false };

const tbody = document.querySelector('#liste-pieces');
const resume = document.querySelector('#resume');
const champRecherche = document.querySelector('#recherche');
const caseAlertes = document.querySelector('#alertes-seules');
const form = document.querySelector('#form-piece');
const message = document.querySelector('#message');

function rafraichir() {
  afficherPieces(tbody, filtrerPieces(etat.pieces, etat.recherche, etat.alertesSeules));
  afficherResume(resume, etat.pieces);
}

function modifierStock(pieces) {
  etat.pieces = pieces;
  sauvegarderPieces(etat.pieces);
  rafraichir();
}

champRecherche.addEventListener('input', () => {
  etat.recherche = champRecherche.value;
  rafraichir();
});

caseAlertes.addEventListener('change', () => {
  etat.alertesSeules = caseAlertes.checked;
  rafraichir();
});

// Délégation : un seul écouteur pour tous les boutons + et − du tableau.
tbody.addEventListener('click', (event) => {
  const b = event.target.closest('button[data-action]');
  if (!b) return;
  const { action, ref } = b.dataset;
  modifierStock(ajusterQuantite(etat.pieces, ref, action === 'ajouter' ? 1 : -1));
  // Le tableau a été reconstruit : on rend le focus au même bouton (chapitre 13).
  tbody.querySelector(`button[data-ref="${ref}"][data-action="${action}"]`)?.focus();
});

form.addEventListener('submit', (event) => {
  event.preventDefault();
  const saisie = Object.fromEntries(new FormData(form));
  const erreurs = validerPiece(saisie, etat.pieces);
  afficherErreurs(form, erreurs);
  if (Object.keys(erreurs).length > 0) {
    message.textContent = 'Le formulaire contient des erreurs.';
    return;
  }
  const piece = creerPiece(saisie);
  modifierStock([...etat.pieces, piece]);
  form.reset();
  message.textContent = `Pièce ${piece.ref} ajoutée.`;
});

async function demarrer() {
  const enregistrees = chargerPieces(null);
  if (enregistrees !== null) {
    etat.pieces = enregistrees;
    rafraichir();
    return;
  }
  resume.textContent = 'Chargement du stock…';
  try {
    modifierStock(await chargerPiecesApi('data/pieces.json'));
  } catch (erreur) {
    resume.textContent = `Impossible de charger le stock : ${erreur.message}`;
  }
}

demarrer();

Et vue.js, qui ajoute aux fonctions du chapitre 7 les boutons + et − de chaque ligne et le résumé :

js
// L'affichage : transforme les données en éléments du DOM.
import { estEnAlerte, valeurStock } from './stock.js';

const euros = new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' });

function cellule(texte) {
  const td = document.createElement('td');
  td.textContent = texte;
  return td;
}

function bouton(texte, action, piece) {
  const b = document.createElement('button');
  b.type = 'button';
  b.textContent = texte;
  b.dataset.action = action;
  b.dataset.ref = piece.ref;
  b.setAttribute('aria-label', `${action === 'retirer' ? 'Retirer' : 'Ajouter'} une unité : ${piece.nom}`);
  return b;
}

export function lignePiece(piece) {
  const tr = document.createElement('tr');
  if (estEnAlerte(piece)) tr.classList.add('alerte');
  const actions = document.createElement('td');
  actions.append(bouton('−', 'retirer', piece), bouton('+', 'ajouter', piece));
  tr.append(
    cellule(piece.ref),
    cellule(piece.nom),
    cellule(String(piece.quantite)),
    cellule(euros.format(piece.prix)),
    actions,
  );
  return tr;
}

export function afficherPieces(tbody, pieces) {
  if (pieces.length === 0) {
    const tr = document.createElement('tr');
    const td = cellule('Aucune pièce ne correspond à ta recherche.');
    td.colSpan = 5;
    tr.append(td);
    tbody.replaceChildren(tr);
    return;
  }
  tbody.replaceChildren(...pieces.map(lignePiece));
}

export function afficherResume(element, pieces) {
  const nbAlertes = pieces.filter(estEnAlerte).length;
  element.textContent =
    `${pieces.length} références, ${nbAlertes} en alerte, ` +
    `valeur du stock : ${euros.format(valeurStock(pieces))}`;
}

export function afficherErreurs(form, erreurs) {
  for (const champ of form.querySelectorAll('input, select')) {
    const message = erreurs[champ.name] ?? '';
    const zone = form.querySelector(`#erreur-${champ.name}`);
    if (zone) zone.textContent = message;
    champ.setAttribute('aria-invalid', message ? 'true' : 'false');
  }
  const premier = form.querySelector('[aria-invalid="true"]');
  if (premier) premier.focus();
}

afficherErreurs est la fonction du chapitre 8, exportée. Au chargement, le résumé affiche 5 références, 2 en alerte, valeur du stock : 970,80 €.

Utiliser une bibliothèque : npm et Vite

Les bibliothèques JavaScript se distribuent par npm. Dans un projet créé avec Vite (chapitre 1) :

bash
npm install dompurify
js
import DOMPurify from 'dompurify';   // un nom de paquet, sans ./ ni extension

Le navigateur ne sait pas, seul, où trouver 'dompurify' : c'est Vite qui résout ce nom pendant le développement, puis qui regroupe (bundle) et minifie tous les modules en quelques fichiers optimisés avec npm run build. Le dossier dist/ produit est ce que tu déposes sur l'hébergement. Le fichier package.json liste les dépendances et leurs versions ; le dossier node_modules, qui les contient, ne se versionne jamais dans Git (fichier .gitignore).

Charger un module à la demande : import()

import(chemin) sous forme de fonction charge un module au moment où tu en as besoin, et renvoie une promesse. Utile pour une fonctionnalité lourde et rarement utilisée, comme l'export du stock en PDF :

js
boutonExport.addEventListener('click', async () => {
  const { exporterPdf } = await import('./export-pdf.js');
  exporterPdf(etat.pieces);
});

CommonJS : require dans Node.js

Tu croiseras une autre syntaxe de modules, plus ancienne, propre à Node.js : CommonJS, avec require et module.exports.

js
// CommonJS (Node.js, code ancien)
const { valeurStock } = require('./stock.js');
module.exports = { estEnAlerte };

Node.js comprend les deux. Il traite un fichier comme module ES s'il se termine par .mjs ou si le package.json du projet contient "type": "module" (c'est le cas des projets créés par Vite). Pour un nouveau projet, utilise les modules ES partout ; le cours Node.js et Express en reparle côté serveur.

Les classes : la syntaxe

Une classe est un modèle pour fabriquer des objets qui partagent les mêmes propriétés et les mêmes méthodes. Les concepts de la programmation orientée objet (encapsulation, héritage, polymorphisme, interfaces) sont enseignés en C# dans le cours Programmation orientée objet ; cette section montre seulement comment ils s'écrivent en JavaScript, pour que tu saches lire et écrire une classe.

js
class Piece {
  static REF_VALIDE = /^[A-Z]{3}-\d{2,3}$/;   // propriété de la classe elle-même
  #quantite;                                  // champ privé : inaccessible hors de la classe

  constructor({ ref, nom, quantite = 0, seuil = 0, prix }) {
    if (!Piece.REF_VALIDE.test(ref)) {
      throw new Error(`Référence invalide : ${ref}`);
    }
    this.ref = ref;
    this.nom = nom;
    this.#quantite = quantite;
    this.seuil = seuil;
    this.prix = prix;
  }

  get quantite() {           // accesseur : se lit comme une propriété
    return this.#quantite;
  }

  get enAlerte() {
    return this.#quantite <= this.seuil;
  }

  retirer(n = 1) {
    if (n > this.#quantite) {
      throw new Error(`Stock insuffisant pour ${this.ref} : ${this.#quantite} disponible(s)`);
    }
    this.#quantite -= n;
  }

  ajouter(n = 1) {
    this.#quantite += n;
  }

  toJSON() {                 // utilisée automatiquement par JSON.stringify
    return { ref: this.ref, nom: this.nom, quantite: this.#quantite, seuil: this.seuil, prix: this.prix };
  }
}

const ram = new Piece({ ref: 'RAM-16', nom: 'Barrette DDR4 16 Go', quantite: 3, seuil: 4, prix: 38.5 });
ram.quantite;        // 3
ram.enAlerte;        // true
ram.ajouter(2);
ram.enAlerte;        // false
ram.retirer(10);     // Error: Stock insuffisant pour RAM-16 : 5 disponible(s)
ram.quantite = 99;   // dans un module : TypeError: Cannot set property quantite of #<Piece> which has only a getter
JSON.stringify(ram); // '{"ref":"RAM-16","nom":"Barrette DDR4 16 Go","quantite":5,"seuil":4,"prix":38.5}'

L'erreur sur ram.quantite = 99 n'apparaît qu'en mode strict, celui des modules et des classes. Tapée dans la console, où le code s'exécute en mode non strict, l'affectation est ignorée sans erreur : ram.quantite vaut toujours 5. Le getter protège la valeur dans les deux cas ; seul le signalement change.

ÉlémentSyntaxe JavaScriptÉquivalent C#
Constructeurconstructor(…) { … }public Piece(…) { … }
Création d'un objetnew Piece({ … }) (le new est obligatoire)new Piece(…)
Accès à l'objet courantthis (obligatoire)this (souvent implicite)
Champ privé#quantiteprivate int quantite;
Accesseurget quantite() { … }public int Quantite { get; }
Membre de classestatic REF_VALIDEstatic
Héritageclass B extends A, super(…)class B : A, base(…)

# est la seule vraie protection d'un champ : ram.#quantite hors de la classe est une erreur de syntaxe. Les anciens codes utilisent une convention, le préfixe _ (this._quantite), qui ne protège rien.

L'héritage s'écrit avec extends, et le constructeur de la classe fille doit appeler super(…) avant d'utiliser this :

js
class PieceReconditionnee extends Piece {
  constructor(donnees) {
    super(donnees);
    this.garantieMois = donnees.garantieMois ?? 6;
  }

  get libelle() {
    return `${this.nom} (reconditionné, garantie ${this.garantieMois} mois)`;
  }

  toJSON() {
    return { ...super.toJSON(), garantieMois: this.garantieMois };
  }
}

const ecran = new PieceReconditionnee({ ref: 'ECR-140', nom: 'Dalle 14 pouces', quantite: 1, seuil: 1, prix: 45 });
ecran.libelle;               // 'Dalle 14 pouces (reconditionné, garantie 6 mois)'
ecran.enAlerte;              // true : hérité de Piece
ecran instanceof Piece;      // true

Faut-il des classes dans une application JavaScript ? Pas forcément. Gestock fonctionne avec des objets de données et des fonctions pures, ce qui se convertit en JSON sans effort et se teste facilement. Les classes sont utiles quand un objet doit protéger son état et garantir ses règles (une pièce dont la quantité ne peut jamais devenir négative), et tu les retrouveras partout dans les bibliothèques et en C#.

Exercices

Exercice 1 sur 4 Exports et imports Réussi À revoir

Le fichier outils.js contient :

js
export const TVA = 0.2;
export function ttc(ht) { return ht * (1 + TVA); }
export default function formaterPrix(n) { return `${n.toFixed(2)} €`; }
function arrondir(n) { return Math.round(n * 100) / 100; }
  1. Écris l'import qui récupère ttc et l'export par défaut sous le nom prix.
  2. Peut-on importer arrondir ? Pourquoi ?
  3. L'instruction import { formaterPrix } from './outils.js'; fonctionne-t-elle ?
Voir la réponseMasquer la réponse de l'exercice 1
  1. import prix, { ttc } from './outils.js'; : l'export par défaut d'abord, sous le nom de ton choix, puis les exports nommés entre accolades.
  2. Non : arrondir n'est pas exportée, elle est privée au module. L'import échoue avec does not provide an export named 'arrondir'.
  3. Non : formaterPrix est l'export par défaut, pas un export nommé. Il s'importe sans accolades : import formaterPrix from './outils.js';.

Tu avais trouvé ?

Exercice 2 sur 4 Diagnostiquer un chargement Réussi À revoir

Pour chaque message de la console, donne la cause et la correction.

  1. Uncaught SyntaxError: Cannot use import statement outside a module
  2. Dans l'onglet Réseau, une ligne rouge Stock.js 404 (Not Found), et la page reste vide. Le fichier s'appelle js/stock.js, l'import est import { valeurStock } from './Stock.js';.
  3. Access to script at 'file:///C:/gestock/js/main.js' from origin 'null' has been blocked by CORS policy
Voir la réponseMasquer la réponse de l'exercice 2
  1. La balise de la page est <script src="js/main.js"> sans type="module". Ajoute type="module".
  2. La casse est fausse : Stock.js au lieu de stock.js. Le serveur ne trouve pas le fichier et répond 404 ; le module qui l'importe ne s'exécute pas, ni ceux qui en dépendent. Sous Windows et macOS, le disque ignore souvent la casse, ce qui masque l'erreur en local, mais les serveurs Linux de l'hébergement la respectent : écris toujours la casse exacte.
  3. La page est ouverte par double-clic (file://). Sers le projet avec npm run dev (Vite) ou python3 -m http.server.

Tu avais trouvé ?

Exercice 3 sur 4 Une classe Commande Réussi À revoir

Écris une classe Commande avec : un numéro passé au constructeur ; une liste privée de lignes { ref, quantite, prix } ; une méthode ajouterLigne(ref, quantite, prix) qui refuse une quantité nulle ou négative en levant une erreur ; un accesseur total ; un accesseur nbLignes. Que valent total et nbLignes après l'ajout de 5 × SSD-512 à 42,90 € et 4 × CHG-65 à 24 € ?

Voir la réponseMasquer la réponse de l'exercice 3
js
class Commande {
  #lignes = [];

  constructor(numero) {
    this.numero = numero;
  }

  ajouterLigne(ref, quantite, prix) {
    if (quantite <= 0) throw new Error('Quantité invalide');
    this.#lignes.push({ ref, quantite, prix });
  }

  get total() {
    return this.#lignes.reduce((t, l) => t + l.quantite * l.prix, 0);
  }

  get nbLignes() {
    return this.#lignes.length;
  }
}

const c = new Commande('CMD-2026-0042');
c.ajouterLigne('SSD-512', 5, 42.9);
c.ajouterLigne('CHG-65', 4, 24);
c.nbLignes;   // 2
c.total;      // 310.5 : 5 × 42,9 + 4 × 24 = 214,5 + 96

Le champ privé peut être initialisé directement dans sa déclaration (#lignes = []). Personne ne peut vider ou modifier les lignes depuis l'extérieur : c.lignes vaut undefined et c.#lignes est une erreur de syntaxe.

Tu avais trouvé ?

Exercice 4 sur 4 Ranger le code Réussi À revoir

Un étudiant a tout écrit dans un seul fichier app.js de 400 lignes. Pour chacune de ces fonctions, indique dans quel module de Gestock elle devrait aller : calculerRemise(total, client), afficherFacture(facture), enregistrerBrouillon(facture), envoyerFacture(facture) (requête POST), factureEstValide(facture), l'écouteur submit du formulaire.

Voir la réponseMasquer la réponse de l'exercice 4
  • calculerRemise et factureEstValide : règles métier, dans stock.js (ou un facture.js du même type), sans DOM, testables.
  • afficherFacture : vue.js.
  • enregistrerBrouillon : stockage.js.
  • envoyerFacture : api.js.
  • L'écouteur submit : main.js, qui relie le formulaire, les règles, l'API et l'affichage.

Tu avais trouvé ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Déboguer JavaScript avec les DevTools et gérer les erreursRevoir le sommaire du cours