Injection de dépendance > Injection de dépendance en action

Références

L'actualité

Librairie

L'information


Injection de dépendance en action

Cette section explore de nombreuses caractéristiques de l'injection de dépendance dans Angular.

Dépendances de service imbriquées

Le consommateur d'un service injecté n'a pas besoin de savoir comment créer ce service. C'est le travail du framework DI de créer et de mettre en cache des dépendances. Le consommateur doit simplement informer le framework DI de la dépendance dont il a besoin.

Parfois, un service dépend d'autres services, qui peuvent dépendre encore d'autres services. L'infrastructure d'injection de dépendance résout ces dépendances imbriquées dans le bon ordre. À chaque étape, le consommateur de dépendances déclare ce dont il a besoin dans son constructeur et laisse le cadre les fournir.

L'exemple suivant montre que AppComponent déclare sa dépendance à LoggerService et UserContext.

UserContext à son tour, dépend à la fois LoggerService et UserService, un autre service qui rassemble des informations sur un utilisateur particulier.

Lorsque Angular crée AppComponent, le cadre DI crée une instance de LoggerService et commence à créer UserContextService. UserContextService à besoin LoggerService, que le cadre a déjà été, afin de pouvoir fournir la même instance. UserContextService à également besoin de UserService, que le cadre doit encore créer. UserService n'a pas de ces dépendances, le framework peut simplement utiliser new pour instancier la classe et fournir l'instance au constructeur de UserContextService.

Le parent AppComponent n'a pas besoin de connaître les dépendances des dépendances. Déclarez ce qui est nécessaire dans le constructeur (dans ce cas LoggerService et UserContextService) et le cadre résout les dépendances imbriquées.

Lorsque toutes les relations sont en place, AppComponent affiche les informations de l'utilisateur.




Limiter l'étendue du service à un sous arbre de composant

Une application Angular comporte plusieurs injecteurs, disposés dans une hiérarchie arborescente parallèle à l'arborescence des composants. Chaque injecteur crée une instance singleton d'une dépendance. Cette même instance est injectée partout où l'injecteur fournit ce service. Un service particulier peut être fourni et créé à n'importe quel niveau de la hiérarchie des injecteurs, ce qui signifie qu'il peut y avoir plusieurs instances d'un service s'il est fourni par plusieurs injecteurs.

Les dépendances fournies par l'injecteur racine peuvent être injectées dans n'importe quel composant n'importe où dans l'application. Dans certains cas, vous pouvez souhaiter limiter la disponibilité du service à une région particulière de l'application. Par exemple, vous pouvez laisser les utilisateurs choisir explicitement d'utiliser un service plutôt que de laisser l'injecteur racine le fournir automatiquement.

Vous pouvez limiter la portée d'un service injecté à une branche de la hiérarchie des applications en fournissant ce service au niveau du composant sous-racine de cette branche. Cet exemple montre comment rendre disponible une instance différente de HeroService en HeroesBaseComponent en l'ajoutant au providers du décorateur @Component() du sous-composant.


Lorsque Angular crée HeroesBaseComponent, il crée également une nouvelle instance HeroService visible uniquement pour ce composant et ses enfants, le cas échéant.

Vous pouvez également fournir à HeroService un composant différent ailleurs dans l'application. Cela se traduirait par une instance différente du service, vivant dans un injecteur différent.

Plusieurs instances de service (sandbox)

Parfois, vous souhaitez plusieurs instances d'un service au même niveau de la hiérarchie des composants.

Un bon exemple est un service qui conserve l'état de son instance de composant associé. Vous avez besoin d'une instance distincte du service pour chaque composant. Chaque service a son propre état de travail, isolé du service et de l'état d'un composant différent. Cela s'appelle le "sandboxing" parce que chaque instance de service et de composant a son propre "sandbox" dans lequel jouer.

Dans cet exemple, HeroBiosComponent présente trois instances de HeroBioComponent.


Chaque HeroBioComponent peut éditer la biographie d'un seul héros. HeroBioComponent s'appuie sur HeroCacheService pour extraire, mettre en cache et effectuer d'autres opérations de persistance sur ce héros.


