Skip to content

6. Portées des données

Une application web sert de nombreux utilisateurs à la fois, et chacun d'eux enchaîne de nombreuses requêtes. Une donnée peut donc être partagée à trois niveaux, qu'on appelle sa portée :

  • la portée application : la donnée est partagée par tous les utilisateurs, pendant toute la vie du serveur (la liste des médecins, par exemple) ;
  • la portée requête : la donnée n'existe que le temps de traiter une requête (le modèle d'une vue) ;
  • la portée client : la donnée est propre à un utilisateur, et doit survivre d'une requête à la suivante (la langue choisie, l'utilisateur connecté).

Le cours d'origine utilisait pour cette dernière portée la session d'[ASP.NET] : une mémoire, tenue par le serveur, propre à chaque navigateur. Ce document, comme l'étude de cas, s'en passe : les données de portée client sont gardées par le navigateur, dans des cookies. Le serveur ne garde rien entre deux requêtes d'un même utilisateur : on dit qu'il est sans état ([stateless]). C'est ce qui permet, par exemple, de faire tourner plusieurs serveurs identiques derrière un répartiteur de charge. La contrepartie : un cookie est sous le contrôle de l'utilisateur, qui peut le lire et le modifier. Le chapitre 8 montrera comment un jeton signé permet de s'y fier malgré tout.

6.1. Exemple 23 : trois portées

Le fichier [src/compteur.service.ts] :

import { Injectable } from '@nestjs/common';

// portée APPLICATION (le défaut) : une seule instance, créée au démarrage et
// partagée par TOUTES les requêtes de TOUS les utilisateurs
@Injectable()
export class CompteurService {
  private total = 0;

  incrementer(): number {
    return ++this.total;
  }
}

lignes 5-6 : par défaut, un service [NestJS] est un singleton : une seule instance, créée au démarrage, partagée par toutes les requêtes. Son champ [total] est donc de portée application.

Le fichier [src/requete.service.ts] :

1
2
3
4
5
6
7
8
9
import { Injectable, Scope } from '@nestjs/common';

let instancesCreees = 0;

// portée REQUÊTE : une NOUVELLE instance pour chaque requête HTTP
@Injectable({ scope: Scope.REQUEST })
export class RequeteService {
  readonly numero = ++instancesCreees;
}

ligne 6 : l'option [scope: Scope.REQUEST] demande une nouvelle instance pour chaque requête. Le numéro d'instance le montre. On utilise rarement cette possibilité : elle a un coût (création d'objets à chaque requête), et elle se propage : un contrôleur qui dépend d'un service de portée requête devient lui-même de portée requête. [res.locals] suffit le plus souvent.

Le fichier [src/portees.controller.ts] :

import { Controller, Get, Req, Res } from '@nestjs/common';
import { Request, Response } from 'express';
import { CompteurService } from './compteur.service';
import { RequeteService } from './requete.service';

@Controller()
export class PorteesController {
  constructor(
    private readonly compteur: CompteurService,
    private readonly requete: RequeteService,
  ) {}

  @Get()
  portees(@Req() req: Request, @Res() res: Response): void {
    // portée CLIENT : une donnée conservée par le NAVIGATEUR (cookie),
    // renvoyée au serveur à chaque requête
    const mesVisites = Number(req.cookies?.mesVisites ?? 0) + 1;
    res.cookie('mesVisites', String(mesVisites), { httpOnly: true, sameSite: 'lax' });
    res.render('portees', {
      total: this.compteur.incrementer(),
      instance: this.requete.numero,
      mesVisites,
    });
  }
}
  • lignes 8-11 : les deux services sont injectés ;
  • lignes 17-18 : la portée client : un compteur gardé dans un cookie. Le navigateur le renvoie à chaque requête ; l'action l'incrémente et le redépose ;
  • lignes 19-23 : les trois valeurs sont passées à la vue.

Le fichier [src/main.ts] :

import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import * as cookieParser from 'cookie-parser';
import { NextFunction, Request, Response } from 'express';
import { dirname, join } from 'path';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  app.useStaticAssets(join(dirname(require.resolve('bootstrap/package.json')), 'dist'), { prefix: '/bootstrap/' });
  app.setBaseViewsDir(join(__dirname, '..', 'views'));
  app.setViewEngine('hbs');
  app.use(cookieParser());
  // portée REQUÊTE : res.locals naît et meurt avec la requête
  app.use((req: Request, res: Response, next: NextFunction) => {
    res.locals.debut = new Date().toISOString().slice(11, 23);
    next();
  });
  await app.listen(3000);
  console.log('Serveur démarré sur http://localhost:3000');
}

bootstrap();

lignes 15-18 : la portée requête avec [res.locals] : l'heure de réception de la requête.

Le fichier [views/portees.hbs] :

1
2
3
4
5
6
<h1 class="h3">Les portées des données</h1>
<ul>
  <li>application : {{total}} visite(s) du site, tous navigateurs confondus</li>
  <li>requête : instance n° {{instance}} de RequeteService, requête reçue à {{debut}}</li>
  <li>client : {{mesVisites}} visite(s) de ce navigateur</li>
</ul>

Image

Simulons deux navigateurs avec [curl] et deux fichiers de cookies, [a.txt] et [b.txt]. Le navigateur A fait deux requêtes, puis le navigateur B une, puis A une dernière :

1
2
3
4
curl -c a.txt -b a.txt http://localhost:3000/
curl -c a.txt -b a.txt http://localhost:3000/
curl -c b.txt -b b.txt http://localhost:3000/
curl -c a.txt -b a.txt http://localhost:3000/
navigateur A :
application : 1 visite(s) du site, tous navigateurs confondus
requête : instance n° 1 de RequeteService, requête reçue à 14:16:07.195
client : 1 visite(s) de ce navigateur

navigateur A :
application : 2 visite(s) du site, tous navigateurs confondus
requête : instance n° 2 de RequeteService, requête reçue à 14:16:07.237
client : 2 visite(s) de ce navigateur

navigateur B :
application : 3 visite(s) du site, tous navigateurs confondus
requête : instance n° 3 de RequeteService, requête reçue à 14:16:07.251
client : 1 visite(s) de ce navigateur

navigateur A :
application : 4 visite(s) du site, tous navigateurs confondus
requête : instance n° 4 de RequeteService, requête reçue à 14:16:07.263
client : 3 visite(s) de ce navigateur
  • le compteur application compte toutes les visites : 1, 2, 3, 4 ;
  • chaque requête a sa propre instance de [RequeteService] : 1, 2, 3, 4 ;
  • le compteur client est propre à chaque navigateur : A en est à 3 visites, B à 1.

Dans l'étude de cas :

Portée
Données
Où ?
application
listes des médecins et des clients, dictionnaires, captchas en attente
services singletons ([ApplicationModelService], [CaptchaService]...)
requête
langue, chemin, message flash, utilisateur connecté
[res.locals]
client
jeton d’authentification, langue, message flash
cookies