Aller au contenu
BackNode.jsTips

Les événements Node.js par l'exemple

Les événements régissent une grande partie du noyau #Nodejs. Partons d'une boîte aux lettres et (re)découvrons leur fonctionnement.

Bloc de boîtes aux lettres jaunes dans l'entrée d'immeuble
Photo by Andrea De Santis / Unsplash

En Node.js, quand on veut qu'une action se réalise à la suite d'une autre, on peut opter pour l'imbrication de callbacks ou l'enchaînement de promesses. Mais ces deux techniques lient étroitement l'action initiale et l'action consécutive, rendant plus compliquée toute modification ultérieure de l'action consécutive.

Les émetteurs d'événements proposent une autre manière de structurer cette relation : le modèle de publication et souscription. Dans ce modèle, un éditeur (l'émetteur d'événements) transmet un message (un événement), et un souscripteur reçoit l'événement et effectue une action.

L'avantage de ce modèle est que l'éditeur n'a pas besoin de connaître les souscripteurs. Un éditeur émet un message, et c'est à chaque souscripteur de réagir à ce message de la manière qui lui convient le mieux. Si l'on souhaite modifier le comportement de notre application, il est possible de changer la façon dont les souscripteurs réagissent aux événements, sans avoir à modifier l'éditeur.

Nous allons illustrer cette notion d'événementiel avec un peu de courrier ✉️

Créer un premier événement

Le module events va nous permettre de créer et de traiter les événements. Dans ce module se trouve la classe EventEmitter. Des objets de cette classe vont pouvoir émettre des événements avec la méthode emit().

const { EventEmitter } = require("events");

const ourEmitter = new EventEmitter();
ourEmitter.emit('hello');

Nous allons créer une classe Mailbox, qui hérite de l'EventEmitter, pour représenter notre boîte aux lettres. À cette boîte aux lettres, nous ajoutons une taille maximale. Une fois cette limite atteinte, nous considérerons qu'elle est pleine et qu'elle ne peut plus recevoir de courrier supplémentaire.

const { EventEmitter } = require("events");

class Mailbox extends EventEmitter {
    constructor(maxSize) {
        super();
        this.maxSize = maxSize;
        this.box = [];
    }
}

mailbox.js

Ajoutons dans la classe une méthode receiveMail() qui va envoyer un événement quand un nouveau message arrive dans la boîte. Une fois la boîte pleine, nous arrêtons d'insérer de nouveaux messages mais nous écrivons un log.

receiveMail(email) {
    if (this.box.length < this.maxSize) {
        this.box.push(email);
        this.emit("mail");
        return;
    }
    console.error("Mailbox is full");
}

mailbox.js

Traiter les événements

Notre boîte aux lettres est capable d'émettre des événements mais malheureusement, personne ne les écoute actuellement. Pour y remédier, ajoutons un souscripteur avec la méthode on().

const mailbox = new Mailbox(3);
mailbox.on("mail", () => console.log("You've got mail!"));
mailbox.receiveMail("hello");

mailbox.js

La fonction on prend en paramètre le nom de l'événement qu'on souhaite écouter et une fonction callback qui va traiter l'événement. Dans notre cas, on souhaite écouter l'évènement mail émis par notre boîte aux lettres et lorsqu'un nouveau courrier arrive, on log un "You've got mail!".

$ node mailbox.js
You've got mail!
Il existe également la fonction addListener() pour ajouter un souscripteur. Les deux fonctions ont la même utilité, puisque addListener() est en fait un alias de la méthode on().

Comme notre boîte ne peut contenir que 3 messages, si l'on essaye d'envoyer plus de 3 messages, la boîte arrêtera d'envoyer des événements mail et affichera dans le terminal un message de saturation.

const mailbox = new Mailbox(3);
mailbox.on("mail", () => console.log("You've got mail!"));
mailbox.receiveMail("hello");	// Affiche "You've got mail!"
mailbox.receiveMail("this");	// Affiche "You've got mail!"
mailbox.receiveMail("is");		// Affiche "You've got mail!"
mailbox.receiveMail("me");		// Affiche "Mailbox is full"
mailbox.receiveMail("!");		// Affiche "Mailbox is full"