Trois instances de HeroBioComponent ne peuvent pas partager la même instance de HeroCacheService, car elles se feraient concurrence pour déterminer le héros à mettre en cache.

Au lieu de cela, chacun HeroBioComponent obtient sa propre HeroCacheService instance en listant HeroCacheService dans son tableau providers de métadonnées.


Le parent HeroBiosComponent lie une valeur à heroId. ngOnInit transmet cet identifiant au service, qui récupère et met en cache le héros. Le getter de la propriété hero extrait le héros mis en cache du service. Le modèle affiche cette propriété liée aux données.




Qualifiez la recherche de dépendance avec le décorateurs de paramètres

Lorsqu'une classe nécessite une dépendance, cette dépendance est ajoutée au constructeur en tant que paramètre. Quand Angular doit instancier la classe, il fait appel à la structure DI pour fournir la dépendance. Par défaut, l'infrastructure DI recherche un fournisseur dans la hiérarchie des injecteurs, en commençant par l'injecteur local du composant du composant et, si nécessaire, en passant par l'arborescence de l'injecteur jusqu'à ce qu'il atteigne l'injecteur racine.

  • Le premier injecteur configuré avec un fournisseur fournit la dépendance (une instance de service ou une valeur) au constructeur.
  • Si aucun fournisseur n'est trouvé dans l'injecteur racine, la structure DI renvoie null au constructeur.
Il existe un certain nombre d'options pour modifier le comportement de recherche par défaut, en utilisant des décorateurs de paramètres sur les paramètres de valeur de service d'un constructeur de classe.

Faire une dépendance avec @Optional et limiter la recherche avec @Host

Les dépendances peuvent être enregistrées à n'importe quel niveau de la hiérarchie des composants. Lorsqu'un composant demande une dépendance, Angular commence par l'injecteur de ce composant et examine l'arborescence de l'injecteur jusqu'à ce qu'il trouve le premier fournisseur approprié. Angular génère une erreur s'il ne parvient pas à trouver la dépendance au cours de cette promenade.

Dans certains cas, vous devez limiter la recherche ou prendre en charge une dépendance manquante. Vous pouvez modifier le comportement de recherche d'Angular avec les décorateurs @Host et les décorateurs qualifiés @Optional sur un paramètre de valeur de service du constructeur du composant.

  • Le décorateur @Optional de la propriété dit à Angular de renvoyer null lorsqu'il ne peut pas trouver la dépendance.
  • Le décorateur @Host de propriété arrête la recherche ascendante sur le composant hôte. Le composant hôte est généralement le composant demandant la dépendance. Toutefois, lorsque ce composant est projeté dans un composant parent , ce composant parent devient l'hôte. L'exemple suivant couvre ce second cas.

Focus sur le modèle :

Maintenant, il y a un nouvel élément ‹hero-contact› entre les balises ‹hero-bio›. Angular "projets" ou "transcludes", correspondant HeroContactComponent à la vue HeroBioComponent, en le plaçant dans ‹ng-content› de la matrice HeroBioComponent.


Le résultat est présenté ci-dessous, avec le numéro de téléphone HeroContactComponent du héros figurant au-dessus de la description du héros.



Voici HeroContactComponent qui illustre les décorateurs qualifiés.

Concentrez-vous sur les paramètres du constructeur

La fonction @Host() décorant la propriété heroCache du constructeur garantit que vous obtenez une référence au service de cache à partir du parent HeroBioComponent. Angular génère une erreur si le parent ne dispose pas de ce service, même si un composant situé plus haut dans l'arborescence des composants l'inclut.

Une deuxième fonction @Host() décore la propriété du constructeur loggerService. La seule instance LoggerService de l'application est fournie au niveau AppComponent. L'hôte HeroBioComponent n'a pas son propre fournisseur LoggerService.

Angular génère une erreur si vous n'avez pas également décoré la propriété @Optional(). Lorsque la propriété est marquée comme facultative, Angular définit la valeur loggerService sur null et le reste du composant s'adapte.

Voici HeroBiosAndContactsComponent.



Si vous commentez le décorateur @Host(), Angular monte l'arbre des ancêtres injecteurs jusqu'à ce qu'il trouve l'enregistreur au niveau AppComponent. La logique de l'enregistreur entre en jeu et l'affichage du héros se met à jour avec le "!!!" marqueur pour indiquer que l'enregistreur a été trouvé.




