Injection de dépendances dans une application Angular

Cet article fait partie de la série d’articles Angular from Scratch.

L’injection de dépendances est un design pattern pris en charge nativement dans Angular.
Pour davantage d’explications sur la théorie de ce design pattern, voir L’injection de dépendances en théorie.

A l’instanciation d’un composant, Angular peut effectuer la résolution de ces dépendances puis de les injecter en utilisant le constructeur du composant. Suivant la façon dont les dépendances sont déclarées, le framework d’injection de dépendances pourra instancier un nouvel objet ou utiliser une instance existante sous forme de singleton. Les objets injectés peuvent être des classes ou des services.

On va illustrer quelques exemples d’injection de dépendances en utilisant différents mécanismes. On terminera par présenter les services qui sont les éléments le plus souvent injectés.

@jeremybishop

Notion de “provider”

Quand un objet est injecté, il faut pouvoir déterminer si on doit utiliser une nouvelle instance ou une instance déjà existante. Ce mécanisme est effectué par le framework d’injection de dépendances qui va configurer des injecteurs en fonction de la façon dont les objets sont déclarés. Une déclaration importante concerne le provider de dépendances (i.e. fournisseur de dépendances) car il permettra de configurer les injecteurs.

En fonction de l’endroit où le provider sera déclaré, l’injecteur utilisé pourra instancier la dépendance. Il est possible de déclarer des providers dans les metadatas des modules ou des composants:

  • Au niveau d’un composant: avant chaque exécution du constructeur du composant, la dépendance sera résolue et l’injecteur créera une nouvelle instance de l’objet injecté.
    L’injecteur utilisé est de type ElementInjector, une instance de cet injecteur est créé pour chaque composant. Pour injecter une dépendance, si l’injecteur du composant ne réussit pas à la résoudre, il va appeler les injecteurs des composants parents s’ils existent sinon il va appeler l’injecteur du module parent.
  • Au niveau d’un module: si le module est importé, le provider sera aussi importé et l’injecteur utilisera la même instance de l’objet à tous les endroits où il est injecté.
    L’injecteur utilisé est de type ModuleInjector, une instance de cet injecteur est créée pour chaque module. Si l’ElementInjector du composant échoue à résoudre une dépendance, l’injecteur du module auquel appartient le composant sera appelé pour essayer d’instancier la dépendance.
  • Au niveau de l’application: il est possible de préciser un provider directement au niveau de l’élément à injecter en utilisant le paramètre providedIn de la directive @Injectable(). Dans ce paramètre on peut indiquer la valeur 'root' pour désigner le module root. Si le provider est le module root alors la résolution de la dépendance sera effectuée par l’injecteur du module root.

La déclaration d’un provider peut se faire de différentes façons:

  • Paramètre providers d’un composant ou d’un module: c’est la méthode le plus ancienne. L’inconvénient de cette méthode est qu’elle ne permet pas de tirer partie de la fonctionnalité tree-shaking.
    Si le provider est un module, l’instanciation de la classe à injecter est liée à celle du module:

    • Eagerly-loaded module: si le module est chargé au démarrage de l’application, alors la classe à injecter sera instanciée sous forme d’un singleton qui sera disponible dans toute l’application.
    • Lazy-loaded module: si le module est chargé par chargement différé, l’instance de la classe à injecter ne sera disponible que si le module est chargé. Si un élément tente d’utiliser une instance de la classe par injection alors que le module n’est pas chargé il y aura une erreur de ce type dans la console du browser:
      NullInjectorError: No provider for <nom de la classe à injecter>!
      

    Dans le cas d’un composant, l’instance de la classe à injecter sera liée à la durée de vie du composant c’est-à-dire qu’à chaque fois que le constructeur du composant est exécuté une nouvelle instance de la classe est créée.

    Cette méthode permet aux composants et modules de désigner explicitement les objets dont ils auront la charge toutefois il peut préter à confusion si on déclare la classe à injecter dans le paramètre providers de plusieurs éléments.

  • Paramètre providedIn de la directive @Injectable(): on peut indiquer 'root' pour désigner le module root ou directement le module qui sera le provider. Cette méthode est plus récente que la précédente et elle permet de tirer du tree-shaking c’est-à-dire que si l’élément injectable n’est injecté nul part, il ne sera pas inclus dans le bundle obtenu après compilation.
    Cette méthode est la plus simple et convient dans la grand majorité des cas. Elle est simple à mettre en oeuvre car elle permet d’indiquer directement que l’instance de la classe à injecter est valable pour toute l’application dans le cas de l’utilisation de la valeur 'root'.

    L’inconvénient de cette méthode est qu’elle peut rendre plus difficile la réutilisation d’un service si on souhaite utiliser des valeurs différentes de providedIn. En outre cette méthode ne permet de désigner seulement des modules et non des composants en tant que provider (i.e. le paramètre providedIn ne peut être renseigné que par un module et non un composant).

    Dans le cas d’un module chargé par lazy-loading, la classe est instanciable seulement si le module est chargé dans les 2 types de configuration de providedIn: 'root' ou le type du module.

