Comment créer une application Web de journalisation avec des événements envoyés par le serveur, RxJS et Express ?

Supposons que vous travaillez sur votre nouvelle idée géniale – une application web ou mobile, et un serveur back-end. Rien de bien compliqué jusqu’à présent. Jusqu’à ce que vous réalisiez que vous devez transmettre des données de votre serveur à ces clients.

En général, la première chose qui vous vient à l’esprit est d’utiliser l’un des petits génies du quartier, comme WebSockets, SocketIO, ou même un service payant qui s’en occupe pour vous.

Mais il existe une autre méthode qui est généralement laissée de côté et dont vous n’avez peut-être pas encore entendu parler. Il s’agit de la méthode SSE, abréviation de Server-Sent Events.

L’ESS occupe une place particulière dans mon cœur en raison de sa simplicité. C’est une méthode légère, efficace et très puissante.

Pour expliquer l’ESS en détail et la façon dont je l’utilise, je vais passer en revue un de mes petits projets parallèles qui, selon moi, est une excellente vitrine de l’ESS. J’utiliserai Typescript, Express et RxJS, alors préparez votre environnement et attachez votre ceinture, car nous sommes sur le point de plonger dans du code.

Avant de commencer, il y a quelque chose que vous devez savoir sur l’ESS. Comme son nom l’indique, les événements envoyés par le serveur sont unidirectionnels entre le serveur et le client. Cela peut être un obstacle si votre client doit renvoyer des données au serveur. Mais ce n’est pas le cas dans de nombreux scénarios, et nous pouvons simplement nous appuyer sur REST pour envoyer des données au serveur.

En quoi consiste le projet ?

L’idée de ce projet est simple : J’ai un tas de scripts qui tournent sur des Raspberry Pis, des droplets sur Digital Ocean, et d’autres endroits qui ne me sont pas facilement accessibles. Je veux donc un moyen d’imprimer les logs et de les visualiser de n’importe où.

Comme solution, je voudrais une application web de base pour pousser mes journaux et avoir un lien direct vers ma session que je peux ouvrir sur n’importe quel appareil ou même partager avec d’autres.

Voir aussi :  parseInt() in JavaScript - Exemple de conversion d'une chaîne de caractères en un nombre entier de caractères

Il y a quelques points à garder à l’esprit avant de poursuivre.

Tout d’abord, les journaux provenant de mes scripts ne sont pas très fréquents, et les frais généraux liés à l’utilisation de HTTP sont négligeables pour mon cas d’utilisation. Pour cette raison, j’ai décidé de publier mes journaux via une API REST de base et d’utiliser SSE côté client pour s’abonner aux journaux entrants.

Frame-8-1
Exemple de journalisation

Deuxièmement, cet outil sert principalement à déboguer rapidement les choses sur lesquelles je travaille. Il existe de nombreux outils de production et d’entreprise que je pourrais utiliser à la place. Mais je voulais quelque chose de très léger et facile à utiliser.

Écrivons du code côté serveur

La configuration côté serveur est simple. Commençons donc par un diagramme pour vous donner une idée de la configuration avant de tout expliquer en détail.

Frame-10-1
Schéma du serveur

Si nous considérons notre serveur dorsal comme un pipeline, nous avons à une extrémité une série d’éditeurs – dans notre cas, les scripts publiant les journaux. À l’autre extrémité, nous avons des clients qui s’abonnent à ces journaux.

Pour connecter ces deux extrémités, je vais utiliser un sujet RxJS. Il me permettra de publier n’importe quoi à partir des éditeurs via REST, puis de m’abonner à ces événements et de transmettre les messages aux clients via SSE.

Pour commencer, définissons notre interface de journal. Pour garder les choses simples, je vais seulement définir un champ de contenu qui contiendra nos informations de journal.

interface Log {
  contenu : chaîne de caractères ;
}

Comment configurer RxJS

Importons RxJS, créons un nouveau sujet pour nos journaux et définissons une fonction pour publier nos journaux dans ce sujet.

Bien sûr, nous pourrions exporter notre Subject et l’appeler directement depuis notre routeur, mais je préfère faire abstraction de l’implémentation et ne fournir que la fonction emit au reste de mon code.

import { Subject } from 'rxjs' ;

// Sujet du journal
const NewLog$ = new Subject() ;

/**
 * Émet un nouveau log vers le sujet RxJS
 * @param log
 */
export function emitNewLog(log : Log) : void {
    NewLog$.next(log) ;
}

Enfin, définissons une nouvelle route sur notre serveur Express qui acceptera les nouveaux logs de notre client et les publiera dans la méthode emitNewLog que nous venons de créer.

app.post('/', (req : Request, res : Response) => {
  const content = req.body.content ;
  const log : Log = { content : content } ;
  emitNewLog(log) ;
  return res.status(200).json({ ok : true }) ;
}) ;

Nous en avons maintenant terminé avec la partie publication. Il ne nous reste plus qu’à définir notre route SSE, à nous abonner au sujet RxJS et à transmettre les journaux à notre client.

Voir aussi :  What is PostCSS? How to Use Plugins to Automate CSS Tasks

Comment configurer la route SSE

