Services

Avec les composants et les directives, les services sont l'un des principaux blocs de construction d'une application Angular.

La seule préoccupation d'un composant devrait être d'afficher des données et non de les gérer. Les services sont là où l'équipe Angular préconise de placer la logique métier et la gestion des données de l'application. Avoir une séparation claire entre la couche de présentation et les autres traitements de l'application augmente la réutilisabilité et la modularité.

La création d'un service avec le CLI se fait comme suit :

ng generate service services/example

Cela créera le service et sa classe de test associée dans le dossier app/services. Il est courant de placer les services dans un dossier de services, le CLI créera le dossier s'il n'existe pas déjà.

Le contenu suivant est généré automatiquement dans le fichier example.service.ts :

import { Injectable } from '@angular/core'

@Injectable({
  providedIn: 'root'
})
export class ExampleService {

}

Lorsqu'un composant nécessite un service, le service doit être ajouté à son constructeur de la manière suivante :

import { Component } from '@angular/core'
import { ExampleService } from '@services/example.service'

@Component({
  selector: 'app-example',
  templateUrl: './example.component.html'
})
export class ExampleComponent {

  constructor(
    private exampleService: ExampleService
  ) {}
}










 


TIP

Déclarez toujours une dépendance à un service comme privée. En effet le template ne devrait jamais accéder directement à un service mais toujours passer par une propriété ou une méthode exposée par la classe composant.

Dependency Injection

Dans le chapitre précédent, nous avons injecté des services fournis par la librairie @angular/router dans les composants qui en ont besoin. Si vous êtes familier avec Spring, vous n'y avez peut-être pas beaucoup réfléchi car c'est l'un des mécanismes du framework.

Au démarrage, Angular crée un injecteur à l'échelle de l'application. Si d'autres injecteurs sont nécessaires, Angular les créera en cours de route. L'injecteur crée des dépendances (le plus souvent sous forme de services) et maintient un conteneur d'instances de dépendances qu'il réutilise si possible. L'injecteur obtient les informations sur la façon de créer ou de récupérer une dépendance auprès d'un fournisseur (provider). Un service agit généralement comme son propre fournisseur.

Vous ne l'avez peut-être pas réalisé, mais nous avons déjà fait appel à des fournisseurs. Dans le chapitre sur les pipes, pour utiliser le UpperCasePipe dans la classe du composant plutôt que dans le template, nous l'avons ajouté au tableau des providers du composant.

Lorsque Angular découvre qu'un composant dépend d'un service, il vérifie d'abord si l'injecteur a une instance existante de ce service. Si une instance du service demandé n'existe pas encore, l'injecteur en crée une à l'aide du provider enregistré et l'ajoute à l'injecteur avant de renvoyer le service à Angular. Lorsque tous les services demandés ont été résolus et renvoyés, Angular peut appeler le constructeur du composant avec ces services comme arguments.

Les dépendances peuvent être fournies à trois niveaux :

  • au niveau root: c'est le comportement par défaut lors de la création d'un service avec le CLI. C'est ce que signifie providedIn: 'root'. La même instance de la dépendance est injectée partout où elle est nécessaire comme s'il s'agissait d'un singleton.
  • au niveau du module: la dépendance est ajoutée au tableau de providers du NgModule. Le module obtient sa propre instance de la dépendance
  • au niveau du composant: la dépendance est ajoutée au tableau des providers du composant. Chaque instance de ce composant obtient sa propre instance de la dépendance.

TP : Gestion de l'État

  1. Générez un AuthenticationService avec le CLI dans le dossier app/services
  2. Déplacez la logique du loggedIn de l'AppComponent vers le service
  3. Injectez le service dans le LoginFormComponent et utilisez-le.
  4. Implémentez une méthode de déconnexion dans le service d'authentification et ajoutez un bouton de déconnexion dans l'AppComponent qui l'appelle et provoque une navigation vers le LoginFormComponent. Voici l'html et le css :