Fournir un fournisseur personnalisé avec @Inject

L'utilisation d'un fournisseur personnalisé vous permet de fournir une implémentation concrète pour les dépendances implicites, telles que les API de navigateur intégrées. L'exemple suivant utilise un InjectionToken pour fournir l'API du navigateur localStorage en tant que dépendance dans le fichier BrowserStorageService.

La fonction factory retourne la propriété localStorage attachée à l'objet de la fenêtre du navigateur. Le décorateur Inject est un paramètre de constructeur utilisé pour spécifier un fournisseur personnalisé d'une dépendance. Ce fournisseur personnalisé peut désormais être remplacé pendant les tests avec une API fictive ou local Storage non interactive avec des API de navigateur réelles.

Modifier la recherche de fournisseur avec @Selfet / @SkipSelf

Les fournisseurs peuvent également être ciblés par injecteur via les décorateurs de paramètres de constructeur. L'exemple suivant substitue le jeton BROWSER_STORAGE dans les fournisseurs de classe Component avec l'API de navigateur sessionStorage. Le même BrowserStorageService est injecté deux fois dans le constructeur, décoré avec @Self et @SkipSelf pour définir quel injecteur gère la dépendance du fournisseur.

À l'aide du décorateur @Self, l'injecteur ne regarde que l'injecteur du composant pour ses fournisseurs. Le décorateur @SkipSelf vous permet de ignorer l'injecteur local et de rechercher dans la hiérarchie un fournisseur qui satisfait cette dépendance. L'instance sessionStorageService interagit avec BrowserStorageService à l'aide de l'API de navigateur sessionStorage, tandis que localStorageService ignore l'injecteur local et utilise la racine BrowserStorageService qui utilise l'API de navigation localStorage.

Injecter l'élément DOM du composant

Bien que les développeurs s'efforcent de l'éviter, de nombreux effets visuels et outils tiers, tels que jQuery, nécessitent un accès DOM. Par conséquent, vous devrez peut-être accéder à l'élément DOM d'un composant.

Pour illustrer ceci, voici une version simplifiée HighlightDirective de la page Directives d'attribut .

La directive définit l'arrière-plan sur une couleur de surbrillance lorsque l'utilisateur passe la souris sur l'élément DOM auquel la directive est appliquée.

Angular définit le paramètre el du constructeur sur l'injecté ElementRef.

L'exemple de code applique l'attribut myHighlight de la directive à deux balises ‹div›, d'abord sans valeur (donnant la couleur par défaut), puis avec une valeur de couleur attribuée.

L'image suivante montre l'effet de la souris sur la balise ‹hero-bios-and-contacts›.




Définir les dépendances avec les fournisseurs

Cette section montre comment écrire des fournisseurs pour des services dépendants.

Pour obtenir un service d'un injecteur de dépendance, vous devez lui donner un jeton. Angular gère généralement cette transaction en spécifiant un paramètre de constructeur et son type. Le type de paramètre sert de jeton de recherche d'injecteur. Angular transmet ce jeton à l'injecteur et affecte le résultat au paramètre.

Ce qui suit est un exemple typique.

Angular demande à l'injecteur le service associé à LoggerService et attribue la valeur renvoyée au paramètre logger.

Si l'injecteur a déjà mis en cache une instance du service associé au jeton, il fournit cette instance. Si ce n'est pas le cas, il doit en créer un en utilisant le fournisseur associé au jeton.

Si l'injecteur ne dispose pas d'un fournisseur pour un jeton demandé, il délègue la demande à son injecteur parent, où le processus se répète jusqu'à ce qu'il n'y ait plus d'injecteurs. Si la recherche échoue, l'injecteur génère une erreur, à moins que la demande ne soit facultative.

Un nouvel injecteur n'a pas de fournisseur. Angular initialise les injecteurs créés avec un ensemble de fournisseurs préférés. Vous devez configurer les fournisseurs pour vos propres dépendances spécifiques à l'application.

Définir les fournisseurs

