
En tant que framework front-end conçu « pour les projets front-end à grande échelle », Angular possède en fait de nombreuses conceptions dignes de référence et d'apprentissage. Cette série est principalement utilisée pour étudier les principes de mise en œuvre de ces conceptions et fonctions. Cet article se concentre sur la plus grande fonctionnalité d'Angular : l'injection de dépendances, et présente la conception de l'injection de dépendances à plusieurs niveaux dans Angular. [Tutoriels associés recommandés : "Tutoriel Angular"]
Dans l'article précédent, nous avons présenté Injectot , le fournisseur Provider et le mécanisme d'injection dans Angular. Alors, dans les applications Angular, comment les composants et les modules partagent-ils les dépendances ? Le même service peut-il être instancié plusieurs fois ?
Le processus d'injection de dépendances de composants et de modules est indissociable de la conception d'injection de dépendances à plusieurs niveaux d'Angular.
Comme nous l'avons dit plus tôt, l'injecteur dans Angular est héritable et hiérarchique.
Dans Angular, il existe deux hiérarchies d'injecteurs :
ModuleInjector Module Injector : configurez ModuleInjector dans cette hiérarchie à l'aide de l'annotation @NgModule() ou @Injectable() ModuleInjectorElementInjector Injector : créez implicitementdes modules sur chaque élément DOM. Les injecteurs et les injecteurs d'éléments sont structurés en arborescence, mais leurs hiérarchies ne sont pas exactement les mêmes.
La structure hiérarchique de l'injecteur de module n'est pas seulement liée à la conception du module dans l'application, mais a également la structure hiérarchique de l'injecteur de module de plate-forme (PlatformModule) et de l'injecteur de module d'application (AppModule).
Dans la terminologie angulaire, une plate-forme est le contexte dans lequel les applications Angular s'exécutent. La plate-forme la plus courante pour les applications Angular est un navigateur Web, mais il peut également s'agir du système d'exploitation d'un appareil mobile ou d'un serveur Web.
Lorsqu'une application Angular est démarrée, elle crée une couche de plate-forme :
la plate-forme est le point d'entrée d'Angular sur la page Web. Chaque page n'a qu'une seule plate-forme exécutée sur la page, et tous les services communs sont liés àun Angular
Platform, comprenant principalement des fonctions telles que la création d'instances de modules et leur destruction :
@Injectable()
classe d'exportation PlatformRef {
// Passer l'injecteur en tant que constructeur d'injecteur de plate-forme (private _injector : Injector) {}
// Créez une instance de @NgModule pour la plate-forme donnée pour la compilation hors ligne bootstrapModuleFactory<M>(moduleFactory : NgModuleFactory<M>, options ? : BootstrapOptions) :
Promesse<NgModuleRef<M>> {}
// À l'aide du compilateur d'exécution donné, créez une instance de @NgModule pour la plate-forme donnée bootstrapModule<M>(
moduleType : Type<M>,
compilerOptions : (CompilerOptions&BootstrapOptions)|
Array<CompilerOptions&BootstrapOptions> = []) : Promise<NgModuleRef<M>> {}
// Enregistrez l'écouteur à appeler lors de la destruction de la plateforme onDestroy(callback: () => void): void {}
// Récupère l'injecteur de plateforme // L'injecteur de plateforme est l'injecteur parent de chaque application Angular sur la page et fournit le fournisseur singleton get injector() : Injector {}
// Détruire la plateforme Angular actuelle et toutes les applications Angular de la page, y compris la destruction de tous les modules et auditeurs enregistrés sur la plateforme destroy() {}
} En fait, au démarrage de la plateforme (dans la méthode bootstrapModuleFactory ), ngZoneInjector est créé dans ngZone.run pour que tous les services instanciés soient créés dans la zone Angular, et ApplicationRef (application Angular exécutée sur la page) soit dans la zone Angular. Zone angulaire Créée à l'extérieur.
Au lancement dans le navigateur, la plateforme du navigateur est créée :
export const platformBrowser: (extraProviders?: StaticProvider[]) => PlatformRef =
createPlatformFactory(platformCore, 'navigateur', INTERNAL_BROWSER_PLATFORM_PROVIDERS);
// Parmi eux, la plateforme platformCore doit être incluse dans tout autre export de plateforme const platformCore = createPlatformFactory(null, 'core', _CORE_PLATFORM_PROVIDERS); Lors de la création d'une plateforme à l'aide d'une fabrique de plateforme (telle que createPlatformFactory ci-dessus), la plateforme de la page); sera implicitement initialisé :
fonction d'export createPlatformFactory(
parentPlatformFactory : ((extraProviders?: StaticProvider[]) => PlatformRef)|null, nom : chaîne,
fournisseurs : StaticProvider[] = []) : (extraProviders ? : StaticProvider[]) => PlatformRef {
const desc = `Plateforme : ${name}` ;
const marqueur = new InjectionToken(desc); // Retour du jeton DI (extraProviders : StaticProvider[] = []) => {
let platform = getPlatform();
// Si la plateforme a été créée, aucun traitement n'est effectué if (!platform || platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) {
si (parentPlatformFactory) {
// S'il existe une plateforme parent, utiliser directement la plateforme parent et mettre à jour le fournisseur correspondant parentPlatformFactory(
fournisseurs.concat(extraProviders).concat({provide : marqueur, useValue : true}));
} autre {
const injectedProviders : StaticProvider[] =
fournisseurs.concat(extraProviders).concat({provide : marqueur, useValue : true}, {
fournir : INJECTOR_SCOPE,
useValue : « plateforme »
});
// S'il n'y a pas de plateforme parent, créez un nouvel injecteur et créez une plateforme createPlatform(Injector.create({providers: injectedProviders, name: desc}));
}
}
return assertPlatform (marqueur);
} ;
} Grâce au processus ci-dessus, nous savons que lorsque l'application Angular crée une plateforme, elle crée l'injecteur de module de la plateforme ModuleInjector . Nous pouvons également voir dans Injector dans la section précédente que NullInjector est le premier de tous les injecteurs :
export abstract class Injector {
NULL statique : Injecteur = new NullInjector();
} Ainsi, au-dessus de l'injecteur de module de plateforme, il y a NullInjector() . Sous l’injecteur de module plateforme, se trouve également l’injecteur de module application.
Chaque application possède au moins un module angulaire. Le module racine est le module utilisé pour démarrer cette application :
@NgModule ({ fournisseurs : APPLICATION_MODULE_PROVIDERS })
classe d'exportation ApplicationModule {
// ApplicationRef nécessite que le bootstrap fournisse le constructeur de composants (appRef : ApplicationRef) {}
} AppModule est réexporté par BrowserModule , et lorsque nous créons une nouvelle application à l'aide de la new commande de la CLI, elle est automatiquement incluse dans le module racine AppModule . Dans le module racine de l'application, le fournisseur est associé à un jeton DI intégré utilisé pour configurer l'injecteur racine pour le bootstrap.
Angular ajoute également ComponentFactoryResolver à l'injecteur du module racine. Cet analyseur stocke entryComponents , il est donc responsable de la création dynamique des composants.
À ce stade, nous pouvons simplement trier la relation hiérarchique des injecteurs de modules :
le niveau supérieur de l'arborescence des injecteurs de modules est l'injecteur du module racine de l'application (AppModule), appelé racine.
Il y a deux injecteurs au-dessus de la racine, l'un est l'injecteur du module de plate-forme (PlatformModule) et l'autre est NullInjector() .
Par conséquent, la hiérarchie des injecteurs de modules est la suivante :

