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
- Générez un
AuthenticationService
avec le CLI dans le dossierapp/services
- Déplacez la logique du
loggedIn
de l'AppComponent
vers le service - Injectez le service dans le
LoginFormComponent
et utilisez-le. - 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 leLoginFormComponent
. Voici l'html et le css :
<button class="logout">Logout</button>
<router-outlet></router-outlet>
.logout {
align-self: end;
}
- Afficher conditionnellement le bouton Logout en fonction du statut
loggedIn
de l'utilisateur - 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 unUrlTree
via la méthodecreateUrlTree
du serviceRouter
). Pour prendre en considération des cas d'usage futur, ajoutez un returnUrl en tant que queryParam auUrlTree
renvoyé afin que leLoginFormComponent
sache où revenir après l'authentification et modifiez leLoginFormComponent
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
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 ici
TIP
Le contrat d'interface backend est disponible ici : api-docs
- Ajoutez au dossier
src
soit le fichierproxy.conf.json
si vous n'êtes pas derrière un proxy d'entreprise, soit le fichierproxy.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.
- Installez la dépendance suivante uniquement si vous êtes derrière un proxy d'entreprise
npm install --save-dev https-proxy-agent
- Dans le fichier de configuration CLI -
angular.json
- ajoutez l'optionproxyConfig
à la target serve :
...
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
...
"options": {
"proxyConfig": "src/proxy.conf.json" // or "src/proxy.conf.js"
},
"defaultConfiguration": "development"
},
...
- Ajoutez le
HttpClientModule
au tableauimports
deAppModule
. Si VSCode ne parvient pas à trouver l'importation, ajoutez la ligne suivante manuellement en haut du fichierapp.module.ts
:
import { HttpClientModule } from '@angular/common/http'
- 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 ici.
- Implémentez les méthodes
register
etlogin
dansAuthenticationService
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)
}
- La modification de la signature d'appel de la méthode
login
va nécessiter un peu de refactorisation dans leLoginFormComponent
:
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)
}
- Une refactorisation est également nécessaire pour que
AuthenticationGuard
continue de fonctionner. Faites en sorte que le booléenloggedIn
dansAuthenticationService
dépende d'un champtoken
et faites en sorte que leLoginFormComponent
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}` : '')
})
}
- Ajoutez un bouton d'enregistrement à côté du bouton de connexion dans le
LoginFormComponent
, donnez-lui l'attributtype="button"
afin qu'Angular sache que ce n'est pas ce bouton qui déclenche l'événementngSubmit
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;
}
- 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 leLoginFormComponent
et le mettre à jour en vous servant de l'argument renvoyé par la callbackerror
. 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.
- 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 }
],
- Créez un
FilmService
à l'aide du CLI et implémentez l'appel au endpointapi/movies/search
. Notez que le queryParamtitle
n'est pas facultatif. Pour ajouter des query params à une requête, utilisez le paramètreoptions
de la méthode get.
const options = {
params: new HttpParams().set('title', title)
}
Apportez des modifications au
FilmSearchComponent
pour appeler ce nouveau service avec le titre renseigné par l'utilisateur, enregistrez la réponse dans le champfilms
duFilmSearchComponent
.Vérifiez que le token est envoyé sous forme d'en-tête HTTP via les outils de développement de votre navigateur.
Bonus : Modifiez la méthode de déconnexion
AuthenticationService
pour qu'elle passe le token ànull
.