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
exportetimport; - 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/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/vue.js
import { estEnAlerte, valeurStock } from './stock.js';
| Écriture | Rô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.
<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 defer | Il s'exécute après la lecture du HTML : querySelector trouve les éléments |
| Mode strict automatique | Les erreurs silencieuses deviennent des erreurs visibles (variable non déclarée, this à undefined) |
| Portée propre | Ses variables ne sont pas globales : une console ne les voit pas directement |
| Exécuté une seule fois | Importé par dix fichiers, un module ne s'exécute qu'une fois |
| Chargé selon les règles CORS | Ouvert en file://, il est bloqué : il faut un serveur (chapitre 1) |
await autorisé au premier niveau | const pieces = await chargerPiecesApi(…) directement dans le module |
Les erreurs de chargement que tu rencontreras :
| Message dans la console | Cause |
|---|---|
Cannot use import statement outside a module | La balise <script> n'a pas type="module" |
Access to script at 'file:///…' from origin 'null' has been blocked by CORS policy | La 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écute | Le 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é :
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)
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 :
<!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 :
// 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).
[
{ "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 :
// 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é :
// 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) :
npm install dompurify
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 :
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.
// 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.
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ément | Syntaxe JavaScript | Équivalent C# |
|---|---|---|
| Constructeur | constructor(…) { … } | public Piece(…) { … } |
| Création d'un objet | new Piece({ … }) (le new est obligatoire) | new Piece(…) |
| Accès à l'objet courant | this (obligatoire) | this (souvent implicite) |
| Champ privé | #quantite | private int quantite; |
| Accesseur | get quantite() { … } | public int Quantite { get; } |
| Membre de classe | static REF_VALIDE | static |
| Héritage | class 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 :
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 :
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; }
- Écris l'import qui récupère
ttcet l'export par défaut sous le nomprix. - Peut-on importer
arrondir? Pourquoi ? - L'instruction
import { formaterPrix } from './outils.js';fonctionne-t-elle ?
Voir la réponseMasquer la réponse de l'exercice 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.- Non :
arrondirn'est pas exportée, elle est privée au module. L'import échoue avecdoes not provide an export named 'arrondir'. - Non :
formaterPrixest 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.
Uncaught SyntaxError: Cannot use import statement outside a module- Dans l'onglet Réseau, une ligne rouge
Stock.js 404 (Not Found), et la page reste vide. Le fichier s'appellejs/stock.js, l'import estimport { valeurStock } from './Stock.js';. 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
- La balise de la page est
<script src="js/main.js">sanstype="module". Ajoutetype="module". - La casse est fausse :
Stock.jsau lieu destock.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. - La page est ouverte par double-clic (
file://). Sers le projet avecnpm run dev(Vite) oupython3 -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
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
calculerRemiseetfactureEstValide: règles métier, dansstock.js(ou unfacture.jsdu 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é ?