Un autre souscripteur qui n'a pas l'envie d'être sollicité en permanence veut écouter la boîte aux lettres qu'une seule fois. Ainsi la méthode once() permet d'écouter l'événement de façon unique avant de se désabonner automatiquement.

mailbox.on("mail", () => console.log("You've got mail!"));
mailbox.once("mail", () => console.log("One mail is all it takes..."));
mailbox.receiveMail("first mail");
mailbox.receiveMail("second mail");
mailbox.receiveMail("last mail");

// You've got mail!
// One mail is all it takes...
// You've got mail!
// You've got mail!

Comme vous l'avez compris, il y a bien deux souscripteurs pour le même événement. Mais il est tout à fait possible de rajouter plusieurs souscripteurs qui écoutent le même événement, et chaque souscripteur va lancer son callback quand l'événement sera émis.

Enrichir les messages

Pour le moment, notre boîte aux lettres envoie des événements vides, c'est-à-dire sans donnée supplémentaire. La méthode emit() de la classe EventEmitter accepte des paramètres supplémentaires, qui sont des éléments qui vont accompagner l'événement. Nous allons modifier la classe Mailbox afin d'émettre des événements enrichis.

const { EventEmitter } = require("events");

class Mailbox extends EventEmitter {
    constructor(maxSize) {
        super();
        this.maxSize = maxSize;
        this.box = [];
    }

    receiveMail(email) {
        if (this.box.length < this.maxSize) {
            this.box.push(email);
            this.emit("mail", email, Date.now());
            return;
        }
        console.error("Mailbox is full");
    }
}

const mailbox = new Mailbox(3);
mailbox.on("mail", (email, date) => console.log(`Mail received at ${date}! It says: ${email}`));
mailbox.receiveMail("hello");
mailbox.receiveMail("this");
mailbox.receiveMail("is");
mailbox.receiveMail("me");
mailbox.receiveMail("!");

mailbox.js

L'événement mail est enrichi avec le texte du mail et la date de réception. Quand on lance le programme, on voit que le callback récupère les éléments envoyés par notre boîte aux lettres.

$ node mailbox.js
Mail received at 1683031214652! It says: hello
Mail received at 1683031214655! It says: this
Mail received at 1683031214655! It says: is
Mailbox is full
Mailbox is full

Gestions des erreurs

Modifions une nouvelle fois le fichier. Au lieu d'écrire dans la console, nous allons émettre un événement error qui contient une erreur.

receiveMail(email) {
    if (this.box.length < this.maxSize) {
        this.box.push(email);
        this.emit("mail", email, Date.now());
        return;
    }
    this.emit("error", new Error("Mailbox is full"));
}

Personne n'écoute cet événement error pour le moment mais exécutons quand même le fichier pour voir ce qu'il se passe.

$ node mailbox.js
Mail received at 1683031792230! It says: hello
Mail received at 1683031792233! It says: this
Mail received at 1683031792233! It says: is
node:events:491
      throw er; // Unhandled 'error' event
      ^

