frontend / javascript
Bases du runtime Node.js
Explication
Ce que vous allez apprendre
- Situer les différences entre l'environnement navigateur et l'environnement Node.js
- Utiliser les modules natifs
fs,pathethttppour des opérations serveur de base - Justifier pourquoi les opérations synchrones bloquantes sont à éviter en Node.js
- Traiter un fichier volumineux avec des streams plutôt qu'un chargement complet en mémoire
- Décrire à quoi sert
worker_threadspour du calcul intensif en CPU
Dans quel contexte ?
Une API Node.js répond normalement en 50ms, mais devient totalement injoignable pendant 2 secondes chaque fois qu'un utilisateur importe un fichier CSV de configuration. En investiguant, un développeur trouve const contenu = fs.readFileSync(cheminFichier); dans le gestionnaire de la route : cette version synchrone bloque l'intégralité du serveur Node.js pendant la lecture, empêchant TOUTES les autres requêtes d'être traitées entre-temps, pas seulement celle de l'utilisateur qui importe le fichier. Remplacer readFileSync par fs.promises.readFile (asynchrone) règle immédiatement le problème.
1. Le même langage, un environnement différent
Jusqu'à présent, une bonne partie du cours a supposé un environnement navigateur (le DOM, fetch). Node.js change ce contexte : il fait tourner du JavaScript côté serveur, sans navigateur ni DOM, avec accès direct au système de fichiers et au réseau bas niveau.
2. Ce qui ne change pas, et ce qui change
Le langage JavaScript reste rigoureusement le même : fonctions, Promises, event loop, tout ce qui a été appris jusqu'ici s'applique identiquement. Ce qui change, ce sont les API disponibles : fs, path, http remplacent les API navigateur absentes côté serveur.
| Environnement | Disponible | Absent |
|---|---|---|
| Navigateur | DOM, fetch, localStorage, window | fs, require CommonJS natif |
| Node.js | fs, path, http, process, worker_threads | DOM, window, document |
Prérequis
Cette leçon suppose l'event loop (leçon 13) bien assimilé : comprendre pourquoi une opération synchrone bloque tout repose entièrement sur le fait que Node.js, comme le navigateur, reste mono-thread.
3. La règle qui domine ce chapitre
Toujours préférer les versions asynchrones des opérations d'entrée-sortie (lecture de fichier, requête réseau) aux versions synchrones.
4. Pourquoi cette règle est non négociable
Node.js reste mono-thread comme le navigateur : une opération synchrone bloque littéralement TOUT le serveur pendant sa durée, inacceptable dès qu'il doit répondre à plusieurs utilisateurs en même temps.
Piège fréquent
Toute fonction native Node.js se terminant par Sync (readFileSync, execSync...) bloque l'event loop entier. Elles sont acceptables dans un script ponctuel exécuté une fois, mais jamais dans le code d'un serveur qui doit répondre à plusieurs requêtes concurrentes.
5. Traiter de gros volumes sans tout charger
Pour des données trop volumineuses pour tenir en mémoire (un gros fichier de logs), les streams permettent de les traiter morceau par morceau plutôt que d'un seul coup.
6. Une échappatoire pour le calcul lourd
Enfin, worker_threads offre une vraie échappatoire à la contrainte du mono-thread pour des calculs lourds en CPU — un concept que vous retrouverez sous une forme différente (les Web Workers) dans une leçon dédiée au navigateur, plus loin dans le cours.
Commandes & code
Bases du runtime Node.js
JavaScript côté serveur : ce qui change par rapport au navigateur.
// --- Modules integres (built-in) : pas d'installation necessaire ---
const fs = require("fs"); // systeme de fichiers
const fsPromises = require("fs/promises"); // version basee sur des Promises
const path = require("path");
const http = require("http");
const os = require("os");
const crypto = require("crypto");
// --- Systeme de fichiers : version synchrone vs asynchrone (toujours preferer l'asynchrone) ---
// Synchrone : BLOQUE l'event loop entier -- a eviter en production sauf au demarrage du script
const contenuSync = fs.readFileSync("fichier.txt", "utf8");
// Asynchrone avec callback (API historique)
fs.readFile("fichier.txt", "utf8", (erreur, contenu) => {
if (erreur) return console.error(erreur);
console.log(contenu);
});
// Asynchrone avec Promises (API moderne recommandee)
async function lireFichier() {
const contenu = await fsPromises.readFile("fichier.txt", "utf8");
return contenu;
}
// --- path : manipulation de chemins portable entre OS ---
const cheminComplet = path.join(__dirname, "data", "fichier.txt");
console.log(path.extname("photo.jpg")); // ".jpg"
console.log(path.basename("/dossier/fichier.txt")); // "fichier.txt"
console.log(path.resolve("./relatif")); // chemin absolu
// --- process : informations et controle du processus courant ---
console.log(process.env.NODE_ENV); // variables d'environnement
console.log(process.argv); // arguments de la ligne de commande
console.log(process.platform, process.version); // OS, version de Node
process.exit(0); // termine le processus (a utiliser avec precaution)
process.on("uncaughtException", (erreur) => {
console.error("Exception non capturee :", erreur);
process.exit(1); // recommande : redemarrer proprement apres une exception non geree
});
// --- Serveur HTTP natif (sans framework) ---
const serveur = http.createServer((requete, reponse) => {
reponse.writeHead(200, { "Content-Type": "application/json" });
reponse.end(JSON.stringify({ message: "Bonjour depuis Node.js" }));
});
serveur.listen(3000, () => console.log("Serveur demarre sur le port 3000"));
// --- Streams : traiter de gros volumes sans tout charger en memoire ---
const { createReadStream, createWriteStream } = require("fs");
const streamLecture = createReadStream("gros-fichier.log", "utf8");
const streamEcriture = createWriteStream("copie.log");
streamLecture.pipe(streamEcriture); // traite le fichier morceau par morceau
streamLecture.on("data", (chunk) => console.log(`Chunk de ${chunk.length} caracteres`));
streamLecture.on("end", () => console.log("Lecture terminee"));
streamLecture.on("error", (erreur) => console.error("Erreur de stream :", erreur));
// --- EventEmitter : le pattern observer natif de Node ---
const { EventEmitter } = require("events");
class ServiceCommandes extends EventEmitter {
creerCommande(donnees) {
// ... logique metier
this.emit("commande:creee", donnees);
}
}
const service = new ServiceCommandes();
service.on("commande:creee", (cmd) => console.log("Nouvelle commande :", cmd));
// --- Variables globales specifiques a Node (absentes du navigateur) ---
console.log(__dirname); // dossier du fichier courant
console.log(__filename); // chemin complet du fichier courant
console.log(typeof module); // "object" -- CommonJS, absent en environnement navigateur
// --- npm : gestionnaire de paquets et package.json ---
// npm init -y -- initialise un package.json
// npm install express -- installe une dependance
// npm install --save-dev jest -- dependance de developpement uniquement
// npm run start -- execute un script defini dans package.json
// --- Worker Threads : vrai parallelisme pour du CPU-bound (contourne le mono-thread) ---
const { Worker, isMainThread, parentPort } = require("worker_threads");
if (isMainThread) {
const worker = new Worker(__filename);
worker.on("message", (resultat) => console.log("Resultat du worker :", resultat));
} else {
parentPort.postMessage(2 + 2); // execute dans un thread separe
}Résumé
- Toujours préférer les API asynchrones (
fs/promises) aux synchrones, qui bloquent l'event loop entier. - Les streams traitent de gros volumes de données par morceaux, sans tout charger en mémoire.
EventEmitterest le pattern observer natif de Node, base de nombreux modules (http, streams...).worker_threadsapporte du vrai parallélisme CPU, contournant la nature mono-thread de Node.js.
Exercices pratiques
Mission : l'API qui devient injoignable pendant les imports CSV
Objectif : Diagnostiquer un blocage de l'event loop causé par une opération synchrone, la corriger, et choisir la bonne approche pour un calcul CPU-intensif.
Contexte
L'API Node.js de Technologik répond normalement en 50ms, mais devient totalement injoignable pendant 2 secondes chaque fois qu'un utilisateur importe un fichier CSV. Le code du gestionnaire de route :
app.post("/import", (req, res) => {
const contenu = fs.readFileSync(req.file.path, "utf8");
const lignes = parserCSV(contenu);
res.json({ importees: lignes.length });
});Pendant ces 2 secondes, même les requêtes GET les plus simples d'autres utilisateurs, sans aucun rapport avec l'import, ne reçoivent aucune réponse. Il faut aussi décider comment traiter un calcul CPU-intensif (validation cryptographique de chaque ligne) sans reproduire le même blocage.