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] :
| 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] :
| <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>
|

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 :
| 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 |