Tree-shaking

Le tree-shaking est une optimisation qui permet de ne pas inclure dans le bundle obtenu après compilation, les éléments qui sont inutiles.
Ainsi, si un élément se trouve dans le paramètre providers d’un composant ou d’un module alors qu’il n’est pas injecté, il sera quand même inclus dans le bundle.
A l’opposé, si le paramètre providedIn du décorateur @Injectable() est utilisé pour une classe à injecter, elle sera présente dans le bundle seulement si elle est effectivement injectée.

Pour résumer:

Type de déclaration Tree-shaking ? Elément fournissant la classe à injecter Type de module Type d’instance de l’objet injecté Commentaire
providers Non @Component() N/A Nouvelle instance à chaque utilisation du composant L’instance de la classe est accessible dans le composant, dans son template et, le cas échéant, dans le composant effectuant le rendu s’il y a un paramètre selector.
@NgModule() root Singleton L’instance de la classe est injectable partout dès le démarrage de l’application.
Eagerly-loaded
Lazy-loaded L’instance de la classe n’est utilisable que si le module est chargé sinon il se produit une erreur.
providedIn Oui 'root' N/A L’instance de la classe est injectable partout dès le démarrage de l’application.
<type du module> Eagerly-loaded
Lazy-loaded L’instance de la classe n’est utilisable que si le module est chargé sinon il se produit une erreur.

Exemples d’implémentation pour indiquer le “provider”

En utilisant le paramètre “providers”

Dans @NgModule()

Si on déclare le provider en utilisant le paramètre providers des metadatas d’un module, la déclaration est:

@Injectable()
export class Dependency {}
 
@NgModule({
  providers: [ Dependency ]
})
export class CustomModule {}

L’instance injectée est un singleton.

Dans @Component()

Si le provider est déclaré avec le paramètre providers des metadatas d’un composant, la déclaration est:

@Injectable()
export class Dependency {}
 
@Component({
  providers: [ Dependency ]
})
export class CustomComponent {}

Une nouvelle instance est injectée à chaque exécution du constructeur.

Options utilisables avec le paramètre “providers”

Le paramètre providers permet d’ajouter des options pour perfectionner l’injection.

Pour les exemples suivants, on utilise les classes:

@Injectable()
class FirstDependency {
  innerId = 'first';
}
 
@Injectable()
class SecondDependency {
  innerId = 'second';
}

Le membre innerId permet d’identifier l’instance pendant le débug.

useClass
L’option useClass permet d’indiquer le type de la classe qui sera injectée dans le but de remplacer le type injecté.

Dans cet exemple, le paramètre secondDependency contiendra une instance de la classe FirstDependency. L’instance sera différente de celle du paramètre firstDependency:

@Component({
  templateUrl: './custom.component.html',
  providers: [
      FirstDependency,
      { provide: SecondDependency, useClass: FirstDependency }
  ]
})
export class CustomComponent {
  constructor(private firstDependency: FirstDependency,
    private secondDependency: SecondDependency) {
      console.log('firstDependency: ' + firstDependency.innerId);
      console.log('secondDependency: ' + secondDependency.innerId);
  }
}

Le type SecondDependency est remplacé par le type FirstDependency.

useExisting
L’option useExisting permet d’utiliser une instance déjà fournie par le provider.

Par exemple, si on configure le paramètre providers de cette façon:

@Component({
  templateUrl: './custom.component.html',
  providers: [
      FirstDependency,
      { provide: SecondDependency, useExisting: FirstDependency }
  ]
})
export class CustomComponent {
  constructor(private firstDependency: FirstDependency,
    private secondDependency: SecondDependency) {
      console.log('firstDependency: ' + firstDependency.innerId);
      console.log('secondDependency: ' + secondDependency.innerId);
  }
}