<button class="logout">Logout</button>
<router-outlet></router-outlet>
.logout {
  align-self: end;
}
  1. Afficher conditionnellement le bouton Logout en fonction du statut loggedIn de l'utilisateur
  2. Utilisez un navigation guard pour rediriger l'utilisateur qui souhaite accéder à la page de recherche de films vers /login s'il n'est pas authentifié (rendez le CanActivate vrai si la route est accessible sinon retournez un UrlTree via la méthode createUrlTree du service Router). Pour prendre en considération des cas d'usage futur, ajoutez un returnUrl en tant que queryParam au UrlTree renvoyé afin que le LoginFormComponent sache où revenir après l'authentification et modifiez le LoginFormComponent en conséquence. Pour générer le navigation guard, utilisez la commande du CLI suivante :
ng generate guard guards/authentication
# ? Which interfaces would you like to implement? CanActivate
Aide pour l'UrlTree
this.router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url }})
Résultat attendu

Résultat visuel du TP services

Résultat visuel du TP services

Le HttpClient

Dans une Single Page Application (SPA), la communication avec le serveur se fait via des requêtes HTTP asynchrones (AJAX) ou des protocoles plus spécialisés tels que WebSocket. Nous allons voir comment faire ces requêtes réseau depuis une application Angular.

Angular fournit un module, le HttpClientModule, pour effectuer des appels HTTP. Le module fournit un service injectable, le HttpClient, pour faire des requêtes GET, POST, PATCH, DELETE et PUT. Pour injecter le HttpClient dans un service, ajoutez d'abord le HttpClientModule au tableau imports de AppModule.

Voici quelques exemples:

import { Injectable } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { Observable } from 'rxjs'
import { User } from '@models/user/user'
import { UserCreation } from '@models/user/user-creation'
import { UserEdition } from '@models/user/user-edition'

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private baseUrl = 'api/backoffice/users'

  constructor(private httpClient: HttpClient) {}

  public create(user: UserCreation): Observable<User> {
    return this.httpClient.post<User>(this.baseUrl, user)
  }

  public update(ref: string, user: UserEdition): Observable<User> {
    return this.httpClient.put<User>(`${this.baseUrl}/${ref}`, user)
  }

  public getByUserReference(ref: string): Observable<User> {
    return this.httpClient.get<User>(`${this.baseUrl}/${ref}`)
  }
}
import { Component } from '@angular/core'
import { User } from '@models/user/user'
import { UserService } from '@services/user.service'

@Component({
  selector: 'app-user',
  templateUrl: './user.component.html'
})
export class UserComponent {
  user: User | null = null
  reference = ''

  constructor(private userService: UserService) {}

  getUser(): void {
    this.userService.getByUserReference(this.reference))
      .subscribe(user => this.user = user)
  }
}

Les méthodes du service HttpClient renvoient des Observables. Ils seront traités dans le prochain chapitre sur la librairie RxJS. Un Observable n'est exécuté qu'une fois souscrit via la méthode subscribe. La méthode subscribe s'attend à ce qu'au moins une callback lui soit passé. La callback est le plus souvent fournie sous forme d'une fonction flèche (arrow function).

TP : Appeler un backend

Nous utiliserons une API (le backend) pour authentifier les utilisateurs et rechercher des films. Ce backend a déjà été développé et peut être déployé localement en utilisant les lignes de commande suivantes (cloner le repo dans votre dossier de dépôts git habituel):

git clone https://github.com/worldline/vuejs-training-backend.git
cd vuejs-training-backend
npm install
npm start

La commande npm start vous demandera une clé d'API. Attendez que votre instructeur vous la donne ou vous pouvez en générer une iciopen in new window

TIP

Le contrat d'interface backend est disponible ici : api-docsopen in new window

  1. Ajoutez au dossier src soit le fichier proxy.conf.json si vous n'êtes pas derrière un proxy d'entreprise, soit le fichier proxy.conf.js.
{
  "/api/*": {
    "target": "http://localhost:3030",
    "changeOrigin": true,
    "pathRewrite": {
      "^/api": ""
    }
  }
}
var HttpsProxyAgent = require('https-proxy-agent')
var proxyConfig = [{
  context: '/api',
  target: 'http://localhost:3030',
  changeOrigin: true,
  pathRewrite: {
    "^/api": ""
  }
}]

