Node.js : concepts de base
Node.js est un environnement d'exécution JavaScript construit sur le moteur JavaScript V8 de Chrome. En termes simples, Node.js permet à JavaScript de s'exécuter côté serveur, et pas seulement dans les navigateurs.
Fonctionnalités principales de Node.js
1. Boucle d'événements monothread
- Node.js utilise un modèle de boucle d'événements monothread
- Gère les requêtes concurrentes via des mécanismes événementiels et des fonctions de rappel
- Évite la surcharge de commutation de threads de la programmation multithread traditionnelle
2. E/S non bloquantes
- Toutes les opérations d'E/S (lecture/écriture de fichiers, requêtes réseau, etc.) sont asynchrones
- Lorsqu'il attend la fin d'une opération d'E/S, le programme n'est pas bloqué et peut continuer à traiter d'autres tâches
- Améliore considérablement le débit des applications
3. Multiplateforme
- Prend en charge plusieurs systèmes d'exploitation tels que Windows, macOS et Linux
- Écrire une fois, exécuter partout
4. Écosystème riche
- npm (Node Package Manager) dispose de millions de paquets open source
- Communauté active de développeurs
Différences avec les technologies côté serveur traditionnelles
1. Technologies côté serveur traditionnelles (par exemple, Apache + PHP) :
请求1 → 创建线程1 → 处理请求 → 返回响应 → 销毁线程1 请求2 → 创建线程2 → 处理请求 → 返回响应 → 销毁线程2 请求3 → 创建线程3 → 处理请求 → 返回响应 → 销毁线程3

2. Comment Node.js gère les requêtes :
请求1 → 事件循环 → 处理请求 → 返回响应 请求2 → 事件循环 → 处理请求 → 返回响应 (复用同一线程) 请求3 → 事件循环 → 处理请求 → 返回响应