Les 2 paramètres firstDependency et secondDependency contiennent la même instance de type FirstDependency.

useValue
L’option useValue permet d’utiliser explicitement une instance. Si on configure le paramètre providers de cette façon:

@Injectable()
class SecondDependency {
  constructor(public innerId: string) {}
}

const dependency = new SecondDependency('other');

@Component({
  templateUrl: './custom.component.html',
  providers: [
      FirstDependency,
      { provide: SecondDependency, useValue: dependency }
  ]
})
export class CustomComponent {
  constructor(private firstDependency: FirstDependency,
    private secondDependency: SecondDependency) {
      console.log('firstDependency: ' + firstDependency.innerId);
      console.log('secondDependency: ' + secondDependency.innerId);
  }
}

Le paramètre SecondDependency contiendra l’instance créée au préalable.

useFactory

L’option useFactory permet d’utiliser une fonction factory pour instancier un objet à injecter dans le cas où l’instanciation nécessite l’implémentation d’une logique. La fonction factory peut comporter des arguments qui seront injectés.

Par exemple, si on redéfinit la classe SecondDependency de cette façon:

@Injectable()
class SecondDependency {
  innerId: string;
 
  constructor(firstDependency: FirstDependency) {
    this.innerId = firstDependency.innerId;
  }
}

Si on implémente la fonction factory de cette façon:

let dependencyFactory = (firstDependency: FirstDependency) => {
  return new SecondDependency(firstDependency);
};

Il est possible de configurer le provider pour que la fonction factory crée l’instance de l’objet SecondDependency à injecter:

@Component({
  templateUrl: './comp-second-mod.component.html',
  providers: [
    FirstDependency,   
    { provide: SecondDependency, useFactory: dependencyFactory, deps: [ FirstDependency ] }
  ]
})
export class CustomComponent {
  constructor(private firstService: FirstServiceService,
    private firstDependency: FirstDependency,
    private secondDependency: SecondDependency) {
      console.log('firstDependency content: ' + firstDependency.innerId);
      console.log('secondDependency content: ' + secondDependency.innerId);
  }
}

Ainsi, la fonction factory dependencyFactory crée une instance de SecondDependency et l’injecte dans le composant CustomComponent.

En utilisant le paramètre “providedIn” dans @Injectable()

Le décorateur @Injectable() est utilisé pour indiquer qu’un objet est injectable en utilisant l’injection de dépendances. On précise son utilisation plus bas. Ce décorateur permet d’indiquer directement le provider d’un objet à injecter en utilisant le paramètre providedIn.

Pour déclarer en utilisant le paramètre providedIn, la déclaration est:

@Injectable({
  providedIn: 'root'
})
class Dependency {}

Dans ce cas, le provider sera le module root et l’instance injectée est un singleton.

Pour indiquer un module en tant que provider, on indique le type du module, la déclaration est du type:

import { ItemModule } from '../item.module.ts';
 
@Injectable({
  providedIn: ItemModule
})
class Dependency {}

Exemples d’implémentation de l’injection

Pour implémenter l’injection, après avoir précisé le provider, il faut indiquer qu’un objet est injectable en utilisant les décorateurs:

  • @Inject() utilisable sur les paramètres du constructeur d’un composant ou
  • @Injectable() utilisable sur la classe de l’objet à injecter.

En utilisant @Inject()

Le décorateur @Inject() est plus rarement utilisé, il permet d’indiquer que l’élément est injectable. Il se déclare dans les paramètres du constructeur du composant où l’élément doit être injecté. Si on utilise @Inject(), il n’y a pas d’autres décorateurs à préciser sur la classe de l’objet à injecter.

Par exemple:

class FirstDependency {}
 
@Component({
  providers: [ FirstDependency ]
})
export class CustomComponent {
  constructor(@Inject(FirstDependency) private firstDependency) {}
}

En utilisant @Injectable()

La décorateur @Injectable() peut être utilisé directement sur la classe de l’objet à injecter. C’est le décorateur utilisé le plus couramment. Si on utilise @Injectable(), il n’est pas nécessaire d’utiliser @Inject.

Par exemple:

@Injectable()
class FirstDependency {}
 
@Component({
  providers: [ FirstDependency ]
})
export class CustomComponent {
  constructor(private firstDependency: FirstDependency) {}
}

@Injectable() permet de préciser directement le provider avec le paramètre providedIn.

