La notion d'Injector en Angular
Un Injector
est une mécanique dédiée à l’injection de dépendances en Angular. Les différents frameworks n’ont pas tous la même approche pour la DI. Par exemple, le fameux framework Java utilise le Spring IoC Container. La documentation d’Angular a longtemps mis en avant les providers
, tout en négligeant d’autres éléments importants pour la DI. Les Injector
ne font pas exception à cette règle. C’est pourquoi je vous propose de découvrir ensemble comment ils fonctionnent.
Jusqu’à la version 13, les explications sur la DI se concentrent sur les cas d’usages. Ce n’est qu’après que les rouages internes ont été documentés (quelques exemples ici et là).
Comprendre le rôle de l’Injector en Angular
L’Injector
est simplement un objet qui gère les providers
. Il doit fournir les dépendances et s’assurer qu’elles sont des singletons grâce à un cache interne. Ce cache interne conserve les références aux providers
, ce qui permet de renvoyer les instances existantes. Si nécessaire, l’Injector
se charge de l’instanciation et conserve une référence pour les prochaines demandes, comme un jeton d’injection pour une classe ou un service. L’Injector
joue donc un rôle clé entre les providers
définis dans l’application et les composants/services qui utilisent ces providers
.
Il est à noter que le fonctionnement d’un seul Injector
est relativement simple. Toutefois, la complexité provient du fait qu’il en existe un grand nombre dans une application Angular et que, en tant que développeur Angular, on ne les manipule pas directement (sauf dans des cas exceptionnels).
Des Injector partout
Ils sont nombreux, mais combien sont-ils exactement ? Chaque NgModule
crée automatiquement un Injector
, tandis que chaque composant ou directive qui possède des providers
ou des viewProviders
en crée également un. Dans le premier cas, il s’agit d’un ModuleInjector
, et dans le deuxième, on parle d’ElementInjector
. Il est donc évident que le nombre d’Injector
augmentera rapidement avec l’application, avec au minimum autant d’Injector
que de NgModule
.
Lorsqu’un Injector
est détruit, les instances qu’il gérait dans son cache sont également détruites. Les NgModule
ne sont pas souvent détruits, mais ce n’est pas le cas pour les composants. Les ElementInjector
sont donc fréquemment créés et détruits, tout comme les services associés.
Gestion de dépendances et Injector
Chaque demande de dépendance est résolue en parcourant la hiérarchie des Injector
. Pour un service injecté dans un composant, l’ElementInjector
du composant est interrogé en premier (s’il existe), puis le ModuleInjector
du module déclarant le composant, ensuite l’Injector
du module parent, et ainsi de suite jusqu’au ModuleInjector
root. La recherche s’interrompt dès qu’une instance du service est trouvée.
Pour le sommet de la structure,le fonctionnement est similaire pour toutes les applications Angular. Le PlatformModule
est invoqué dans main.ts
et tout ce qui n’a pas été trouvé est finalement géré par un NullInjector
(cf. illustration ci-dessous).
Le contenu des providers
d’un Injector
n’est pas immuable. Il est tout à fait possible d’ajouter des services supplémentaires à l’Injector
root après le démarrage de l’application. C’est ce qui se produit lorsque vous spécifiez providedIn: "root"
pour un service qui n’est utilisé que dans un module en mode lazy-loading.
Injector, Bundle et Instanciation
Il est impératif d’aborder la gestion du bundle et de l’instanciation en parlant d’Injector
, car les erreurs de compréhension sont fréquentes. Il ne faut pas croire que le simple fait d’avoir un service déclaré avec providedIn: "root"
signifie qu’il sera inclus dans le bundle principal, ni qu’il sera automatiquement instancié lors du démarrage de l’application.
Concernant le bundle, le processus de tree-shaking se charge de ne garder que ce qui est vraiment nécessaire. Les services qui ne sont pas utilisés dans le bundle principal sont donc exclus. Cependant, les services définis dans les providers
d’un module ne bénéficient pas de cet avantage et seront inclus dans le bundle en même temps que le module. Quant aux services providedIn: "root"
utilisés dans plusieurs modules avec lazy-loading, l’option commonChunk détermine s’ils seront dupliqués dans chaque bundle ou s’ils seront regroupés dans un seul bundle commun (valeur par défaut).
Quand à l’instanciation, elle est reportée jusqu’au dernier moment. Si aucun consommateur n’utilise le service, il n’y a pas besoin de l’instancier. La déclaration providedIn: "root"
garantit simplement que le service sera un singleton accessible dans toute l’application, mais il peut ne jamais être instancié. Soyez donc attentif lors de l’utilisation de services, car la DI peut être magique, mais il y a certaines nuances à prendre en compte.
Conclusion
J’espère avoir amélioré votre compréhension du rôle des Injector
dans Angular. Vous êtes à présent au courant que c’est un objet responsable de la gestion des instanciations à la racine, dans les modules et dans certains composants ou directives. Ces instanciations sont partagées avec les éléments enfants, mais peuvent également être remplacées localement par un sous-module ou un composant.
Lorsque vous effectuez un chargement de composant dynamique, vous pouvez spécifier l’Injector
à utiliser (voir documentation sur ViewContainerRef
). Pour en savoir plus, je vous recommande de vérifier l’impact des standalone components sur les Injector
, disponible ici.
Aller plus loin : Si vous voulez plus de détails sur les Injector
et l’injection en Angular, je vous recommande cet article (en anglais) qui vous apprendra tous les rouages : How Angular Dependency Injection works under the hood