15 min to read
La puissance des providers en Angular
Dans cet article, nous allons passer en revue des solutions concrètes qui utilisent les providers et voir ensemble le pourquoi et le comment.
Nous avons déjà pu voir un cas concret d’utilisation interne de poviders de Angular dans l’article parlant du fonctionnement de HttpClient.
Si vous souhaitez d’abord voir la liste des cas vus dans cet article, vous pouvez vous rendre sur la table des matières. Mais je vous invite à lire tout l’article, car chaque concept met en avant une façon différente d’utiliser les providers.
Explications préalables
Dans cet article vous allez voir les deux façons de créer un provider:
- En utilisant l’attribut
@Injectable()
: Le provider sera créé automatiquement, - En créant un objet depuis le type Provider.
{ provide: ..., ... }
Configuration de modules
Commençons par voir comment il est possible de rendre un module (souvent une librairie) configurable, et nous allons prendre comme exemple la librairie ng-openapi-gen qui permet de générer des services http ainsi que ces modèles (DTO) depuis un contract openapi (anciennement swagger).
Ce que nous utilisons généralement est appelé forRoot pattern qui a pour but de créer un singleton (instance unique) de providers pour toutes les utilisations de ce module.
La fonction forRoot
est “simplement” une fonction statique qui se trouve dans le module et doit obligatoirement retourner un ModuleWithProviders
. Attention que cette fonction doit obligatoirement être pure.
@Injectable({
providedIn: 'root'
})
export class MonModuleConfiguration {
rootUrl: string = 'http://google.be';
}
// -----------------
@NgModule({
providers: [
MonModuleConfiguration
]
})
export class MonModule {
static forRoot(params: Partial<MonModuleConfiguration>): ModuleWithProviders<MonModule> {
return {
ngModule: MonModule,
providers: [
{
provide: MonModuleConfiguration,
useValue: params
}
]
}
}
}
// -----------------
@NgModule({
imports: [
SubModule.forRoot({ rootUrl: "https://wetry.tech" })
]
})
export class AppModule {}
Ce simple bout de code illustre déjà beaucoup de choses:
-
La notion de
providedIn
. Il s’agit du “scope de disponibilité de l’instance” (en d’autres termes, il donne à Angular une indication sur le choix de l’Injector où placer cette instance). Sa valeur peut être un nom de module ou"root"
. Cette dernière valeur permettant de rendre disponible l’instance dans l’application entière.Malheureusement dans cet exemple il n’a pas beaucoup de sens, car nous créons nous-mêmes le provider. Plus d’infos ici
- Il est possible d’écraser un provider. Vous pouvez remarquer que dans cette exemple, il y a deux providers pour
MonModuleConfiguration
. Un créé depuis@NgModule
qui représente la valeur de config par défaut et l’autre depuisparams
. Cela permet à l’utilisateur du module de pouvoir l’importer dans l’AppModule en appelantMonModule
(prenant la config par défaut) ouMonModule.forRoot({...})
imposant sa propre configuration. - L’utilisation de
useValue
. Comme son nom l’indique, cela permet de forcer une valeur pour un provider. ModuleWithProviders
contient le module ainsi que des providers à y ajouter ou remplacer.
Attention que comme son nom l’indique, il ne faut utiliser le .forRoot()
que dans module le plus “haut” où sera utilisé notre module. AppModule
est le module le plus haut de l’application donc le mettre là permet d’évite tout problème.
Logger
Dans cet exemple, nous allons traiter la gestion de loggers multiples.
Pour ce faire nous allons avoir besoin de deux niveaux:
- Un logger global qui pourra être utilisé en tant que service,
- Un ensemble de sous loggers qui vont réellement logger en étant utilisés par le “logger global”.
Les “sous loggers”
Pour être certain que tous les providers utilisés possèdent les bonnes méthodes, j’utilise une classe abstraite (vous pouvez également utiliser une interface).
export const LOGGERS = new InjectionToken<BaseLogger>("LOGGERS");
@Injectable()
export abstract class BaseLogger {
public log(message: string): void {
this.processLog(LogLevelEnum.Info, message);
}
protected abstract processLog(level: LogLevelEnum, message: string): void;
}
- Vous pouvez remarquer l’attribut
@Injectable()
malgré qu’il s’agisse d’une classe abstraite. En effet, cela est obligatoire depuis Angular 9 (Ivy). Cela ne veut cependant pas dire que nous allons pouvoir l’injecter ! - Vous pouvez également remarquer l’utilisation d’un
InjectionToken
. Cela permettra un accès à des providers typé (ici BaseLogger) à l’aide de l’attribut@Inject()
. Nous allons voir son utilisation dans les parties de codes qui vont suivre.
L’implémentation d’une classe qui hérite de celle-ci n’a rien de spécial, nous n’allons donc pas nous attarder dessus, mais vous pourrez tout de même voir des exemples via le lien en fin de chapitre.
Voyons maintenant comment rendre des loggers disponibles.
Pour ce faire, nous allons utiliser une fonctionnalité également utilisée par APP_INITIALIZER
: multi: true
.
providers: [
{
provide: LOGGERS,
useClass: ConsoleLogger,
multi: true
},
{
provide: LOGGERS,
useClass: MyLogger,
multi: true
}
]
Remarquez également l’utilisation de useClass
qui nous permet cette fois de donner un type à créer. Cette méthode permet l’utilisation de l’injection de dépendance dans les classes fournies alors que useValue
est utilisé pour donner des données fixes.
Le “logger global”
Ce provider exploite ceux précédemment créés et est le seul qui sera utilisé à travers l’application.
@Injectable()
export class Logger {
constructor(@Inject(LOGGERS) private loggers: BaseLogger[]) {}
public log(message: string): void {
this.loggers.forEach(logger => logger.log(message));
}
}
// -----------------
providers: [
Logger
]
Avec l’option multi: true
nous pouvons voir que l’injection via @Inject(LOGGERS)
ne nous fournit pas un seul provider, mais une liste BaseLogger[]
.
Nous pouvons alors l’exploiter à l’aide d’un simple forEach
.
Vous pouvez maintenant vous demander pourquoi ne pas faire un seul service qui fait tout ? Le fait de les séparer en providers vous permet non seulement de pouvoir en ajouter un sans touché aux loggers déjà fonctionnels mais aussi de pouvoir en activer/désactiver facilement.
Dans cet exemple , j’ai poussé les choses encore un peu plus loin en fournissant la liste des loggers dans le forRoot.
LoggerModule.forRoot({
loggers: [
ConsoleLogger,
MyLogger
]
})
Multiplateforme
Votre application doit fonctionner sur Mobile et en version Web ? Voilà encore un excellent exemple d’utilisation des providers.
Il est en effet possible de conditionner le provider à utiliser au moment du build en utilisant le mécanisme d’environnement/configuration d’Angular.
Prenons par exemple l’ouverture d’un explorateur de fichier :
providers: [
{
provide: FileExplorerService,
useClass: environment.isMobile ? FileExplorerMobileService : FileExplorerWebService
}
]
Cet exemple démontre aussi qu’il n’est pas toujours nécessaire de passer par un token d’injection. En effet ici FileExplorerService
étant une classe abstraite (cela ne fonctionne pas avec des interfaces) et n’utilisant pas l’option , nous pouvons l’utiliser comme un token.multiple: true
Mocks
Pour cette partie, nous allons nous concentrer à démontrer que ces principes sont utilisés en interne à Angular.
Nous allons utilisé le token d’injection HTTP_INTERCEPTORS
qui nécessite aussi multi: true
. Il y a un air de déjà vu avec ce que nous avons mis en place pour le logger n’est-ce pas ?
@Injectable()
export class MockHttpInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const url = request.url;
const method = request.method;
return myMock(url, method, request) || next.handle(request);
}
}
export const mockInterceptorProvider = {
provide: HTTP_INTERCEPTORS,
useClass: MockHttpInterceptor,
multi: true
};
// -----------------
export function myMock(
url: string, method: string,
request: HttpRequest<any>
): Observable<HttpEvent<any>> | false {
let result: Observable<HttpEvent<any>> | false = false;
if ((environment.mock.all || environment.mock.services.getMy)
&& url.includes('api/my') && method === 'GET') {
result = of(
new HttpResponse({
status: 200,
body: {
...
}
})
);
}
return result;
}
La différence entre HttpInterceptor
et notre logger est que les méthodes intercept
s’exécutent en cascade.
Voici comment Angular crée cette cascade plutôt que de faire un simple forEach:
const interceptors = this.injector.get(HTTP_INTERCEPTORS, []);
this.chain = interceptors.reduceRight(
(next, interceptor) => new HttpInterceptorHandler(next, interceptor),
this.backend
);
// -----------------
export class HttpInterceptorHandler implements HttpHandler {
constructor(private next: HttpHandler, private interceptor: HttpInterceptor) {}
handle(req: HttpRequest<any>): Observable<HttpEvent<any>> {
return this.interceptor.intercept(req, this.next);
}
}
Si vous ne connaissez pas la méthode reduceRight
voici une illustration de ce qu’elle génère:
Enfin, cet exemple nous propose une troisième manière d’injecter un provider:
constructor(private myService: MyService)
le plus utiliséconstructor(private @Inject(MY_SERVICE) myService: MyService)
en cas d’utilisation d’un tokenthis.myService = this.injector.get(MY_SERVICE)
outhis.myService = this.injector.get(MyService)
via une injection “manuelle”
Base(Component/Service)
Il vous est probablement déjà venu à l’esprit de faire une classe de “base” (classe parente) pour des composants ou des services. Cela ne semble pas être une mauvaise idée, cependant je vous conseille fortement de n’injecter qu’une seule chose dans votre parent: Injector
.
Pourquoi ? Pour préparer l’avenir de votre application. Si votre “parent” a besoin un jour de plus d’injections, vous n’aurez pas à refactoriser tous ces enfants !
@Component({})
export abstract class BaseComponent {
protected myService: MyService;
constructor(
injector: Injector
) {
this.myService = injector.get(MyService);
}
}
// -----------------
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent extends BaseComponent {
constructor(
injector: Injector,
private myOtherService: MyOtherService
) {
super(injector);
}
}
Authentification
Nous n’allons pas voir jusqu’au bout comment faire une authentification dans une application Angular mais nous concentrer sur une possibilité de créer une instance de provider (APP_INITIALIZER
) non repris par les autres exemples.
export const authInitializerFactory = (authService: AuthService) => () => authService.initAuthentication();
export const AuthInitializerProvider = {
provide: APP_INITIALIZER,
useFactory: authInitializerFactory,
deps: [ AuthService ],
multi: true
};
Je préfère centraliser toute la logique d’authentification dans un service (AuthService
), mais celui-ci ne peut pas directement être utilisé en tant que APP_INITIALIZER
. Nous utilisons alors useFactory
.
Ce useFactory
est un peu entre useValue
et useClass
. Il permet d’utiliser une valeur tout en pouvant utiliser l’injection de dépendance.
Notez que le tableau deps: [ AuthService ]
permet de récupérer/injecter une instance à fournir à notre factory (authService: AuthService) =>
. L’ordre des paramètres de la factory est l’ordre des éléments du tableau deps
.
Encore quelques petites explications
providedIn
Cette option a été ajoutée pour permettre de faire du lazy-loading de provider tout en faisant du singleton.
Malheureusement elle n’a pas encore été ajoutée partout, et n’est utilisable que via l’attribut @Injectable()
.
Pour démontrer ceci via un exemple, nous allons utiliser la dernière possibilité pour injecter un provider: useExisting
.
@Injectable({
providedIn: 'root'
})
export class MyService {
}
// -----------------
providers: [
MyService,
{
provide: 'TEST',
useExisting: MyService
}
]
En faisant cela, MyService
est bien injecté dans l’Injector
principale, mais pas TEST
! Même si la valeur derrière est la même, d’autres modules ne connaitront pas TEST
alors que tout le monde connaitra MyService
.
PS: Oui il y a bien un string comme valeur de provide
. Cela rend évidement impossible le typage fort de ce provider contrairement à l’utilisation d’un token d'injection
. Je ne l’utilise que quand il est difficile de partager le token entre les modules (qui nécessiterait une librairie ne contenant que ce token par exemple).
Résumé méthodes de création de provider
Tout au long de cet article, nous avons vu les 4 façons de créer un provider manuellement.
useValue
: Fournir une valeur statiqueuseClass
: Fournir un type dont Angular va créer une instanceuseFactory
: Fournir une méthode permettant de créer la valeur finale du provider (compatible DI)useExisting
: Créer un alias vers un provider déjà existant
d'avoir pris le temps de lire cet article
Table des matières
Commentaires