Dans notre application actuelle, cela ressemblera probablement à ceci :

Angular DI a une architecture d'injection en couches, ce qui signifie que les injecteurs de niveau inférieur peuvent également créer leurs propres instances de service.
Comme mentionné précédemment, il existe deux hiérarchies d'injecteurs dans Angular, à savoir l'injecteur de module et l'injecteur d'élément.
Lorsque les modules à chargement paresseux ont commencé à être largement utilisés dans Angular, un problème est survenu : le système d'injection de dépendances a fait doubler l'instanciation des modules à chargement paresseux.
Dans ce correctif, une nouvelle conception a été introduite : l'injecteur utilise deux arbres parallèles, un pour les éléments et un pour les modules .
Angular crée des usines hôtes pour tous entryComponents , qui sont les vues racine de tous les autres composants.
Cela signifie que chaque fois que nous créons un composant angulaire dynamique, la vue racine ( RootData ) sera créée avec les données racine ( RootView ) :
class ComponentFactory_ extends ComponentFactory<any>{
créer(
injecteur : Injecteur, projectableNodes ? : any[][], rootSelectorOrNode ? : string|any,
ngModule ? : NgModuleRef<any>): ComponentRef<any> {
si (!ngModule) {
throw new Error ('ngModule doit être fourni');
}
const viewDef = solveDefinition(this.viewDefFactory);
const composantNodeIndex = viewDef.nodes[0].element!.componentProvider!.nodeIndex;
//Créez la vue racine à l'aide des données racine const view = Services.createRootView(
injecteur, projectableNodes || [], rootSelectorOrNode, viewDef, ngModule, EMPTY_CONTEXT);
// Accesseur pour view.nodes const composant = asProviderData(view, composantNodeIndex).instance;
si (rootSelectorOrNode) {
view.renderer.setAttribute(asElementData(view, 0).renderElement, 'ng-version', VERSION.full);
}
//Créer un composant return new ComponentRef_(view, new ViewRef_(view), composant);
}
} Les données racine ( RootData ) contiennent des références elInjector et ngModule :
function createRootData(
elInjector : Injecteur, ngModule : NgModuleRef<any>, rendererFactory : RendererFactory2,
projectableNodes : any[][], rootSelectorOrNode : any) : RootData {
const sanitizer = ngModule.injector.get (Sanitizer);
const errorHandler = ngModule.injector.get (ErrorHandler);
const renderer = rendererFactory.createRenderer(null, null);
retour {
ngModule,
injecteur: elInjector,
Nœuds projetables,
sélecteurOrNode : rootSelectorOrNode,
désinfectant,
rendererFactory,
moteur de rendu,
gestionnaire d'erreurs,
} ;
} Présentation de l'arborescence des injecteurs d'éléments car cette conception est relativement simple. En modifiant la hiérarchie des injecteurs, évitez d'entrelacer les injecteurs de modules et de composants, ce qui entraînerait une double instanciation des modules chargés paresseux. Parce que chaque injecteur n'a qu'un seul parent, et chaque résolution doit trouver exactement un injecteur pour récupérer les dépendances.
Dans Angular, une vue est une représentation d'un modèle. Elle contient différents types de nœuds, parmi lesquels se trouve le nœud d'élément. L'injecteur d'élément se trouve sur ce nœud :
interface d'export ElementDef {.
...
// Fournisseurs publics de DI visibles dans cette vue publicProviders : {[tokenKey: string] : NodeDef}|null ;
// Identique à visiblePublicProviders, mais inclut également les fournisseurs privés situés sur cet élément allProviders : {[tokenKey: string]: NodeDef}|null ;
} ElementInjector est vide par défaut sauf configuration dans l'attribut providers de @Directive() ou @Component() .
Lorsque Angular crée un injecteur d'élément pour un élément HTML imbriqué, soit il en hérite de l'injecteur d'élément parent, soit il attribue l'injecteur d'élément parent directement à la définition du nœud enfant.
Si un injecteur d'élément sur un élément HTML enfant a un fournisseur, il doit être hérité. Sinon, il n'est pas nécessaire de créer un injecteur distinct pour le composant enfant et les dépendances peuvent être résolues directement à partir de l'injecteur parent si nécessaire.
Alors, où les injecteurs d'éléments et les injecteurs de modules commencent-ils à devenir des arbres parallèles ?
Nous savons déjà que le module racine de l'application ( AppModule ) sera automatiquement inclus dans l' AppModule racine lors de la création d'une nouvelle application à l'aide de la new commande de la CLI.
Lorsque l'application ( ApplicationRef ) démarre ( bootstrap ), entryComponent est créé :
const compRef = composantFactory.create(Injector.NULL, [], selectorOrNode, ngModule
Ce processus crée la vue racine ( RootView ) en utilisant les données racine ( RootData ) , et l'injecteur d'élément racine sera créé, où elInjector est Injector.NULL .
Ici, l'arborescence des injecteurs d'Angular est divisée en arborescence des injecteurs d'éléments et arborescence des injecteurs de modules, ces deux arbres parallèles.
Angular créera régulièrement des injecteurs subordonnés. Chaque fois qu'Angular crée une instance de composant providers spécifiés dans @Component() , il créera également un nouveau sous-injecteur pour l'instance. De même, lorsqu'un nouveau NgModule est chargé au moment de l'exécution, Angular peut créer un injecteur pour celui-ci avec son propre fournisseur.
Les injecteurs de sous-modules et de composants sont indépendants les uns des autres et créent chacun leur propre instance pour le service fourni. Lorsque Angular détruit NgModule ou de composant, il détruit également ces injecteurs et ces instances de service dans les injecteurs.
Ci-dessus, nous avons introduit deux types d'arborescences d'injecteurs dans Angular : l'arborescence d'injecteurs de modules et l'arborescence d'injecteurs d'éléments. Alors, comment Angular résout-il le problème lors de la fourniture de dépendances ?
Dans Angular, lors de la résolution de jetons pour obtenir des dépendances pour les composants/instructions, Angular le résout en deux étapes :
ElementInjector (son parent)ModuleInjector (son parent).Le processus est le suivant (voir Multi-Level). Injecteur - Règles de résolution) :
Lorsqu'un composant déclare une dépendance, Angular essaiera de satisfaire cette dépendance en utilisant son propre ElementInjector .
Si l'injecteur d'un composant n'a pas de fournisseur, il transmettra la requête au ElementInjector de son composant parent.
Ces requêtes continueront à être transmises jusqu'à ce qu'Angular trouve un injecteur capable de gérer la requête ou soit à court d' ElementInjector ancêtre.
Si Angular ne trouve pas le fournisseur dans un ElementInjector , il reviendra à l'élément à partir duquel la demande a été effectuée et recherchera ModuleInjector .
Si Angular ne parvient toujours pas à trouver le fournisseur, une erreur sera générée.
À cette fin, Angular introduit un injecteur de fusion spécial.
L'injecteur de fusion lui-même n'a aucune valeur, c'est juste une combinaison de définitions de vues et d'éléments.
la classe Injector_ implémente l'injecteur {
constructeur (vue privée : ViewData, elDef privé : NodeDef|null) {}
get(token : any, notFoundValue : any = Injector.THROW_IF_NOT_FOUND) : any {
constallowPrivateServices =
this.elDef ? (this.elDef.flags & NodeFlags.ComponentView) !== 0 : faux;
retourner Services.resolveDep(
this.view, this.elDef, allowPrivateServices,
{flags : DepFlags.None, token, tokenKey : tokenKey(token)}, notFoundValue );
}
} Lorsque Angular résout les dépendances, l'injecteur de fusion est le pont entre l'arborescence des injecteurs d'éléments et l'arborescence des injecteurs de modules. Lorsqu'Angular tente de résoudre certaines dépendances dans un composant ou une directive, il utilise l'injecteur de fusion pour parcourir l'arborescence de l'injecteur d'éléments, puis, si la dépendance n'est pas trouvée, passe à l'arborescence de l'injecteur de module pour résoudre la dépendance.
la classe ViewContainerRef_ implémente ViewContainerData {
...
//Requête pour l'injecteur d'élément de vue parent get parentInjector() : Injector {
laissez view = this._view;
laissez elDef = this._elDef.parent;
while (!elDef && vue) {
elDef = viewParentEl(vue);
view = view.parent!;
}
return view ? new Injector_(view, elDef) : new Injector_(this._view, null);
}
} Les injecteurs sont héritables, ce qui signifie que si l'injecteur spécifié ne peut pas résoudre une dépendance, il demandera à l'injecteur parent de la résoudre. L'algorithme d'analyse spécifique est implémenté dans resolveDep() :
fonction d'exportation solveDep(
vue : ViewData, elDef : NodeDef, allowPrivateServices : booléen, depDef : DepDef,
notFoundValue : any = Injector.THROW_IF_NOT_FOUND) : any {
//
//mod1
///
// el1 mod2
// /
//el2
//
// Lors de la demande de el2.injector.get(token), vérifie et renvoie la première valeur trouvée dans l'ordre suivant :
// - el2.injector.get (jeton, par défaut)
// - el1.injector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) -> ne vérifie pas le module
// - mod2.injector.get (jeton, par défaut)
} S'il s'agit du composant racine AppComponent d'un modèle tel que <child></child> , alors il y aura trois vues dans Angular :
<!-- HostView_AppComponent -->
<mon-application></mon-application>
<!-- View_AppComponent -->
<enfant></enfant>
<!-- View_ChildComponent -->
Certains contenus reposent sur le processus d'analyse. L'algorithme d'analyse sera basé sur la hiérarchie des vues, comme le montre la figure :