Injecter des services

Les composants sont très liés à leur template, par exemple ils permettent de mettre à disposition des données à afficher. L’existence de l’instance du composant ne survit pas au passage d’un composant à l’autre. il est donc nécessaire de prévoir un type d’élément permettant de stocker des données dont l’existence va survivre aux changements de vue. Les éléments utilisés pour être partagés entre les différents composants sont les services, ils permettent:

  • d’effectuer des traitements communs à plusieurs composants,
  • de mettre à disposition des propriétés utilisables par plusieurs composants.

D’un point de vue implémentation, un service est une classe injectable, il n’y a pas de différence entre un service et une simple classe à injecter. Ainsi, tous les éléments indiqués dans le paragraphe précédent sont valable pour les services.

Par crééer un service avec l’CLI Angular, il faut exécuter dans le répertoire de l’application ou dans le répertoire d’un module, l’instruction suivante:

ng generate service <nom du service>

ou

ng g s <nom du service>

Le fichier du service sera généré avec le décorateur suivant:

@Injectable({
  providedIn: 'root'
})

Ce paramétrage permet de configurer le service pour que le provider soit le module root. L’instance du service est un singleton injectable dans toute l’application.

Pour résumer…

Pour injecter un service, il faut:

  • Quel sera le provider de ce service: le module root, module importé ou un composant. Suivant le choix du provider, une nouvelle instance du service ou un singleton sera utilisé à chaque injection.
  • Suivant le provider choisi, il faut choisir la syntaxe à utiliser:
    • Décorateur @Injectable() avec le paramètre providedIn.
    • Paramètre providers dans le décorateur @Component() ou @NgModule() + décorateur @Inject().
    • Paramètre providers dans le décorateur @Component() ou @NgModule() + décorateur @Injectable().

Les syntaxes peuvent être synthétiser dans ce tableau:

Syntaxe Commentaires
Décorateur @Injectable() avec le paramètre providedIn
@Injectable({
    providedIn: 'root'
})
export class InjectedService {}

@Component({
    ...
})
export class ServiceConsumerComponent() {
    constructor(service: InjectedService) {}
}
Cette syntaxe est la plus triviale.
La valeur du paramètre providedIn dans @Injectable() peut être:

  • 'root' pour indiquer le module root, dans ce cas le service sera un singleton.
  • utilisé comme provider, le service sera un singleton utilisable si le service est chargé.
Paramètre providers dans le décorateur @Component() + @Inject()
export class InjectedService {}

@Component({
  providers: [ InjectedService ],
  ...
})
export class ServiceConsumerComponent() {
  constructor(
     @Inject(InjectedService) private service) 
  {}
}
Une nouvelle instance du service sera instanciée à chaque injection.
Paramètre providers dans le décorateur @NgModule() + Inject()
export class InjectedService {}

@NgModule({
  providers: [ InjectedService ],
  ...
})
export class CustomModule() {}

@Component({
  ...
})
export class ServiceConsumerComponent() {
  constructor(
    @Inject(InjectedService) private service) 
  {}
}
Le service injecté est un singleton utilisable si le module est chargé.
Paramètre providers dans le décorateur @NgModule() + le décorateur @Injectable()
@Injectable()
export class InjectedService {}

@NgModule({
  providers: [ InjectedService ],
  ...
})
export class CustomModule() {}

@Component({
  ...
})
export class ServiceConsumerComponent() {
  constructor(service: InjectedService) {}
}
Le service injecté est un singleton utilisable si le module est chargé.

La syntaxe avec le paramètre providers dans le décorateur @NgModule() et @Component() autorise des options:

@Component({
  providers: [
    ...
    { provide: InjectedService, <option>  }
  ]
})
export class ServiceConsumerComponent(
  constructor(service: InjectedService) {}
)

Les options peuvent être:

  • { provide: InjectedService, useClass: OtherTypeService } pour injecter une instance du type OtherTypeService quand le type demandé est InjectedService.
  • { provide: InjectedService, useExisting: OtherTypeService } pour injecter une instance existante de type OtherTypeService quand le type demandé est InjectedService.
  • { provide: InjectedService, useValue: new InjectedService() } pour injecter une instance particulière quand le type demandé est InjectedService.
  • { provide: InjectedService, useFactory: () => { return new InjectedService(); } } pour utiliser une logique particulière implémentée dans une factory.

Leave a Reply