Une dépendance ne peut pas toujours être créée par la méthode d'instanciation d'une classe par défaut. Vous avez découvert d'autres méthodes dans les fournisseurs de dépendance. L'exemple HeroOfTheMonthComponent suivant montre plusieurs des alternatives et pourquoi vous en avez besoin. C'est visuellement simple: quelques propriétés et les journaux produits par un enregistreur.



Le code sous-jacent personnalise comment et où le framework DI fournit des dépendances. Les cas d'utilisation illustrent différentes manières d'utiliser le littéral de fourniture d'objet pour associer un objet de définition à un jeton DI.

Le tableau providers montre comment vous pouvez utiliser les différentes clés de définition de fournisseur. useValue, useClass, useExisting ou useFactory.

Fournisseurs de valeur : useValue

La clé useValue vous permet d'associer une valeur fixe à un jeton DI. Utilisez cette technique pour fournir des constantes de configuration d'exécution telles que les adresses de base de sites Web et les indicateurs de fonctionnalités. Vous pouvez également utiliser un fournisseur de valeur dans un test unitaire pour fournir des données fictives à la place d'un service de données de production.

L'exemple HeroOfTheMonthComponent a deux fournisseurs de valeur.

  • La première fournit une instance existante de la classe Hero à utiliser pour le jeton Hero, au lieu de demander à l'injecteur de créer une nouvelle instance new ou d'utiliser sa propre instance mise en cache. Ici, le jeton est la classe elle-même.
  • La seconde spécifie une ressource chaîne littérale à utiliser pour le jeton TITLE. Le jeton TITLE fournisseur n'est pas une classe, mais un type spécial de clé de recherche de fournisseur appelé jeton d'injection , représenté par une instance InjectionToken.

Vous pouvez utiliser un jeton d'injection pour tout type de fournisseur, mais cela s'avère particulièrement utile lorsque la dépendance est une valeur simple, telle qu'une chaîne, un nombre ou une fonction.

La valeur d'un fournisseur de valeur doit être définie avant que vous le spécifiiez ici. Le littéral de chaîne de titre est immédiatement disponible. La variable someHero dans cet exemple a été définie précédemment dans le fichier, comme indiqué ci-dessous. Vous ne pouvez pas utiliser une variable dont la valeur sera définie ultérieurement.

D'autres types de prestataires peuvent créer leurs valeurs paresseusement ; c'est-à-dire, quand ils sont nécessaires pour l'injection.

Fournisseurs de classe: useClass

La clé useClass de fournisseur vous permet de créer et de renvoyer une nouvelle instance de la classe spécifiée.

Vous pouvez utiliser ce type de fournisseur pour remplacer une implémentation alternative par une classe commune ou par défaut. L'implémentation alternative pourrait, par exemple, implémenter une stratégie différente, étendre la classe par défaut ou émuler le comportement de la classe réelle dans un scénario de test.

Le code suivant montre deux exemples dans HeroOfTheMonthComponent.

Le premier fournisseur est la forme développée et sucrée du cas le plus typique dans lequel la classe à créer (HeroService) est également le jeton d'injection de dépendance du fournisseur. La forme courte est généralement préférée; cette forme longue rend les détails explicites.

Le deuxième fournisseur se substitue DateLoggerService à LoggerService. LoggerService est déjà inscrit au niveau AppComponent. Lorsque ce composant enfant le demande LoggerService, il reçoit une instance DateLoggerService à la place.

Ce composant et son arborescence de composants enfants reçoivent une instance DateLoggerService. Les composants situés en dehors de l'arborescence continuent de recevoir l'instance LoggerService d'origine.

DateLoggerService hérite de LoggerService; il ajoute la date/heure actuelle à chaque message :
Alias providers: useExisting

La clé useExisting de fournisseur vous permet de mapper un jeton sur un autre. En effet, le premier jeton est un alias du service associé au deuxième jeton, ce qui crée deux façons d'accéder au même objet de service.

Vous pouvez utiliser cette technique pour restreindre une API via une interface d'alias. L'exemple suivant montre un alias introduit à cet effet.

Imaginez que LoggerService ait une grande API, beaucoup plus grande que les trois méthodes et une propriété. Vous voudrez peut-être réduire cette surface d'API aux membres dont vous avez réellement besoin. Dans cet exemple, l'interface de classe MinimalLogger réduit l'API à deux membres:

L'exemple suivant utilise MinimalLogger dans une version simplifiée de HeroOfTheMonthComponent.

Le paramètre HeroOfTheMonthComponent du constructeur logger est saisi de la MinimalLogger manière suivante : seuls les membres logs et logInfo sont donc visibles dans un éditeur compatible avec TypeScript.



Dans les coulisses, Angular définit le paramètre logger sur le service complet enregistré sous le jeton LoggingService, qui se trouve être l'instance DateLoggerService fournie ci-dessus.

Ceci est illustré dans l'image suivante, qui affiche la date de journalisation.


Fournisseurs d'usine: useFactory

La clé useFactory de fournisseur vous permet de créer un objet de dépendance en appelant une fonction de fabrique, comme dans l'exemple suivant.

L'injecteur fournit la valeur de dépendance en appelant une fonction de fabrique que vous indiquez comme valeur de la clé useFactory. Notez que cette forme de fournisseur a une troisième clé deps, qui spécifie les dépendances de la fonction useFactory.

Utilisez cette technique pour créer un objet de dépendance avec une fonction fabrique dont les entrées combinent des services injectés et un état local.

L'objet de dépendance (renvoyé par la fonction factory) est généralement une instance de classe, mais peut également être différent. Dans cet exemple, l'objet de dépendance est une chaîne des noms des coureurs jusqu'au concours "Héros du mois".

Dans l'exemple, l'état local est le nombre 2, le nombre de gagnants que le composant doit afficher. La valeur d'état est transmise entant qu'argument runnersUpFactory(). Les runnersUpFactory() rendements de la fonction usine de fournisseur, qui peut utiliser à la fois la valeur d'état-passé et les services injectés Hero et HeroService.

La fonction de fabrique du fournisseur (renvoyée par runnersUpFactory()) renvoie l'objet de dépendance réel, la chaîne de noms.

  • La fonction prend un gagnant Hero et un HeroService comme arguments. Angular fournit ces arguments à partir des valeurs injectées identifiées par les deux jetons du tableau.
  • La fonction retourne la chaîne de noms, qui Angular qu'injecte dans le runnersUpparam de HeroOfTheMonthComponent.


Alternatives de jetons de fournisseur: interface de classe et 'InjectionToken'

L'injection de dépendance Angular est plus facile lorsque le jeton de fournisseur est une classe qui est également le type de l'objet de dépendance renvoyé ou du service.

Cependant, un jeton ne doit pas nécessairement être une classe et même s'il s'agit d'une classe, il ne doit pas nécessairement être du même type que l'objet renvoyé. C'est le sujet de la section suivante.

Interface de classe

L'exemple précédent du héros du mois utilisait la classe MinimalLogger comme jeton pour un fournisseur de LoggerService.

MinimalLogger est une classe abstraite.
Une classe abstraite est généralement une classe de base que vous pouvez étendre. Dans cette application, cependant, il n'y a pas de classe qui hérite de MinimalLogger. Le LoggerService et le DateLoggerService auraient pu hériter de MinimalLogger, ou ils auraient pu le mettre en oeuvre à la place, à la manière d'une interface. Mais ils ne font ni l'un ni l'autre. MinimalLogger est utilisé uniquement en tant que jeton d'injection de dépendance.

Lorsque vous utilisez une classe de cette manière, cela s'appelle une interface de classe.

Comme indiqué dans les fournisseurs DI , une interface n'est pas un jeton DI valide, car il s'agit d'un artefact TypeScript qui n'existe pas au moment de l'exécution. Utilisez cette interface de classe abstraite pour obtenir le typage fort d'une interface et utilisez-la également comme jeton de fournisseur, à la manière d'une classe normale.

Une interface de classe ne doit définir que les membres que ses clients sont autorisés à appeler. Une telle interface étroite permet de découpler la classe concrète de ses consommateurs.

L'utilisation d'une classe en tant qu'interface vous donne les caractéristiques d'une interface dans un objet JavaScript réel. Pour minimiser les coûts de mémoire, cependant, la classe ne devrait avoir aucune implémentation. Les transcriptions MinimalLogger dans ce JavaScript prédéfini non optimisé pour une fonction constructeur.