| Fonctionnalité | Modèle multi-thread traditionnel | Modèle mono-thread de Node.js |
|---|---|---|
| Utilisation de la mémoire | Chaque thread occupe environ 2MB | Mono-thread, faible utilisation de la mémoire |
| Gestion de la concurrence | Le nombre de threads limite la concurrence | La boucle d'événements gère une concurrence élevée |
| Changement de contexte | Surcharge fréquente de commutation de threads | Aucune surcharge de commutation de threads |
| Complexité de programmation | Nécessite de gérer la synchronisation des threads | Évite les problèmes de verrouillage et de synchronisation des threads |
| Scénarios adaptés | Tâches gourmandes en CPU | Tâches intensives en E/S |
Scénarios d'application de Node.js
1. Scénarios d'application adaptés
Applications web
- Services API RESTful
- Services backend pour applications monopages (SPA)
- Applications web en temps réel
Applications en temps réel
- Applications de chat
- Jeux en ligne
- Outils de collaboration (par ex., édition de documents en ligne)
Architecture de microservices
- Microservices légers
- Passerelles API
- Communication inter-services
Outils et applications en ligne de commande
- Outils de build (par ex., Webpack, Gulp)
- Outils de scaffolding
- Scripts d'automatisation
Applications Internet des objets (IoT)
- Collecte de données d'appareils
- Traitement des données de capteurs
2. Scénarios d'application inadaptés
Tâches gourmandes en CPU
- Traitement d'images/vidéos
- Calculs mathématiques complexes
- Analyse de big data
Applications nécessitant des calculs intensifs
- Entraînement d'apprentissage automatique
- Calcul scientifique
- Minage de cryptomonnaies
Modèle d'E/S événementiel et non bloquant
Exemple de modèle d'E/S bloquant traditionnel :
Exemple
// Chaque readFileSync « bloque » le programme, en attendant que la lecture du fichier se termine avant de continuer
const data1 = readFileSync('file1.txt'); // Le programme se met en pause ici, en attendant que la lecture soit terminée
const data2 = readFileSync('file2.txt'); // Une fois data1 lu, il se met à nouveau en pause pour attendre
const data3 = readFileSync('file3.txt'); // Une fois data2 lu, il se met à nouveau en pause pour attendre
console.log('Tous les fichiers ont été lus'); // Cette ligne n'est exécutée qu'après la lecture des trois fichiers
// Temps total = temps de lecture de file1 + temps de lecture de file2 + temps de lecture de file3
Exemple de modèle d'E/S non bloquant de Node.js :
Exemple
const fs = require('fs');
// Les trois opérations de lecture de fichiers sont lancées presque simultanément ; le programme n'attend pas qu'elles se terminent
fs.readFile('file1.txt', (err, data1) => {
// Cette fonction de rappel sera automatiquement appelée après la lecture de file1.txt
console.log('Lecture du fichier 1 terminée');
});
fs.readFile('file2.txt', (err, data2) => {
// Cette fonction de rappel sera automatiquement appelée après la lecture de file2.txt
console.log('Lecture du fichier 2 terminée');
});
fs.readFile('file3.txt', (err, data3) => {
// Cette fonction de rappel sera automatiquement appelée après la lecture de file3.txt
console.log('Lecture du fichier 3 terminée');
});
// Remarque : Cette ligne de code s'exécutera en premier ! car les trois opérations de lecture de fichier ci-dessus sont asynchrones,
// Après avoir émis les requêtes de lecture, le programme continue immédiatement, sans attendre que les lectures de fichier soient terminées
console.log('Le programme continue de s'exécuter sans attendre les lectures de fichier');
// Exemple d'ordre de sortie réel :
// Le programme continue de s'exécuter sans attendre les lectures de fichier ← affiché en premier
// Lecture du fichier 2 terminée ← le fichier qui se termine en premier est affiché en premier, l'ordre n'est pas fixe
// Lecture du fichier 1 terminée
// Lecture du fichier 3 terminée
// Temps total ≈ le temps du plus lent parmi les trois fichiers (exécution concurrente, pas d'accumulation séquentielle)
Mécanisme de la boucle d'événements :
La boucle d'événements est le mécanisme central par lequel Node.js met en œuvre les E/S non bloquantes. Elle s'exécute en continu, en vérifiant constamment les tâches en attente, réalisant ainsi l'effet de « concurrence mono-thread » :
- Pile d'appels: Exécute du code synchrone. Les appels de fonction sont empilés sur la pile et dépilés après exécution.
- File d'attente d'événements: Stocke les fonctions de rappel des opérations asynchrones terminées, à exécuter en séquence lorsque la pile d'appels est vide.
- Boucle d'événements: Surveille en continu la pile d'appels et la file d'attente d'événements. Une fois la pile d'appels vide, elle prend une fonction de rappel dans la file d'attente d'événements et la place sur la pile d'appels pour exécution.
La boucle d'événements traite les phases suivantes dans un ordre fixe :
┌───────────────────────────┐
┌─>│ timers │ ← 执行 setTimeout、setInterval 的到期回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │ ← 执行上一轮循环中延迟的 I/O 回调(如某些系统错误回调)
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │ ← Node.js 内部使用,开发者一般不需要关心
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ poll │ ← 核心阶段:获取新的 I/O 事件并执行其回调(如文件读取、网络请求完成)
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ check │ ← 执行 setImmediate 的回调(在 poll 阶段之后立即执行)
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │ ← 执行关闭事件的回调,如 socket.on('close', ...)
└───────────────────────────┘
Pour les débutants, la chose la plus importante à retenir est :Le code synchrone s'exécute toujours en premier. Les fonctions de rappel asynchrones (comme la gestion de la fin de lecture d'un fichier) ne sont exécutées qu'une fois la pile d'appels vidée.
Environnement d'exécution JavaScript
Introduction au moteur V8
V8 est un moteur JavaScript hautes performances développé par Google et est un composant central du navigateur Chrome.
Node.js utilise le moteur V8 pour exécuter du code JavaScript.
Caractéristiques du moteur V8 :
Compilation juste-à-temps (JIT)
- Compile le code JavaScript directement en code machine
- Aucun bytecode intermédiaire n'est nécessaire, ce qui améliore l'efficacité d'exécution
Récupération de mémoire
- Gestion automatique de la mémoire ; les développeurs n'ont pas besoin de libérer manuellement la mémoire
- Utilise un algorithme de récupération de mémoire générationnel, divisant la mémoire en génération jeune et génération ancienne pour une gestion séparée, ce qui améliore l'efficacité de la collecte
Techniques d'optimisation
- Mise en cache en ligne : met en cache les résultats des recherches de propriétés d'objets pour éviter les recherches répétées
- Classes cachées : rendent statique la structure des objets des langages dynamiques afin d'accélérer l'accès aux propriétés
- Optimisation dynamique : analyse le code chaud à l'exécution et effectue une optimisation ciblée
JavaScript dans le navigateur vs JavaScript dans Node.js
Bien que les deux utilisent le langage JavaScript, des différences dans l'environnement d'exécution entraînent des distinctions importantes :
Points communs :
- Les deux utilisent la même syntaxe JavaScript
- Les deux prennent en charge les fonctionnalités ES6+
- Les deux utilisent le moteur V8 (navigateur Chrome)
Différences :
| Fonctionnalité | JavaScript dans le navigateur | JavaScript sous Node.js |
|---|---|---|
| Objet global | window |
global |
| Système de modules | Modules ES6, AMD | CommonJS, modules ES6 |
| Accès au système de fichiers | Non accessible (en raison de restrictions de sécurité) | Accès complet |
| Requêtes réseau | XMLHttpRequest, Fetch | modules http, https |
| manipulation du DOM | Pris en charge (manipulation des éléments de la page) | Non pris en charge (aucune page côté serveur) |
| Contrôle des processus | Non pris en charge | Pris en charge (peut lire les variables d'environnement, quitter le processus, etc.) |
Exemple d'environnement de navigateur :
Exemple
console.log(window); // Objet global du navigateur, contient toutes les API du navigateur
document.getElementById('app'); // Récupérer un élément de page par ID (non disponible dans Node.js)
localStorage.setItem('key', 'value'); // Stockage local du navigateur (non disponible dans Node.js)
Exemple d'environnement Node.js :
Exemple
console.log(global); // Objet global de Node.js (correspondant à window dans le navigateur)
const fs = require('fs'); // Importer le module de système de fichiers intégré (non disponible dans le navigateur)
fs.readFile('data.txt', 'utf8', callback); // Lire un fichier local sur le serveur (non disponible dans le navigateur)
Différences entre les objets globaux
Objet global dans le navigateur :
Exemple
console.log(this === window); // true, le this au niveau supérieur est l'objet window
var globalVar = 'hello';
console.log(window.globalVar); // 'hello', les variables déclarées avec var deviennent des propriétés de window
Objet global dans Node.js :
Exemple
// Remarque : dans Node.js, chaque fichier est un module indépendant, et this à l'intérieur d'un module n'est pas égal à global
console.log(this === global); // false (dans la portée du module, this pointe vers module.exports)
var globalVar = 'hello';
console.log(global.globalVar); // undefined, les variables à l'intérieur d'un module ne sont pas automatiquement attachées à global
// Portée du module dans Node.js
console.log(this); // {} objet vide, c'est-à-dire la valeur initiale de module.exports
console.log(module.exports === this); // true, this à l'intérieur d'un module pointe vers module.exports
Variables globales spécifiques à Node.js :
Exemple
console.log(__filename); // Chemin absolu du fichier du module courant, par ex. /home/user/project/app.js
console.log(process); // Objet process, peut lire les arguments de ligne de commande (process.argv), les variables d'environnement (process.env), etc.
console.log(Buffer); // Constructeur pour manipuler des données binaires, couramment utilisé pour la lecture/écriture de fichiers et la communication réseau
Avantages et limites de Node.js
Avantages détaillés
1. Capacité de traitement de forte concurrence
Un serveur multi-thread traditionnel a besoin d'environ 20GB de mémoire pour gérer 10,000 connexions simultanées (environ 2MB par thread), tandis que Node.js nécessite très peu de mémoire pour gérer le même nombre de connexions.
Exemple
const http = require('http');
const server = http.createServer((req, res) => {
// Simuler une opération asynchrone (comme l'interrogation d'une base de données), renvoyant une réponse après 100ms
// Pendant ces 100ms, la boucle d'événements peut continuer à traiter d'autres requêtes sans être bloquée
setTimeout(() => {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
}, 100);
});
server.listen(3000, () => {
console.log('Server running at http://localhost:3000/');
});
// Grâce aux E/S non bloquantes et au mécanisme de boucle d'événements, ce serveur peut gérer des milliers de requêtes simultanément sans blocage
2. Développement rapide
Une pile technologique JavaScript unifiée (le frontend et le backend utilisent tous deux JavaScript) rend le développement frontend et backend plus efficace. Les développeurs n'ont besoin de maîtriser qu'un seul langage pour écrire à la fois le code frontend et backend, et peuvent partager la logique de code entre les deux côtés :
Exemple
function validateEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
// Utilisation côté frontend (exécution dans le navigateur) : lorsqu’un utilisateur soumet un formulaire, validez-le d’abord côté frontend afin d’éviter d’envoyer des requêtes invalides au serveur
if (validateEmail(userInput)) {
// La validation réussit, envoyer au serveur
}
// Utilisation côté backend (exécution dans Node.js) : le serveur valide à nouveau pour empêcher les requêtes API directes qui contournent le frontend
if (validateEmail(req.body.email)) {
// La validation réussit, enregistrer dans la base de données
}
3. Avantages d’une pile technologique unifiée
- Réutilisation du code: le frontend et le backend peuvent partager des fonctions utilitaires, la logique de validation des données, des définitions de constantes, etc.
- Efficacité de l’équipe: les développeurs peuvent gérer à la fois le développement frontend et backend, ce qui réduit les coûts de communication
- Pile technologique simplifiée: il suffit de maîtriser le langage JavaScript, ce qui réduit les coûts d’apprentissage et la complexité technique
4. Un écosystème npm riche
# npm 提供了数百万个开源包,几乎任何功能都能找到现成的库 npm search express # 搜索名为 express 的包 npm install express # 安装 express 包到当前项目 npm update # 更新当前项目中所有包到最新兼容版本
Analyse des limitations
1. Problèmes de performance avec les tâches intensives en CPU
Exemple
// fibonacciSync est une tâche de calcul pure qui continue d'occuper le CPU, pendant laquelle la boucle d'événements ne peut pas traiter d'autres requêtes
function fibonacciSync(n) {
if (n < 2) return n;
return fibonacciSync(n - 1) + fibonacciSync(n - 2);
}
// Cela bloquera l'ensemble de l'application pendant plusieurs secondes, pendant lesquelles toutes les requêtes resteront sans réponse
console.log(fibonacciSync(40));
// Solution : utiliser des Worker Threads pour exécuter les tâches intensives en CPU dans des threads séparés,
// afin que la boucle d'événements du thread principal ne soit pas bloquée
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
if (isMainThread) {
// Thread principal : créer un Worker et lui passer l'argument { n: 40 }
// Le Worker exécutera le fichier actuel dans un thread séparé (__filename fait référence au chemin du fichier actuel)
const worker = new Worker(__filename, {
workerData: { n: 40 }
});
// Écouter les messages envoyés par le Worker (le résultat du calcul)
worker.on('message', (result) => {
console.log('Résultat :', result); // Une fois que le Worker a terminé le calcul, le thread principal reçoit le résultat ici
});
} else {
// Thread Worker : récupérer les paramètres transmis par le thread principal via workerData, exécuter le calcul, puis envoyer le résultat
const result = fibonacciSync(workerData.n);
parentPort.postMessage(result); // Renvoyer le résultat au thread principal
}
2. Problème de l'enfer des callbacks
Exemple
fs.readFile('file1.txt', (err, data1) => {
if (err) throw err;
fs.readFile('file2.txt', (err, data2) => { // Premier niveau d'imbrication
if (err) throw err;
fs.readFile('file3.txt', (err, data3) => { // Deuxième niveau d'imbrication
if (err) throw err;
// Dans des projets réels, cette imbrication peut atteindre une douzaine de niveaux ou plus, ce qui nuit gravement à la maintenabilité du code
console.log('Tous les fichiers ont été lus avec succès');
});
});
});
// Solution moderne : utiliser Promise + async/await pour rendre le code asynchrone aussi intuitif que le code synchrone
const fsPromises = require('fs').promises;
async function readFiles() {
try {
// await attendra que chaque fichier soit lu, mais la fonction entière ne bloque pas la boucle d'événements
// Remarque : ceci lit séquentiellement ; pour lire plusieurs fichiers en parallèle, utilisez Promise.all()
const data1 = await fsPromises.readFile('file1.txt');
const data2 = await fsPromises.readFile('file2.txt');
const data3 = await fsPromises.readFile('file3.txt');
console.log('Tous les fichiers ont été lus avec succès');
} catch (err) {
// try/catch peut gérer uniformément toutes les erreurs asynchrones, sans avoir besoin de vérifier séparément dans chaque callback
console.error('Échec de la lecture des fichiers :', err);
}
}
3. Fragilité du mono-threading
Exemple
// Contrairement au multi-threading, une seule erreur non gérée dans un environnement mono-thread arrêtera l'ensemble du service
setTimeout(() => {
throw new Error('Exception non interceptée'); // Cela fera planter toute l'application, et toutes les requêtes resteront sans réponse
}, 1000);
// Solution : enregistrer un gestionnaire global d'exceptions pour journaliser les erreurs et quitter proprement avant que l'application ne plante
process.on('uncaughtException', (err) => {
console.error('Exception non interceptée :', err);
// Ici, vous devez d'abord journaliser l'erreur, puis quitter, afin d'éviter les plantages silencieux qui rendent les problèmes difficiles à diagnostiquer
process.exit(1); // Le code de sortie 1 indique une sortie anormale
});
process.on('unhandledRejection', (reason, promise) => {
// Déclenché lorsqu'une Promise est rejetée mais n'a pas de gestionnaire .catch()
console.error('Rejet de Promise non géré :', reason);
process.exit(1);
});
Analyse des scénarios adaptés
1. Service API RESTful
Exemple
// Tout d'abord, vous devez installer express : npm install express
const express = require('express');
const app = express();
app.use(express.json()); // Middleware : analyser automatiquement les données JSON dans le corps de la requête
// Requête GET : obtenir la liste des utilisateurs (correspond à la méthode HTTP GET, chemin /api/users)
app.get('/api/users', (req, res) => {
const users = [
{ id: 1, name: 'Zhang San' },
{ id: 2, name: 'Li Si' }
];
res.json(users); // Renvoyer les données au format JSON
});
// Requête POST : Créer un nouvel utilisateur (le client envoie les informations de l'utilisateur dans le corps de la requête)
app.post('/api/users', (req, res) => {
const newUser = req.body; // Récupérer les données JSON envoyées par le client
// Dans les projets réels, les informations de l'utilisateur sont généralement enregistrées dans une base de données ici
res.status(201).json({ message: 'Utilisateur créé avec succès', user: newUser });
});
// Requête PUT : Mettre à jour les informations de l'utilisateur spécifié (:id est un paramètre de route, par ex. /api/users/1 signifie mettre à jour l'utilisateur avec l'ID 1)
app.put('/api/users/:id', (req, res) => {
const { id } = req.params; // Récupérer l'ID de l'utilisateur à partir du chemin
const updatedData = req.body; // Récupérer le contenu de mise à jour depuis le corps de la requête
res.json({ message:`Utilisateur ${id}mis à jour avec succès`, data: updatedData });
});
// Requête DELETE : supprimer l'utilisateur spécifié
app.delete('/api/users/:id', (req, res) => {
const { id } = req.params;
res.json({ message:`Utilisateur ${id}supprimé avec succès`});
});
app.listen(3000, () => {
console.log('Serveur API en cours d'exécution à http://localhost:3000/');
});
2. Applications en temps réel
Exemple
// Tout d'abord, vous devez installer : npm install socket.io
const io = require('socket.io')(server);
// Écouter les événements de connexion client, déclenchés chaque fois qu'un nouvel utilisateur se connecte
io.on('connection', (socket) => {
// Écouter l'événement 'chat message' envoyé par le client actuellement connecté
socket.on('chat message', (msg) => {
io.emit('chat message', msg); // Diffuser le message à tous les clients en ligne (y compris l'expéditeur)
});
});
3. Middleware et services proxy
Exemple
// Transférer les requêtes /api/xxx du front-end vers le véritable serveur API backend, en masquant l'adresse réelle, couramment utilisé pour résoudre les problèmes CORS
// Tout d'abord, vous devez installer : npm install http-proxy-middleware
const httpProxy = require('http-proxy-middleware');
const proxy = httpProxy({
target: 'http://api.example.com', // Adresse réelle du serveur API backend
changeOrigin: true, // Remplace le Host dans l'en-tête de la requête par l'adresse cible
pathRewrite: {
'^/api': '' // Supprimer le préfixe /api du chemin avant de transférer, par exemple, /api/users → /users
}
});