function setupForCorporateProxy(proxyConfig) {
  var proxyServer = process.env.http_proxy || process.env.HTTP_PROXY
  if (proxyServer) {
    var agent = new HttpsProxyAgent(proxyServer);
    console.log('Using corporate proxy server: ' + proxyServer)
    proxyConfig.forEach(function(entry) {
      entry.agent = agent
    })
  }
  return proxyConfig
}

module.exports = setupForCorporateProxy(proxyConfig)

Le proxy détournera tous les appels à l'URL commençant par http://localhost:4200/api vers le serveur disponible à l'adresse http://localhost:3030. Cela garantit également que nous ne rencontrerons aucun problème lié aux CORS (dans le cas où le backend ne serait hébergé en local). Cette configuration concerne uniquement le serveur de développement webpack fourni par le CLI pour exécuter l'application sur votre machine dans un environnement de développement. Ce ne sera pas la configuration utilisée en production.

  1. Installez la dépendance suivante uniquement si vous êtes derrière un proxy d'entreprise
npm install --save-dev https-proxy-agent
  1. Dans le fichier de configuration CLI - angular.json - ajoutez l'option proxyConfig à la target serve :
...
"serve": {
  "builder": "@angular-devkit/build-angular:dev-server",
  ...
  "options": {
    "proxyConfig": "src/proxy.conf.json" // or "src/proxy.conf.js"
  },
  "defaultConfiguration": "development"
},
...




 
 
 



  1. Ajoutez le HttpClientModule au tableau imports de AppModule. Si VSCode ne parvient pas à trouver l'importation, ajoutez la ligne suivante manuellement en haut du fichier app.module.ts :
import { HttpClientModule } from '@angular/common/http'
  1. Créez les interfaces/classes pour les modèles utilisés par le backend, ajoutez un fichier par modèle dans le dossier models/authentication :
export class RegistrationRequest {
  constructor(
    public email: string,
    public password: string,
    public firstname: string,
    public lastname: string
  ) {}
}
export class LoginRequest {
  constructor(
    public email: string,
    public password: string
  ) {}
}
import { User } from './user'

export class UserResponse {
  constructor(
    public user: User,
    public token: string
  ) {}
}
/* eslint-disable @typescript-eslint/naming-convention */
export class User {
  constructor(
    public id: number,
    public firstname: string,
    public lastname: string,
    public email: string,
    public created_at: string,
    public update_at: string
  ) {}
}

Prenez note du token dans la UserResponse, il servira à authentifier l'utilisateur via l'entête Authorization : Authorization: Bearer <token>. Apprenez-en plus sur les JWT iciopen in new window.

  1. Implémentez les méthodes register et login dans AuthenticationService comme suit :
private baseUrl = 'api/user'

constructor(private httpClient: HttpClient) {}

login(loginRequest: LoginRequest): Observable<UserResponse> {
  return this.httpClient.post<UserResponse>(`${this.baseUrl}/login`, loginRequest)
}

register(loginRequest: LoginRequest): Observable<UserResponse> {
  const registrationRequest = new RegistrationRequest(
    loginRequest.email,
    loginRequest.password,
    'John',
    'Smith'
  )

  return this.httpClient.post<UserResponse>(`${this.baseUrl}/register`, registrationRequest)
}
  1. La modification de la signature d'appel de la méthode login va nécessiter un peu de refactorisation dans le LoginFormComponent :
constructor(
  private router: Router,
  private route: ActivatedRoute,
  private authenticationService: AuthenticationService
) {}

login(): void {
  this.authenticationService.login(this.loginRequest)
    .subscribe({ next: () => {
      const returnUrl = this.route.snapshot.queryParamMap.get('returnUrl')
      this.router.navigateByUrl(returnUrl ? `/${returnUrl}` : '')
    } })
}

register(): void {
  this.authenticationService.register(this.loginRequest)
    .subscribe()
}

get loginRequest(): LoginRequest {
  return new LoginRequest(this.email, this.password)
}
  1. Une refactorisation est également nécessaire pour que AuthenticationGuard continue de fonctionner. Faites en sorte que le booléen loggedIn dans AuthenticationService dépende d'un champ token et faites en sorte que le LoginFormComponent sauvegarde le token qu'il obtient de l'appel de connexion dans ce champ.