Si certains jetons sont résolus dans un composant enfant, Angular :
examinera d'abord l'injecteur d'élément enfant, en vérifiant elRef.element.allProviders|publicProviders .
Parcourez ensuite tous les éléments de la vue parent (1) et vérifiez le fournisseur dans l'injecteur d'éléments.
Si l'élément de vue parent suivant est égal à null (2), revenez à startView (3) et vérifiez startView.rootData.elnjector (4).
Seulement si le jeton n'est pas trouvé, vérifiez startView.rootData module.injector (5).
Il s'ensuit qu'Angular, lors de la traversée de composants pour résoudre certaines dépendances, recherchera l'élément parent d'une vue spécifique plutôt que l'élément parent d'un élément spécifique. L'élément parent de la vue peut être obtenu via :
// Pour les vues de composants, il s'agit de l'élément hôte // Pour les vues intégrées, il s'agit de l'index du nœud parent de la fonction d'exportation du conteneur de vue contenant viewParentEl(view: ViewData): NodeDef| nul {
const parentView = view.parent;
si (parentView) {
return view.parentNodeDef !.parent;
} autre {
renvoie null ;
}
} Cet article présente principalement la structure hiérarchique des injecteurs dans Angular. Il existe deux arborescences d'injecteurs parallèles dans Angular : l'arborescence des injecteurs de modules et l'arborescence des injecteurs d'éléments.
L'introduction de l'arborescence des injecteurs d'éléments vise principalement à résoudre le problème de la double instanciation des modules causée par l'analyse par injection de dépendances et le chargement paresseux des modules. Après l'introduction de l'arborescence des injecteurs d'éléments, le processus d'analyse des dépendances d'Angular a également été ajusté. Il donne la priorité à la recherche des dépendances des injecteurs tels que les injecteurs d'éléments et les injecteurs d'éléments de la vue parent uniquement lorsque le jeton est introuvable dans l'injecteur d'éléments, l'injecteur de module. seront interrogés dans les dépendances.