Définissons une nouvelle route pour notre connexion SSE. Pour activer l’ESS, nous devons renvoyer un certain nombre d’en-têtes à notre client.

Nous voulons que la « Connexion » soit définie sur « keep-alive », le « Cache-Control » sur « no-cache »et le « Content-Type » sur « text/event-stream ». De cette façon, notre client comprendra qu’il s’agit d’une route SSE.

De plus, j’ai ajouté ‘Access-Control-Allow-Origin’ pour CORS et ‘X-Accel-Buffering’ défini à ‘no’ pour empêcher Nginx d’interférer avec cette route. Enfin, nous pouvons renvoyer les en-têtes à notre client pour lancer le flux d’événements.

app.get('/', (req : Request, res : Response) => {
  res.setHeader('Cache-Control', 'no-cache') ;
  res.setHeader('Content-Type', 'text/event-stream') ;
  res.setHeader('Connection', 'keep-alive') ;
  res.setHeader('Access-Control-Allow-Origin', '*') ;
  res.setHeader('X-Accel-Buffering', 'no') ;
  res.flushHeaders() ;
}) ;

Nous pouvons maintenant commencer le flux de données en écrivant quelque chose dans notre réponse.

SSE fournit un protocole textuel que nous pouvons utiliser pour aider nos clients à différencier les types d’événements. Chacun de nos événements devrait ressembler à ce qui suit :

événement : ${nom de l'événement}n
données : ${données de l'événement}nn

Pour me faciliter un peu la vie, j’ai créé une fonction d’aide qui s’occupe de la sérialisation pour nous.

/**
 * Sérialiseur de messages SSE
 * @param event : Nom de l'événement
 * @param data : Données de l'événement
 */
function serializeEvent(event : string, data : any) : string {
  const jsonString = JSON.stringify(data) ;
  return `event : ${event}ndata : ${jsonString}nn` ;
}

Nous pouvons maintenant nous abonner au sujet RxJS que nous avons créé plus tôt, sérialiser chaque nouveau journal et l’écrire en tant qu’événement NEW_LOG sur notre connexion.

app.get('/', (req : Request, res : Response) => {
  res.setHeader('Cache-Control', 'no-cache') ;
  res.setHeader('Content-Type', 'text/event-stream') ;
  res.setHeader('Connection', 'keep-alive') ;
  res.setHeader('Access-Control-Allow-Origin', '*') ;
  res.setHeader('X-Accel-Buffering', 'no') ;
  res.flushHeaders() ;

  NewLog$.subscribe((log : Log) => {
    res.write(serializeEvent('NEW_LOG', log)) ;
  }) ;

}

Enfin, nous devons nous assurer de nous désabonner de notre observateur lorsque la connexion SSE est fermée. En mettant tout cela ensemble, nous devrions avoir quelque chose comme ceci :

app.get('/', (req : Request, res : Response) => {
  res.setHeader('Cache-Control', 'no-cache') ;
  res.setHeader('Content-Type', 'text/event-stream') ;
  res.setHeader('Connection', 'keep-alive') ;
  res.setHeader('Access-Control-Allow-Origin', '*') ;
  res.setHeader('X-Accel-Buffering', 'no') ;
  res.flushHeaders() ;

  const stream$ = NewLog$.subscribe((log : Log) => {
    res.write(serializeEvent('NEW_LOG', log)) ;
  }) ;

  req.on('close', () => {
    stream$.unsubscribe() ;
  }) ;
}) ;

C’est tout ! Nous avons terminé avec notre serveur backend et il est temps de passer au code frontend.

Voir aussi :  Comment créer un clone de Wordle en JavaScript

Écrire le code client

S’abonner à notre route SSE sur le navigateur est très simple. Tout d’abord, passons à notre code client et créons une nouvelle instance de l’interface EventSource et passons notre endpoint au constructeur.

const eventSource = new EventSource("/") ;

Ensuite, nous pouvons ajouter des écouteurs d’événements pour les événements auxquels nous voulons nous abonner (dans notre cas, NEW_LOG) et définir une méthode de rappel pour gérer notre journal.

eventSource.addEventListener(
   "NEW_LOG", (event) => {
       const log = JSON.parse(event.data) ;
       // utilise les données pour mettre à jour l'interface utilisateur
    }, false
) ;

Et enfin, nous pouvons fermer la connexion dès que nous avons fini d’écouter ces événements.

eventSource.close() ;

Conclusion

Comme vous pouvez le constater, les événements envoyés par le serveur permettent de diffuser très facilement du contenu du serveur vers le client. Ils sont particulièrement utiles parce que nous disposons d’une interface intégrée dans la plupart des navigateurs modernes, et nous pouvons facilement poly-remplir ceux qui ne fournissent pas l’interface.

En outre, l’ESS gère automatiquement la reconnexion pour nous au cas où le client perdrait la connexion avec le serveur. Il s’agit donc d’une alternative valable à SocketIO et WebSockets dans divers scénarios où nous avons besoin d’un flux d’événements unidirectionnel en provenance du serveur.

Si vous êtes intéressés par ce projet, j’ai ajouté quelques fonctionnalités supplémentaires au code que nous venons de passer en revue et une interface graphique web que vous pouvez consulter ici : Console LogSnag.

Frame-9-1
Démonstration de la console