Como marco front-end diseñado "para proyectos front-end a gran escala", Angular en realidad tiene muchos diseños dignos de referencia y aprendizaje. Esta serie se utiliza principalmente para estudiar los principios de implementación de estos diseños y funciones. Este artículo se centra en la característica más importante de Angular: la inyección de dependencia y presenta el diseño de inyección de dependencia multinivel en Angular. [Tutoriales relacionados recomendados: "Tutorial de Angular"]
En el artículo anterior, presentamos Injectot
, el Provider
y el mecanismo del inyector en Angular. Entonces, en las aplicaciones Angular, ¿cómo comparten dependencias los componentes y módulos? ¿Se pueden crear instancias del mismo servicio varias veces?
El proceso de inyección de dependencia de componentes y módulos es inseparable del diseño de inyección de dependencia multinivel de Angular.
Como dijimos anteriormente, el inyector en Angular es heredable y jerárquico.
En Angular, hay dos jerarquías de inyectores:
ModuleInjector
Inyector de módulo: configure ModuleInjector en esta jerarquía usando la anotación @NgModule()
o @Injectable()
ModuleInjector
ElementInjector
: creemódulos implícitamente en cada elemento DOM. Tanto los inyectores como los inyectores de elementos tienen una estructura de árbol. pero sus jerarquías no son exactamente las mismas.
La estructura jerárquica del inyector de módulo no solo está relacionada con el diseño del módulo en la aplicación, sino que también tiene la estructura jerárquica del inyector del módulo de plataforma (PlatformModule) y del inyector del módulo de aplicación (AppModule).
En terminología de Angular, una plataforma es el contexto en el que se ejecutan las aplicaciones de Angular. La plataforma más común para aplicaciones Angular es un navegador web, pero también puede ser el sistema operativo de un dispositivo móvil o un servidor web.
Cuando se inicia una aplicación Angular, se creará una capa de plataforma:
la plataforma es el punto de entrada de Angular en la página web. Cada página tiene solo una plataforma. Cada aplicación Angular se ejecuta en la página, y todos los servicios comunes están vinculados aun Angular
Plataforma, que incluye principalmente funciones como crear instancias de módulos y destruirlas:
@Injectable() exportar clase PlatformRef { // Pasar el inyector como constructor del inyector de plataforma (private _injector: Injector) {} // Crea una instancia de @NgModule para la plataforma dada para la compilación fuera de línea bootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>, options?: BootstrapOptions): Promesa<NgModuleRef<M>> {} // Usando el compilador de tiempo de ejecución dado, crea una instancia de @NgModule para la plataforma dada bootstrapModule<M>( tipo de módulo: tipo <M>, Opciones de compilador: (Opciones de compilador y Opciones de Bootstrap) | Array<CompilerOptions&BootstrapOptions> = []): Promesa<NgModuleRef<M>> {} // Registre el oyente que se llamará al destruir la plataforma onDestroy(callback: () => void): void {} // Obtener el inyector de plataforma // El inyector de plataforma es el inyector principal para cada aplicación Angular en la página y proporciona el proveedor singleton get injector(): Injector {} // Destruye la plataforma Angular actual y todas las aplicaciones Angular en la página, incluida la destrucción de todos los módulos y oyentes registrados en la plataforma destroy() {} }
De hecho, cuando se inicia la plataforma (en el método bootstrapModuleFactory
), se crea ngZoneInjector
en ngZone.run
para que todos los servicios instanciados se creen en la zona Angular, y ApplicationRef
(aplicación Angular que se ejecuta en la página) estará en la Zona angular creada en el exterior.
Cuando se inicia en el navegador, se crea la plataforma del navegador:
export const platformBrowser: (extraProviders?: StaticProvider[]) => PlatformRef = createPlatformFactory(platformCore, 'navegador', INTERNAL_BROWSER_PLATFORM_PROVIDERS); // Entre ellos, la plataforma platformCore debe incluirse en cualquier otra plataforma export const platformCore = createPlatformFactory(null, 'core', _CORE_PLATFORM_PROVIDERS
Al crear una plataforma utilizando una fábrica de plataformas (como createPlatformFactory
arriba), la plataforma de la página); se inicializará implícitamente:
función de exportación createPlatformFactory( parentPlatformFactory: ((extraProviders?: StaticProvider[]) => PlatformRef)|null, nombre: cadena, proveedores: StaticProvider[] = []): (proveedores adicionales?: StaticProvider[]) => PlatformRef { const desc = `Plataforma: ${nombre}`; marcador constante = new injectionToken(desc); // retorno del token DI (extraProviders: StaticProvider[] = []) => { let plataforma = getPlatform(); // Si se ha creado la plataforma, no se realiza ningún procesamiento if (!platform || platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) { si (parentPlatformFactory) { // Si hay una plataforma principal, use la plataforma principal directamente y actualice el proveedor correspondiente parentPlatformFactory( proveedores.concat(extraProviders).concat({proporcionar: marcador, useValue: verdadero})); } demás { const injectionProviders: StaticProvider[] = proveedores.concat(extraProviders).concat({proporcionar: marcador, useValue: verdadero}, { proporcionar: INYECTOR_SCOPE, useValue: 'plataforma' }); // Si no hay una plataforma principal, crea un nuevo inyector y crea una plataforma createPlatform(Injector.create({providers: injectionProviders, name: desc})); } } return afirmarPlataforma(marcador); }; }
A través del proceso anterior, sabemos que cuando la aplicación Angular crea la plataforma, crea el inyector de módulo de la plataforma ModuleInjector
. También podemos ver en Injector
en la sección anterior que NullInjector
es el superior de todos los inyectores:
export abstract class Injector { NULL estático: Inyector = nuevo NullInjector(); }
Entonces, encima del inyector del módulo de plataforma, está NullInjector()
. Debajo del inyector del módulo de plataforma, también se encuentra el inyector del módulo de aplicación.
Cada aplicación tiene al menos un módulo Angular. El módulo raíz es el módulo utilizado para iniciar esta aplicación:
@NgModule({ proveedores: APPLICATION_MODULE_PROVIDERS }) exportar clase ApplicationModule { // ApplicationRef requiere que el arranque proporcione el componente constructor(appRef: ApplicationRef) {} }
BrowserModule
reexporta AppModule
y cuando creamos una nueva aplicación usando el new
comando de la CLI, se incluye automáticamente en el AppModule
raíz. En el módulo raíz de la aplicación, el proveedor está asociado con un token DI integrado que se utiliza para configurar el inyector raíz para el arranque.
Angular también agrega ComponentFactoryResolver
al inyector del módulo raíz. Este analizador almacena entryComponents
, por lo que es responsable de crear componentes dinámicamente.
En este punto, podemos simplemente ordenar la relación jerárquica de los inyectores de módulos:
el nivel superior del árbol de inyectores de módulos es el inyector del módulo raíz de la aplicación (AppModule), llamado raíz.
Hay dos inyectores encima de la raíz, uno es el inyector del módulo de plataforma (PlatformModule) y el otro es NullInjector()
.
Por lo tanto, la jerarquía del inyector del módulo es la siguiente:
En nuestra aplicación real, es probable que sea así:
Angular DI tiene una arquitectura de inyección en capas, lo que significa que los inyectores de nivel inferior también pueden crear sus propias instancias de servicio.
Como se mencionó anteriormente, hay dos jerarquías de inyectores en Angular, a saber, inyector de módulo y inyector de elementos.
Cuando los módulos con carga diferida comenzaron a usarse ampliamente en Angular, surgió un problema: el sistema de inyección de dependencia provocó que se duplicara la creación de instancias de módulos con carga diferida.
En esta solución, se introdujo un nuevo diseño: el inyector utiliza dos árboles paralelos, uno para elementos y otro para módulos .
Angular crea fábricas de host para todos entryComponents
, que son las vistas raíz de todos los demás componentes.
Esto significa que cada vez que creamos un componente Angular dinámico, la vista raíz ( RootData
) se creará con los datos raíz ( RootView
):
clase ComponentFactory_ extends ComponentFactory<any>{ crear( inyector: Inyector, projectableNodes?: cualquiera[][], rootSelectorOrNode?: cadena|cualquiera, ngModule?: NgModuleRef<cualquiera>): ComponentRef<cualquiera> { si (!ngModule) { throw new Error('se debe proporcionar ngModule'); } const viewDef = resolveDefinition(this.viewDefFactory); const componenteNodeIndex = viewDef.nodes[0].element!.componentProvider!.nodeIndex; //Crea la vista raíz usando datos raíz const view = Services.createRootView( inyector, projectableNodes || [], rootSelectorOrNode, viewDef, ngModule, EMPTY_CONTEXT); // Accesor para view.nodes const componente = asProviderData(view, componenteNodeIndex).instance; si (rootSelectorOrNode) { view.renderer.setAttribute(asElementData(view, 0).renderElement, 'ng-version', VERSION.full); } //Crear un componente return new ComponentRef_(view, new ViewRef_(view), componente); } }
Los datos raíz ( RootData
) contienen referencias a elInjector
y ngModule
:
función createRootData( elInjector: Inyector, ngModule: NgModuleRef<cualquier>, rendererFactory: RendererFactory2, projectableNodes: cualquiera[][], rootSelectorOrNode: cualquiera): RootData { desinfectante const = ngModule.injector.get(Sanitizador); const errorHandler = ngModule.injector.get(ErrorHandler); renderizador constante = rendererFactory.createRenderer (nulo, nulo); devolver { módulo ng, inyector: elInjector, nodos proyectables, selectorOrNode: raízSelectorOrNode, desinfectante, fábrica de renderizadores, renderizador, controlador de errores, }; }
Presentamos el árbol del inyector de elementos porque este diseño es relativamente simple. Al cambiar la jerarquía de los inyectores, se evita entrelazar los inyectores de módulos y componentes, lo que da como resultado una doble creación de instancias de módulos con carga diferida. Porque cada inyector tiene solo un padre y cada resolución debe encontrar exactamente un inyector para recuperar las dependencias.
En Angular, una vista es una representación de una plantilla. Contiene diferentes tipos de nodos, entre los cuales se encuentra el nodo de elemento. El inyector de elementos se encuentra en este nodo:
export interface ElementDef {. ... // Proveedores públicos de DI visibles en esta vista publicProviders: {[tokenKey: string]: NodeDef}|null; // Igual que visiblePublicProviders, pero también incluye proveedores privados ubicados en este elemento allProviders: {[tokenKey: string]: NodeDef}|null; }
ElementInjector
está vacío de forma predeterminada a menos que esté configurado en el atributo providers
de @Directive()
o @Component()
.
Cuando Angular crea un inyector de elementos para un elemento HTML anidado, lo hereda del inyector de elementos principales o asigna el inyector de elementos principales directamente a la definición del nodo secundario.
Si un inyector de elementos en un elemento HTML secundario tiene un proveedor, debe heredarse. De lo contrario, no es necesario crear un inyector separado para el componente secundario y las dependencias se pueden resolver directamente desde el inyector principal si es necesario.
Entonces, ¿dónde comienzan a convertirse los inyectores de elementos y los inyectores de módulos en árboles paralelos?
Ya sabemos que el módulo raíz de la aplicación ( AppModule
) se incluirá automáticamente en el AppModule
raíz al crear una nueva aplicación utilizando el new
comando de la CLI.
Cuando se inicia la aplicación ( ApplicationRef
) ( bootstrap
), se crea entryComponent
:
const compRef = componenteFactory.create(Injector.NULL, [], selectorOrNode, ngModule
Este proceso crea la vista raíz ( RootView
) utilizando los datos raíz ( RootData
), y se creará el inyector del elemento raíz, donde elInjector
es Injector.NULL
.
Aquí, el árbol de inyectores de Angular se divide en árbol de inyectores de elementos y árbol de inyectores de módulos, estos dos árboles paralelos.
Angular creará inyectores subordinados con regularidad. Siempre que Angular cree una instancia de componente providers
especificados en @Component()
, también creará un nuevo subinyector para la instancia. De manera similar, cuando se carga un nuevo NgModule
en tiempo de ejecución, Angular puede crear un inyector con su propio proveedor.
Los inyectores de submódulos y componentes son independientes entre sí y cada uno crea su propia instancia para el servicio proporcionado. Cuando Angular destruye NgModule
o una instancia de componente, también destruye estos inyectores y esas instancias de servicio en los inyectores.
Arriba, introdujimos dos tipos de árboles de inyectores en Angular: árbol de inyectores de módulos y árbol de inyectores de elementos. Entonces, ¿cómo lo resuelve Angular al proporcionar dependencias?
En Angular, al resolver tokens para obtener dependencias para componentes/instrucciones, Angular lo resuelve en dos etapas:
ElementInjector
(su padre)ModuleInjector
(su padre).El proceso es el siguiente (consulte Multinivel). Inyector - Reglas de resolución):
cuando un componente declara una dependencia, Angular intentará satisfacer esa dependencia utilizando su propio ElementInjector
.
Si el inyector de un componente carece de un proveedor, pasará la solicitud al ElementInjector
de su componente principal.
Estas solicitudes continuarán reenviándose hasta que Angular encuentre un inyector que pueda manejar la solicitud o se quede sin ElementInjector
ancestros.
Si Angular no puede encontrar el proveedor en ningún ElementInjector
, volverá al elemento desde el que se realizó la solicitud y buscará ModuleInjector
.
Si Angular aún no puede encontrar el proveedor, generará un error.
Para ello, Angular introduce un inyector de fusión especial.
El inyector de combinación en sí no tiene valor, es solo una combinación de definiciones de vista y elementos.
clase Inyector_ implementa Inyector { constructor(vista privada: ViewData, elDef privado: NodeDef|null) {} get(token: cualquiera, notFoundValue: cualquiera = Injector.THROW_IF_NOT_FOUND): cualquiera { const permitirServiciosPrivados = this.elDef? (this.elDef.flags y NodeFlags.ComponentView)!== 0: falso; devolver Servicios.resolveDep( this.view, this.elDef, enablePrivateServices, {banderas: DepFlags.None, token, tokenKey: tokenKey(token)}, notFoundValue); } }
Cuando Angular resuelve dependencias, el inyector de fusión es el puente entre el árbol del inyector de elementos y el árbol del inyector de módulos. Cuando Angular intenta resolver ciertas dependencias en un componente o directiva, usa el inyector de combinación para recorrer el árbol del inyector de elementos y luego, si no se encuentra la dependencia, cambia al árbol del inyector de módulos para resolver la dependencia.
clase ViewContainerRef_ implementa ViewContainerData { ... // Consulta para el inyector del elemento de vista principal get parentInjector(): Injector { dejar ver = this._view; let elDef = this._elDef.parent; mientras (!elDef && ver) { elDef = viewParentEl(vista); ver = ver.padre!; } ¿Devolver vista? nuevo Injector_(view, elDef): new Injector_(this._view, null); } }Los inyectores
son heredables, lo que significa que si el inyector especificado no puede resolver una dependencia, le pedirá al inyector principal que la resuelva. El algoritmo de análisis específico se implementa en resolveDep()
:
función de exportación resolveDep( ver: ViewData, elDef: NodeDef, enablePrivateServices: booleano, depDef: DepDef, notFoundValue: cualquiera = Injector.THROW_IF_NOT_FOUND): cualquiera { // // mod1 // / // el1 mod2 // / //el2 // // Al solicitar el2.injector.get(token), verifica y devuelve el primer valor encontrado en el siguiente orden: // - el2.injector.get(token, predeterminado) // - el1.injector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) -> no verificar el módulo // - mod2.injector.get(token, predeterminado) }
Si es el componente raíz AppComponent
de una plantilla como <child></child>
, entonces habrá tres vistas en Angular:
<!-- HostView_AppComponent --> <mi-aplicación></mi-aplicación> <!-- View_AppComponent --> <niño></niño> <!-- View_ChildComponent --> Parte del contenido
depende del proceso de análisis. El algoritmo de análisis se basará en la jerarquía de vistas, como se muestra en la figura:
Si algunos tokens se resuelven en un componente secundario, Angular:
primero mirará el inyector de elementos secundarios y verificará elRef.element.allProviders|publicProviders
.
Luego, repita todos los elementos de la vista principal (1) y verifique el proveedor en el inyector de elementos.
Si el siguiente elemento de la vista principal es igual a null
(2), regrese a startView
(3) y verifique startView.rootData.elnjector
(4).
Solo si no se encuentra el token, verifique startView.rootData module.injector
(5).
De ello se deduce que Angular, al atravesar componentes para resolver ciertas dependencias, buscará el elemento principal de una vista específica en lugar del elemento principal de un elemento específico. El elemento principal de la vista se puede obtener a través de:
// Para vistas de componentes, este es el elemento host // Para vistas incrustadas, este es el índice del nodo principal de la función de exportación del contenedor de vista que lo contiene viewParentEl(view: ViewData): NodeDef| nulo { const parentView = ver.parent; si (parentView) { devolver view.parentNodeDef !.parent; } demás { devolver nulo; } }
Este artículo presenta principalmente la estructura jerárquica de los inyectores en Angular. Hay dos árboles de inyectores paralelos en Angular: árbol de inyectores de módulos y árbol de inyectores de elementos.
La introducción del árbol de inyectores de elementos es principalmente para resolver el problema de la doble creación de instancias de módulos causada por el análisis de inyección de dependencia y la carga diferida de módulos. Después de la introducción del árbol de inyectores de elementos, el proceso de análisis de dependencias de Angular también se ha ajustado. Prioriza la búsqueda de dependencias de inyectores, como los inyectores de elementos y los inyectores de elementos de vista principal, solo cuando el token no se puede encontrar en el inyector de elementos, el inyector de módulo. Se consultarán las dependencias en.