Als Front-End-Framework, das „für große Front-End-Projekte“ entwickelt wurde, verfügt Angular tatsächlich über viele referenz- und lernwürdige Designs. Diese Serie wird hauptsächlich zum Studium der Implementierungsprinzipien dieser Designs und Funktionen verwendet. Dieser Artikel konzentriert sich auf das größte Merkmal von Angular – die Abhängigkeitsinjektion, und stellt das Design der mehrstufigen Abhängigkeitsinjektion in Angular vor. [Empfohlene verwandte Tutorials: „Angular-Tutorial“]
Im vorherigen Artikel haben wir Injectot
-Injektor, den Provider
-Anbieter und den Injektormechanismus in Angular vorgestellt. Wie teilen Komponenten und Module in Angular-Anwendungen Abhängigkeiten? Kann derselbe Dienst mehrmals instanziiert werden?
Der Abhängigkeitsinjektionsprozess von Komponenten und Modulen ist untrennbar mit dem mehrstufigen Abhängigkeitsinjektionsdesign von Angular verbunden.
Wie bereits erwähnt, ist der Injektor in Angular vererbbar und hierarchisch.
In Angular gibt es zwei Injektorhierarchien:
ModuleInjector
Modulinjektor: Konfigurieren Sie ModuleInjector in dieser Hierarchie mithilfe der Annotation @NgModule()
oder @Injectable()
ModuleInjector
ElementInjector
: Erstellen Sie implizitModule für jedes DOM-Element. Sowohl Injektoren als auch Elementinjektoren sind baumstrukturiert. aber ihre Hierarchien sind nicht genau gleich.
Die hierarchische Struktur des Modulinjektors hängt nicht nur mit dem Moduldesign in der Anwendung zusammen, sondern weist auch die hierarchische Struktur des Plattformmodul-Injektors (PlatformModule) und des Anwendungsmodul-Injektors (AppModule) auf.
In der Angular-Terminologie ist eine Plattform der Kontext, in dem Angular-Anwendungen ausgeführt werden. Die häufigste Plattform für Angular-Anwendungen ist ein Webbrowser, es kann sich aber auch um das Betriebssystem eines mobilen Geräts oder einen Webserver handeln.
Wenn eine Angular-Anwendung gestartet wird, wird eine Plattformschicht erstellt:
Die Plattform ist der Einstiegspunkt von Angular auf der Webseite. Jede Angular-Anwendung wird auf der Seite ausgeführt, und alle gemeinsamen Dienste sind aneinen Angular
Plattform, die hauptsächlich Funktionen wie das Erstellen und Zerstören von Modulinstanzen umfasst:
@Injectable() Klasse PlatformRef { exportieren // Den Injektor als Plattform-Injektorkonstruktor übergeben (private _injector: Injector) {} // Erstellen Sie eine Instanz von @NgModule für die angegebene Plattform für die Offline-Kompilierung. bootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>, Optionen?: BootstrapOptions): Promise<NgModuleRef<M>> {} // Mit dem angegebenen Laufzeit-Compiler eine Instanz von @NgModule für die angegebene Plattform erstellen bootstrapModule<M>( moduleType: Typ<M>, CompilerOptions: (CompilerOptions&BootstrapOptions)| Array<CompilerOptions&BootstrapOptions> = []): Promise<NgModuleRef<M>> {} // Registrieren Sie den Listener, der aufgerufen werden soll, wenn die Plattform zerstört wird onDestroy(callback: () => void): void {} // Holen Sie sich den Plattform-Injektor // Der Plattform-Injektor ist der übergeordnete Injektor für jede Angular-Anwendung auf der Seite und stellt den Singleton-Anbieter get inject() bereit: Injector {} // Zerstöre die aktuelle Angular-Plattform und alle Angular-Anwendungen auf der Seite, einschließlich der Zerstörung aller auf der Plattform registrierten Module und Listener destroy() {} }
Tatsächlich wird beim Start der Plattform (in der bootstrapModuleFactory
-Methode) ngZoneInjector
in ngZone.run
erstellt, sodass alle instanziierten Dienste in der Angular-Zone erstellt werden und sich ApplicationRef
(Angular-Anwendung, die auf der Seite ausgeführt wird) in der befindet Eckzone außen erstellt.
Beim Start im Browser wird die Browserplattform erstellt:
export const platformBrowser: (extraProviders?: StaticProvider[]) => PlatformRef = createPlatformFactory(platformCore, 'browser', INTERNAL_BROWSER_PLATFORM_PROVIDERS); // Unter anderem muss die platformCore-Plattform in jede andere Plattform exportiert werden. const platformCore = createPlatformFactory(null, 'core', _CORE_PLATFORM_PROVIDERS);
Beim Erstellen einer Plattform mithilfe einer Plattformfabrik (z. B. createPlatformFactory
oben) wird die Plattform der Seite verwendet wird implizit initialisiert:
export function createPlatformFactory( parentPlatformFactory: ((extraProviders?: StaticProvider[]) => PlatformRef)|null, name: string, Anbieter: StaticProvider[] = []): (extraProviders?: StaticProvider[]) => PlatformRef { const desc = `Plattform: ${name}`; const marker = new InjectionToken(desc); // DI token return (extraProviders: StaticProvider[] = []) => { let platform = getPlatform(); // Wenn die Plattform erstellt wurde, wird keine Verarbeitung durchgeführt if (!platform || platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) { if (parentPlatformFactory) { // Wenn eine übergeordnete Plattform vorhanden ist, verwenden Sie die übergeordnete Plattform direkt und aktualisieren Sie den entsprechenden Anbieter parentPlatformFactory( Providers.concat(extraProviders).concat({provide: marker, useValue: true})); } anders { const injectProviders: StaticProvider[] = Providers.concat(extraProviders).concat({provide: marker, useValue: true}, { bereitstellen: INJECTOR_SCOPE, useValue: 'Plattform' }); // Wenn keine übergeordnete Plattform vorhanden ist, erstellen Sie einen neuen Injektor und eine Plattform createPlatform(Injector.create({providers: injectProviders, name: desc})); } } return affirmPlatform(marker); }; }
Durch den obigen Prozess wissen wir, dass die Angular-Anwendung beim Erstellen der Plattform den Modulinjektor der Plattform ModuleInjector
, erstellt. Aus Injector
Definition im vorherigen Abschnitt können wir auch ersehen, dass NullInjector
der beste aller Injector ist:
export abstract class Injector { static NULL: Injector = new NullInjector(); }
Zusätzlich zum Plattformmodul-Injektor gibt es NullInjector()
. Unter dem Plattformmodul-Injektor befindet sich auch der Anwendungsmodul-Injektor.
Jede Anwendung verfügt über mindestens ein Angular-Modul. Das Root-Modul ist das Modul, das zum Starten dieser Anwendung verwendet wird:
@NgModule({ Anbieter: APPLICATION_MODULE_PROVIDERS }) Exportklasse ApplicationModule { // ApplicationRef benötigt den Bootstrap, um den Komponentenkonstruktor bereitzustellen (appRef: ApplicationRef) {} }
AppModule
Root-Anwendungsmodul wird von BrowserModule
erneut exportiert, und wenn wir mit dem new
Befehl der CLI eine neue Anwendung erstellen, wird sie automatisch in das Root AppModule
aufgenommen. Im Anwendungs-Root-Modul ist der Anbieter mit einem integrierten DI-Token verknüpft, der zum Konfigurieren des Root-Injektors für den Bootstrap verwendet wird.
Angular fügt außerdem ComponentFactoryResolver
zum Root-Modul-Injektor hinzu. Dieser Parser speichert entryComponents
-Fabrikenfamilie und ist daher für die dynamische Erstellung von Komponenten verantwortlich.
An dieser Stelle können wir einfach die hierarchische Beziehung der Modulinjektoren klären:
Die oberste Ebene des Modulinjektorbaums ist der Injektor des Anwendungsstammmoduls (AppModule), genannt Root.
Es gibt zwei Injektoren oberhalb von Root, einer ist der Plattformmodul-Injektor (PlatformModule) und der andere ist NullInjector()
.
Daher ist die Hierarchie der Modulinjektoren wie folgt:
In unserer tatsächlichen Anwendung sieht es wahrscheinlich so aus:
Angular DI verfügt über eine mehrschichtige Injektionsarchitektur, was bedeutet, dass untergeordnete Injektoren auch ihre eigenen Serviceinstanzen erstellen können.
Wie bereits erwähnt, gibt es in Angular zwei Injektorhierarchien, nämlich Modulinjektor und Elementinjektor.
Als Lazy-Loaded-Module in Angular weit verbreitet wurden, trat ein Problem auf: Das Abhängigkeitsinjektionssystem führte dazu, dass sich die Instanziierung von Lazy-Loaded-Modulen verdoppelte.
In diesem Fix wurde ein neues Design eingeführt: Der Injektor verwendet zwei parallele Bäume, einen für Elemente und einen für Module .
Angular erstellt Hostfabriken für alle entryComponents
, die die Stammansichten für alle anderen Komponenten sind.
Das bedeutet, dass jedes Mal, wenn wir eine dynamische Angular-Komponente erstellen, die Stammansicht ( RootData
) mit den Stammdaten ( RootView
) erstellt wird:
class ComponentFactory_ erweitert ComponentFactory<any>{ erstellen( Injector: Injector, projectableNodes?: any[][], rootSelectorOrNode?: string|any, ngModule?: NgModuleRef<any>): ComponentRef<any> { if (!ngModule) { throw new Error('ngModule sollte bereitgestellt werden'); } const viewDef = discoverDefinition(this.viewDefFactory); const ComponentNodeIndex = viewDef.nodes[0].element!.componentProvider!.nodeIndex; //Erstelle die Root-Ansicht mit Root-Daten const view = Services.createRootView( Injektor, projectableNodes ||. [], rootSelectorOrNode, viewDef, ngModule, EMPTY_CONTEXT); // Accessor für view.nodes const Component = asProviderData(view, ComponentNodeIndex).instance; if (rootSelectorOrNode) { view.renderer.setAttribute(asElementData(view, 0).renderElement, 'ng-version', VERSION.full); } //Eine Komponente erstellen return new ComponentRef_(view, new ViewRef_(view), Component); } }
Die Stammdaten ( RootData
) enthalten Verweise auf elInjector
und ngModule
:
function createRootData( elInjector: Injector, 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); zurückkehren { ngModule, Injektor: elInjektor, projectableNodes, selectorOrNode: rootSelectorOrNode, Desinfektionsmittel, rendererFactory, Renderer, errorHandler, }; }
Einführung des Elementinjektorbaums, da dieser Entwurf relativ einfach ist. Vermeiden Sie durch Ändern der Injektorhierarchie die Verschachtelung von Modul- und Komponenteninjektoren, was zu einer doppelten Instanziierung verzögert geladener Module führt. Da jeder Injektor nur einen übergeordneten Injektor hat und jede Auflösung genau einen Injektor finden muss, um Abhängigkeiten abzurufen.
In Angular ist eine Ansicht eine Darstellung einer Vorlage. Sie enthält verschiedene Arten von Knoten, darunter der Elementinjektor. Der Elementinjektor befindet sich auf diesem Knoten:
export interface ElementDef {. ... // Öffentliche Anbieter von DI, die in dieser Ansicht sichtbar sind publicProviders: {[tokenKey: string]: NodeDef}|null; // Identisch mit „visiblePublicProviders“, umfasst aber auch private Anbieter, die sich auf diesem Element befinden. allProviders: {[tokenKey: string]: NodeDef}|null; }
ElementInjector
ist standardmäßig leer, sofern es nicht im providers
-Attribut von @Directive()
oder @Component()
konfiguriert ist.
Wenn Angular einen Elementinjektor für ein verschachteltes HTML-Element erstellt, erbt es diesen entweder vom übergeordneten Elementinjektor oder weist den übergeordneten Elementinjektor direkt der Definition des untergeordneten Knotens zu.
Wenn ein Elementinjektor für ein untergeordnetes HTML-Element einen Anbieter hat, sollte dieser geerbt werden. Andernfalls besteht keine Notwendigkeit, einen separaten Injektor für die untergeordnete Komponente zu erstellen, und Abhängigkeiten können bei Bedarf direkt vom Injektor der übergeordneten Komponente aufgelöst werden.
Wo beginnen also Elementinjektoren und Modulinjektoren, parallele Bäume zu werden?
Wir wissen bereits, dass das Anwendungsstammmodul ( AppModule
) automatisch in das Stamm- AppModule
eingefügt wird, wenn mit dem new
Befehl der CLI eine neue Anwendung erstellt wird.
Wenn die Anwendung ( ApplicationRef
) startet ( bootstrap
), wird entryComponent
erstellt:
const compRef = ComponentFactory.create(Injector.NULL, [], selectorOrNode, ngModule);
Dieser Prozess erstellt die Root-Ansicht ( RootView
) unter Verwendung der Root-Daten ( RootData
) und der Root-Element-Injektor wird erstellt, wobei elInjector
Injector.NULL
ist.
Hier ist der Injektorbaum von Angular in einen Elementinjektorbaum und einen Modulinjektorbaum unterteilt, diese beiden parallelen Bäume.
Angular erstellt regelmäßig untergeordnete Injektoren. Immer wenn Angular eine Komponenteninstanz mit in @Component()
angegebenen providers
erstellt, wird auch ein neuer Subinjektor für die Instanz erstellt. Wenn zur Laufzeit ein neues NgModule
geladen wird, kann Angular mit seinem eigenen Anbieter einen Injektor dafür erstellen.
Submodul- und Komponenteninjektoren sind voneinander unabhängig und erstellen jeweils eine eigene Instanz für den bereitgestellten Dienst. Wenn Angular NgModule
oder Komponenteninstanz zerstört, werden auch diese Injektoren und die Serviceinstanzen in den Injektoren zerstört.
Oben haben wir zwei Arten von Injektorbäumen in Angular vorgestellt: Modulinjektorbaum und Elementinjektorbaum. Wie löst Angular das Problem bei der Bereitstellung von Abhängigkeiten?
Wenn in Angular Token aufgelöst werden, um Abhängigkeiten für Komponenten/Anweisungen zu erhalten, löst Angular diese in zwei Stufen auf:
ElementInjector
-Hierarchie (ihr übergeordnetes Element)ModuleInjector
-Hierarchie (ihr übergeordnetes Element).Der Prozess ist wie folgt (siehe Multi-Level)
.Injector – Auflösungsregeln):
Wenn eine Komponente eine Abhängigkeit deklariert, versucht Angular, diese Abhängigkeit mithilfe seines eigenen ElementInjector
zu erfüllen.
Wenn der Injektor einer Komponente keinen Anbieter hat, leitet er die Anfrage an den ElementInjector
seiner übergeordneten Komponente weiter.
Diese Anfragen werden so lange weitergeleitet, bis Angular einen Injektor findet, der die Anfrage verarbeiten kann, oder der Vorfahr ElementInjector
ausgeht.
Wenn Angular den Anbieter in keinem ElementInjector
finden kann, kehrt es zu dem Element zurück, von dem aus die Anforderung gestellt wurde, und sucht in ModuleInjector
-Hierarchie nach.
Wenn Angular den Anbieter immer noch nicht finden kann, wird eine Fehlermeldung ausgegeben.
Zu diesem Zweck führt Angular einen speziellen Merge-Injektor ein.
Der Merge-Injektor selbst hat keinen Wert, er ist lediglich eine Kombination aus Ansichts- und Elementdefinitionen.
Klasse Injector_ implementiert Injector { Konstruktor(private Ansicht: ViewData, private elDef: NodeDef|null) {} get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any { constallowPrivateServices = this.elDef ? (this.elDef.flags & NodeFlags.ComponentView) !== 0 : false; return Services.resolveDep( this.view, this.elDef,allowPrivateServices, {flags: DepFlags.None, token, tokenKey: tokenKey(token)}, notFoundValue); } }
Wenn Angular Abhängigkeiten auflöst, ist der Merge-Injektor die Brücke zwischen dem Element-Injektor-Baum und dem Modul-Injektor-Baum. Wenn Angular versucht, bestimmte Abhängigkeiten in einer Komponente oder Direktive aufzulösen, verwendet es den Merge-Injektor, um den Element-Injektor-Baum zu durchlaufen, und wechselt dann, wenn die Abhängigkeit nicht gefunden wird, zum Modul-Injektor-Baum, um die Abhängigkeit aufzulösen.
Klasse ViewContainerRef_ implementiert ViewContainerData { ... //Abfrage für den Injektor des übergeordneten Ansichtselements get parentInjector(): Injector { let view = this._view; let elDef = this._elDef.parent; while (!elDef && view) { elDef = viewParentEl(view); view = view.parent!; } return view ? new Injector_(view, elDef) : new Injector_(this._view, null); } }
Injektoren sind vererbbar. Das heißt, wenn der angegebene Injektor eine Abhängigkeit nicht auflösen kann, fordert er den übergeordneten Injektor auf, diese aufzulösen. Der spezifische Analysealgorithmus ist in resolveDep()
implementiert:
Exportfunktion „resolveDep(“ Ansicht: ViewData, elDef: NodeDef,allowPrivateServices: boolean, depDef: DepDef, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any { // // mod1 // / // el1 mod2 // / //el2 // // Wenn Sie el2.injector.get(token) anfordern, überprüfen Sie den ersten gefundenen Wert und geben Sie ihn in der folgenden Reihenfolge zurück: // - el2.injector.get(token, default) // - el1.injector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) -> Modul nicht prüfen // - mod2.injector.get(token, default) }
Wenn es sich um die Root AppComponent
Komponente einer Vorlage wie <child></child>
handelt, gibt es in Angular drei Ansichten:
<!-- HostView_AppComponent --> <meine-app></meine-app> <!-- View_AppComponent --> <Kind></Kind> <!-- View_ChildComponent --> Einige Inhalte
basieren auf dem Analyseprozess. Der Analysealgorithmus basiert auf der Ansichtshierarchie, wie in der Abbildung dargestellt:
Wenn einige Token in einer untergeordneten Komponente aufgelöst werden, wird Angular Folgendes tun:
Schauen Sie sich zunächst den Injektor des untergeordneten Elements an und überprüfen Sie elRef.element.allProviders|publicProviders
.
Anschließend durchlaufen Sie alle übergeordneten Ansichtselemente (1) und überprüfen den Anbieter im Elementinjektor.
Wenn das nächste übergeordnete Ansichtselement gleich null
(2) ist, kehren Sie zu startView
(3) zurück und überprüfen Sie startView.rootData.elnjector
(4).
Nur wenn das Token nicht gefunden wird, überprüfen Sie startView.rootData module.injector
(5).
Daraus folgt, dass Angular beim Durchlaufen von Komponenten zum Auflösen bestimmter Abhängigkeiten nach dem übergeordneten Element einer bestimmten Ansicht und nicht nach dem übergeordneten Element eines bestimmten Elements sucht. Das übergeordnete Element der Ansicht kann wie folgt abgerufen werden:
// Für Komponentenansichten ist dies das Hostelement // Für eingebettete Ansichten ist dies der Index des übergeordneten Knotens der enthaltenden Ansichtscontainer-Exportfunktion viewParentEl(view: ViewData): NodeDef| null { const parentView = view.parent; if (parentView) { return view.parentNodeDef !.parent; } anders { null zurückgeben; } }
In diesem Artikel wird hauptsächlich die hierarchische Struktur von Injektoren in Angular vorgestellt. In Angular gibt es zwei parallele Injektorbäume: den Modulinjektorbaum und den Elementinjektorbaum.
Die Einführung des Elementinjektorbaums dient hauptsächlich dazu, das Problem der doppelten Instanziierung von Modulen zu lösen, das durch das Parsen der Abhängigkeitsinjektion und das verzögerte Laden von Modulen verursacht wird. Nach der Einführung des Elementinjektorbaums wurde auch der Abhängigkeitsanalyseprozess von Angular angepasst. Er priorisiert die Suche nach Abhängigkeiten von Injektoren wie Elementinjektoren und Elementinjektoren der übergeordneten Ansicht. Nur wenn das Token nicht im Elementinjektor, dem Modulinjektor, gefunden werden kann werden Abhängigkeiten abgefragt.