Notez qu'il n'a pas de membres. Elle ne grandit jamais, peu importe le nombre de membres que vous ajoutez à la classe, tant que ces membres sont dactylographiés mais non implémentés.

Examinez à nouveau la classe MinimalLogger TypeScript pour confirmer qu'elle n'a aucune implémentation.

Objets 'InjectionToken'

Les objets de dépendance peuvent être des valeurs simples telles que des dates, des nombres et des chaînes, ou des objets sans forme tels que des tableaux et des fonctions.

De tels objets n'ont pas d'interface d'application et ne sont donc pas bien représentés par une classe. Ils sont mieux représentés par un jeton qui est à la fois unique et symbolique, un objet JavaScript qui porte un nom convivial mais ne sera pas en conflit avec un autre jeton qui porte le même nom.

InjectionToken a ces caractéristiques. Vous les avez rencontrés deux fois dans l'exemple Héros du mois , dans le fournisseur de valeur de titre et dans le fournisseur de runnersUpFactory .

Vous avez créé le TITLE jeton comme ceci:
Le paramètre type, bien que facultatif, transmet le type de dépendance aux développeurs et aux outils. La description du jeton est une autre aide au développement.

Injecter dans une classe dérivée

Faites attention lorsque vous écrivez un composant qui hérite d'un autre composant. Si le composant de base a des dépendances injectées, vous devez les redistribuer et les réinjecter dans la classe dérivée, puis les transmettre à la classe de base par le biais du constructeur.

Dans cet exemple, SortedHeroesComponent hérite de HeroesBaseComponent pour afficher une liste triée des héros.



Le HeroesBaseComponent peut être autonome. Il exige sa propre instance HeroService pour obtenir des héros et les affiche dans l'ordre d'arrivée dans la base de données.

Garder les constructeurs simples

Les constructeurs ne devraient faire qu'initialiser les variables. Cette règle rend le composant sûr à construire sous test sans crainte de faire quelque chose de dramatique, comme de parler au serveur. C'est pourquoi vous appelez le HeroService de l'intérieur ngOnInit plutôt que le constructeur.

Les utilisateurs veulent voir les héros dans l'ordre alphabétique. Plutôt que de modifier le composant d'origine, sous-classez-le et créez un élément SortedHeroesComponent qui trie les héros avant de les présenter. La SortedHeroesComponent laisse la classe de base les héros chercher.

Malheureusement, Angular ne peut pas injecter HeroService directement dans la classe de base. Vous devez fournir le HeroService pour ce composant, puis le transmettre à la classe de base à l'intérieur du constructeur.

Maintenant, prenez note de la méthode afterGetHeroes(). Votre premier instinct aurait pu être de créer une méthode ngOnInit SortedHeroesComponent et d'y faire le tri. Mais Angular appelle les classes dérivées ngOnInit avant d'appeler les classes de base ngOnInit afin de trier le tableau des héros avant leur arrivée. Cela produit une mauvaise erreur.

Redéfinir la méthode afterGetHeroes() de la classe de base résout le problème. Ces complications plaident pour éviter l'héritage des composants.

Rompre les circularités avec une référence de classe directe ( forwardRef )

L'ordre de déclaration de classe est important dans TypeScript. Vous ne pouvez pas vous référer directement à une classe tant qu'elle n'a pas été définie.

Ce n'est généralement pas un problème, surtout si vous respectez la règle de classe par fichier recommandée. Mais parfois, les références circulaires sont inévitables. Vous êtes dans une impasse lorsque la classe "A" fait référence à la classe "B" et que "B" fait référence à "A". L'un d'entre eux doit être défini en premier.

La fonction forwardRef() Angular crée une référence indirecte que Angular peut résoudre ultérieurement.

L'exemple "Parent Finder" est rempli de références de classe circulaires qu'il est impossible de décomposer.

Vous faites face à ce dilemme quand une classe fait référence à elle-même comme AlexComponent dans son tableau providers. Le tableau providers est une propriété de la fonction décorateur qui doit apparaître au dessus de la définition de la classe @Component()

Casser la circularité avec forwardRef.