Guide complet du système de modules CommonJS dans Node.js

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 :

  1. Modules natifs : fs, http, path sont chargés directement depuis la bibliothèque interne de Node.js.
  2. 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
  1. Modules sans chemin : recherche depuis node_modules dans 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

  1. 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.

  1. 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
}

  1. 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.

Étiquettes: nodejs CommonJS modules-javascript require exports

Publié le 10 juillet à 00h09