Error: Mailbox is full
    at Mailbox.receiveMail (mails.js:16:28)
    at Object.<anonymous> (mails.js:25:9)
    at Module._compile (node:internal/modules/cjs/loader:1254:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1308:10)
    at Module.load (node:internal/modules/cjs/loader:1117:32)
    at Module._load (node:internal/modules/cjs/loader:958:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    at node:internal/main/run_main_module:23:47
Emitted 'error' event on Mailbox instance at:
    at Mailbox.receiveMail (mails.js:16:14)
    at Object.<anonymous> (mails.js:25:9)
    [... lines matching original stack trace ...]
    at node:internal/main/run_main_module:23:47

Oups, la boîte aux lettres a fait exploser notre programme. Dans le message, on voit bien que l'événement error a été émis par notre boîte aux lettres (Emitted 'error' event on Mailbox instance) mais il n'a pas été géré (Unhandled 'error' event).

Si un EventEmitter émet un événement error et qu'aucun souscripteur n'est disponible pour l'écouter, alors l'erreur est levée, la stack trace est imprimée et le processus Node.js se termine.

Il faut donc rajouter un souscripteur qui va se charger des erreurs émises par notre boîte aux lettres.

const { EventEmitter } = require("events");

class Mailbox extends EventEmitter {
    constructor(maxSize) {
        super();
        this.maxSize = maxSize;
        this.box = [];
    }

    receiveMail(email) {
        if (this.box.length < this.maxSize) {
            this.box.push(email);
            this.emit("mail", email, Date.now());
            return;
        }
        this.emit("error", new Error("Mailbox is full"));
    }
}

const mailbox = new Mailbox(3);
mailbox.on("mail", (mail, date) => console.log(`Mail received at ${date}! It says: ${mail}`));
mailbox.on("error", (error) => console.log(`Something went wrong with the mailbox: ${error}`));
mailbox.receiveMail("hello");
mailbox.receiveMail("this");
mailbox.receiveMail("is");
mailbox.receiveMail("me");
mailbox.receiveMail("!");

mailbox.js

$ node mailbox.js
Mail received at 1683033385024! It says: hello
Mail received at 1683033385027! It says: this
Mail received at 1683033385027! It says: is
Something went wrong with the mailbox: Error: Mailbox is full
Something went wrong with the mailbox: Error: Mailbox is full

Les événements dans le noyau Node.js

Étant donnée sa nature de moteur Javascript asynchrone, Node.js s'appuie lui-même sur les événements dans son propre noyau.

const { createReadStream } = require('fs');

const readFileStream = createReadStream(__filename, { highWaterMark: 100 });
let bytesRead = 0;

readFileStream.on("ready", () => {
    console.log("Ready to read file");
});
readFileStream.on("data", (chunk) => {
    bytesRead += chunk.length; 
    console.log(`${chunk.length} bytes read`);
});
readFileStream.on("end", () => {
    console.log(`Finished reading file, ${bytesRead} bytes read in total`);
});

process.on("exit", (code) => {
    console.log(`Exiting Node, status code = ${code}`);
});

my-events.js

Dans ce fichier, on utilise le module fs ainsi que la variable globale process qui nous sont fournis clé en main par Node. La fonction createReadStream du module fs est importée et nous renvoie un flux de données lisant le fichier courant (__filename).

Les variables readFileStream et process écoutent des événements prédéfinis dans le cœur de Node.js, il n'y a pas de emit(data) que nous avons codé dans ce fichier. Comme avec notre boîte aux lettres, on écoute des événements, et on les traite.

Le flux nous signale quand il est prêt à être utilisé avec un événement ready. Quand il obtient de la donnée, il envoie un événement data avec la donnée. Enfin, quand il n'y a plus de donnée à traiter, le flux émet un événement end.

Quant au process, il envoie un événement exit quand Node est sur le point de s'arrêter. On décide de l'écouter, et on affiche le code de sortie du processus (0 si tout s'est bien passé, un nombre non nul en cas d'échec).

Si on exécute ce code, on voit les différents événements émis nativement au fil du temps.

$ node my-events.js
Ready to read file
100 bytes read
100 bytes read
100 bytes read
100 bytes read
100 bytes read
48 bytes read
Finished reading file, 548 bytes read in total
Exiting Node, status code = 0

Pour aller plus loin

Approfondissez vos connaissances sur le module events avec la documentation officielle de Node.js.

Ensuite, pourquoi ne pas explorer d'autres mécanismes d'événements, cette fois-ci côté navigateur ? Découvrez comment le Local Storage utilise les événements en JavaScript pour créer des interactions dynamiques.

Découvrez la puissance des événements du Local Storage en JavaScript
Révolutionnez vos applications web en exploitant pleinement les événements du Local Storage

Dernier