Node.js utilise principalement le système de modules CommonJS pour organiser et réutiliser le code JavaScript. Cette approche modulaire permet de structurer la logique applicative de manière plus claire et maintneable.
Syntaxe fondamentale
CommonJS repose sur deux mécanismes essentiels : l'export via module.exports ou exports, et l'import via la fonction require().
// utilisateur.js - Export de données
const prenom = 'marie'
const annees = 25
module.exports = { prenom, annees }
// app.js - Import des données
const donnees = require('./utilisateur.js')
console.log(donnees) // { prenom: 'marie', annees: 25 }
Relation entre exports et module.exports
Il est possible d'utiliser indifféremmment exports.propriete et module.exports.propriete pour ajouter des éléments à l'objet d'export :
// config.js
const env = 'production'
const port = 3000
exports.env = env
module.exports.port = port
// serveur.js
const cfg = require('./config.js')
console.log(cfg) // { env: 'production', port: 3000 }
En réalité, Node.js initialise exports comme une référence vers module.exports. Toute propriété ajoutée à exports se retrouve donc dans module.exports.
Cependant, si l'on réassigne directement module.exports à un nouvel objet, la référence entre les deux est rompue :
// service.js
const version = '1.0.0'
const api = '/api/v1'
exports.version = version
exports.api = api
module.exports = { endpoint: '/graphql' }
// client.js
const svc = require('./service.js')
console.log(svc) // { endpoint: '/graphql' }
Seul le contenu de module.exports est retourné par require(), car la réassignation crée un nouvel objet en mémoire.
Preuve que require utilise module.exports
Si require() retournait le contenu de exports, la réassignation de ce dernier devrait fonctionner :
// helper.js
const saluer = function() {
console.log('bonjour')
}
exports = { saluer }
// main.js
const h = require('./helper.js')
console.log(h) // {}
Le résultat est un objet vide, confirmant que require() retourne toujours module.exports, qui n'a pas été modifié par la réassignasion de exports.
Règles de résolution des modules par require
La fonction require() suit un ordre précis pour localiser les modules :
- Modules natifs :
fs,http,pathsont chargés directement depuis la bibliothèque interne de Node.js. - Chemins relatifs ou absolus :
./,../, ou/
- Fichier : avec extension, recherche exacte ; sans extension, Node essaie successivement
.js,.json,.node - Dossier : recherche de
index.js,index.json,index.node
- Modules sans chemin : recherche depuis
node_modulesdans le répertoire courant, puis remonte récursivement vers les répertoires parents.
Cycle de chargement des modules
Chaque module importé via require() est exécuté une seule fois :
// logger.js
const niveau = 'info'
exports.niveau = niveau
console.log('logger initialisé')
// app.js
require('./logger.js')
// Sortie : logger initialisé
Node.js maintient un cache via la propriété loaded de l'objet module pour éviter les chargements multiples.
Gestion des dépendances circulaires
En cas d'imports circulaires, Node.js applique un parcours en profondeur (DFS). L'ordre d'exécution suit la chaîne jusqu'au nœud terminal, puis remonte :
// main.js -> auth.js -> database.js -> config.js
// Ordre d'exécution : main, auth, database, config
Chaque module n'est exécuté qu'une fois, même s'il est référencé par plusieurs autres.
Caractéristiques principales de CommonJS
- Chargement synchrone
// module-a.js
const titre = 'Module A'
exports.titre = titre
console.log('Module A chargé')
// app.js
require('./module-a.js')
console.log('Application démarrée')
// Sortie : Module A chargé, puis Application démarrée
Cette approche convient au côté serveur où les fichiers sont locaux, mais peut causer des blocages côté navigateur.
- Résolution à l'exécution
Les imports CommonJS sont évalués lors de l'exécution, permettant des imports conditionnels ou dynamiques :
// config.js
const dbName = 'myapp'
module.exports = { dbName }
// app.js
const chemin = './config.js'
const estProduction = process.env.NODE_ENV === 'production'
if (estProduction) {
const { dbName } = require(chemin)
console.log(dbName) // myapp
}
- Référence partagée
L'objet retourné par require() est une référence vers module.exports. Les modifications mutables sont partagées :
// compteur.js
let total = 0
setInterval(() => {
module.exports.total = ++total
}, 1000)
module.exports = { total }
// moniteur.js
const stats = require('./compteur.js')
console.log(stats) // { total: 0 }
setTimeout(() => {
console.log(stats) // { total: 2 }
}, 2500)
Les modifications effectuées sur module.exports après l'import initial sont visibles côté importeur.