token: string | null = null

get loggedIn(): boolean {
  return this.token != null
}
login(): void {
  this.authenticationService.login(this.loginRequest)
    .subscribe(response => {
      this.authenticationService.token = response.token
      const returnUrl = this.route.snapshot.paramMap.get('returnUrl')
      this.router.navigateByUrl(returnUrl ? `/${returnUrl}` : '')
    })
}



 




  1. Ajoutez un bouton d'enregistrement à côté du bouton de connexion dans le LoginFormComponent, donnez-lui l'attribut type="button" afin qu'Angular sache que ce n'est pas ce bouton qui déclenche l'événement ngSubmit sur le formulaire et faites-lui appeler le méthode d'enregistrement. Vous devriez maintenant pouvoir enregistrer un utilisateur et vous connecter.
<div class="button-container">
  <button type="button">Register</button>
  <button type="submit">Login</button>
</div>
.button-container {
  display: flex;
  justify-content: space-between;
}
  1. Il est temps de gérer les erreurs. La méthode subscribe peut prendre un objet qui propose trois callbacks: une next, une error et une complete (nous verrons cela plus en détail dans le chapitre suivant). Déclarer un champ errorMessage sur le LoginFormComponent et le mettre à jour en vous servant de l'argument renvoyé par la callback error. Afficher le message d'erreur sur le formulaire. Vérifier que le message d'erreur s'affiche bien lorsqu'on saisit des identifiants incorrects.
private errorHandler(errorResponse: HttpErrorResponse): void {
  this.errorMessage = errorResponse.error.error ?? `${errorResponse.error.status} - ${errorResponse.error.statusText}`
}

// subscribe syntax
this.authenticationService.login(this.loginRequest)
  .subscribe({
    next: (userResponse) => { /*  */},
    error: (errorResponse) => { /*  */ }
  })

hint

Pour une meilleure UX (User eXperience), penser à vider le champ errorMessage soit avant de lancer une nouvelle requête d'authentification ou d'enregistrement, soit dès que celles-ci se terminent en succès.

  1. Appelons maintenant le backend pour obtenir la liste des films. La route est sécurisée, ce qui signifie que le passage du token dans l'en-tête est nécessaire. Angular fournit un mécanisme - les intercepteurs http - pour intercepter systématiquement les requêtes http, permettant de définir les en-têtes en un seul endroit.

a. Utilisez le CLI pour en générer un : ng generate interceptor interceptors/authentication.

b. Voici son implémentation :

import { Injectable } from '@angular/core'
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'
import { Observable } from 'rxjs'
import { AuthenticationService } from '@services/authentication.service'

@Injectable()
export class AuthenticationInterceptor implements HttpInterceptor {

  constructor(private authenticationService: AuthenticationService) {}

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    const token = this.authenticationService.token
    if (token) {
      const cloned = request.clone({
        headers: request.headers.set('Authorization', `Bearer ${this.authenticationService.token}`)
      })

      return next.handle(cloned)
    }

    return next.handle(request)
  }
}

S'il y a un token dans l'AuthenticationService, l'intercepteur l'ajoutera aux en-têtes de la requête http.

c. Ajouter l'intercepteur aux providers de l'AppModule

providers: [
  { provide: HTTP_INTERCEPTORS, useClass: AuthenticationInterceptor, multi: true }
],
  1. Créez un FilmService à l'aide du CLI et implémentez l'appel au endpoint api/movies/search. Notez que le queryParam title n'est pas facultatif. Pour ajouter des query params à une requête, utilisez le paramètre options de la méthode get.
const options = {
  params: new HttpParams().set('title', title)
}
  1. Apportez des modifications au FilmSearchComponent pour appeler ce nouveau service avec le titre renseigné par l'utilisateur, enregistrez la réponse dans le champ films du FilmSearchComponent.

  2. Vérifiez que le token est envoyé sous forme d'en-tête HTTP via les outils de développement de votre navigateur.

  3. Bonus : Modifiez la méthode de déconnexion AuthenticationService pour qu'elle passe le token à null.

Résultat attendu

Résultat visuel du TP http

Résultat visuel du TP http