Como uma estrutura de front-end projetada "para projetos de front-end em grande escala", o Angular possui muitos designs dignos de referência e aprendizado. Esta série é usada principalmente para estudar os princípios de implementação desses designs e funções. Este artigo se concentra no maior recurso do Angular - injeção de dependência e apresenta o design da injeção de dependência multinível no Angular. [Tutoriais relacionados recomendados: "Tutorial Angular"]
No artigo anterior, apresentamos Injectot
, o provedor Provider
e o mecanismo do injetor no Angular. Então, em aplicativos Angular, como os componentes e módulos compartilham dependências? O mesmo serviço pode ser instanciado várias vezes?
O processo de injeção de dependência de componentes e módulos é inseparável do design de injeção de dependência multinível do Angular.
Como dissemos anteriormente, o injetor em Angular é herdável e hierárquico.
Em Angular, existem duas hierarquias de injetores:
ModuleInjector
Module Injector: Configure ModuleInjector nesta hierarquia usando a anotação @NgModule()
ou @Injectable()
ModuleInjector
ElementInjector
Injector: criemódulos implicitamente em cada elemento DOM Ambos os injetores e injetores de elemento são estruturados em árvore, mas suas hierarquias não são exatamente as mesmas.
A estrutura hierárquica do injetor de módulo não está apenas relacionada ao design do módulo na aplicação, mas também possui a estrutura hierárquica do injetor de módulo de plataforma (PlatformModule) e do injetor de módulo de aplicação (AppModule).
Na terminologia Angular, uma plataforma é o contexto no qual os aplicativos Angular são executados. A plataforma mais comum para aplicações Angular é um navegador web, mas também pode ser um sistema operacional de dispositivo móvel ou um servidor web.
Quando um aplicativo Angular é iniciado, ele cria uma camada de plataforma:
a plataforma é o ponto de entrada do Angular na página da web. Cada página possui apenas uma plataforma. Cada aplicativo Angular é executado na página e todos os serviços comuns estão vinculados aum Angular
Platform, incluindo principalmente funções como criar instâncias de módulos e destruí-las:
@Injectable() classe de exportação PlatformRef { // Passa o injetor como o construtor do injetor de plataforma(private _injector: Injector) {} // Cria uma instância de @NgModule para a plataforma fornecida para compilação offline bootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>, options?: BootstrapOptions): Promessa<NgModuleRef<M>> {} // Usando o compilador de tempo de execução fornecido, crie uma instância de @NgModule para a plataforma especificada bootstrapModule<M>( moduleType: Tipo<M>, compilerOptions: (CompilerOptions&BootstrapOptions)| Array<CompilerOptions&BootstrapOptions> = []): Promessa<NgModuleRef<M>> {} // Registra o listener a ser chamado ao destruir a plataforma onDestroy(callback: () => void): void {} // Obtenha o injetor de plataforma // O injetor de plataforma é o injetor pai para cada aplicativo Angular na página e fornece o provedor singleton get injector(): Injector {} // Destrua a plataforma Angular atual e todas as aplicações Angular na página, incluindo a destruição de todos os módulos e ouvintes registrados na plataforma destroy() {} }
Na verdade, quando a plataforma é iniciada (no método bootstrapModuleFactory
), ngZoneInjector
é criado em ngZone.run
para que todos os serviços instanciados sejam criados na zona Angular, e ApplicationRef
(aplicativo Angular em execução na página) ficará na zona Angular. Zona angular Criada externamente.
Quando iniciado no navegador, a plataforma do navegador é criada:
export const platformBrowser: (extraProviders?: StaticProvider[]) => PlatformRef = createPlatformFactory(platformCore, 'navegador', INTERNAL_BROWSER_PLATFORM_PROVIDERS); // Entre eles, a plataforma platformCore deve ser incluída em qualquer outra plataforma export const platformCore = createPlatformFactory(null, 'core', _CORE_PLATFORM_PROVIDERS
Ao criar uma plataforma usando uma fábrica de plataforma (como createPlatformFactory
acima), a plataforma da página)
será inicializado implicitamente:
função de exportação createPlatformFactory( parentPlatformFactory: ((extraProviders?: StaticProvider[]) => PlatformRef)|null, nome: string, provedores: StaticProvider[] = []): (extraProviders?: StaticProvider[]) => PlatformRef { const desc = `Plataforma: ${nome}`; marcador const = new InjectionToken(desc); // retorno do token DI (extraProviders: StaticProvider[] = []) => { deixe plataforma = getPlatform(); // Se a plataforma foi criada, nenhum processamento será realizado if (!platform || platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) { if (parentPlatformFactory) { // Se houver uma plataforma pai, use a plataforma pai diretamente e atualize o provedor correspondente parentPlatformFactory( provedores.concat(extraProviders).concat({fornecer: marcador, useValue: true})); } outro { const injectedProviders: StaticProvider[] = provedores.concat(extraProviders).concat({fornecer: marcador, useValue: true}, { fornecer: INJECTOR_SCOPE, useValue: 'plataforma' }); // Se não houver plataforma pai, crie um novo injetor e crie uma plataforma createPlatform(Injector.create({providers: injectedProviders, name: desc})); } } return assertPlatform(marcador); }; }
Através do processo acima, sabemos que quando a aplicação Angular cria a plataforma, ela cria o módulo injetor da plataforma ModuleInjector
. Também podemos ver Injector
na seção anterior que NullInjector
é o topo de todos os injetores:
export abstract class Injector { estático NULL: Injetor = new NullInjector(); }
Portanto, no topo do injetor do módulo da plataforma, existe NullInjector()
. Abaixo do injetor do módulo da plataforma, encontra-se também o injetor do módulo de aplicação.
Cada aplicativo possui pelo menos um módulo Angular O módulo raiz é o módulo usado para iniciar este aplicativo:
@NgModule({ provedores: APPLICATION_MODULE_PROVIDERS }) classe de exportação ApplicationModule { // ApplicationRef requer que o bootstrap forneça o construtor do componente(appRef: ApplicationRef) {} }
AppModule
é reexportado pelo BrowserModule
e, quando criamos um novo aplicativo usando o new
comando da CLI, ele é automaticamente incluído no AppModule
raiz. No módulo raiz do aplicativo, o provedor está associado a um token DI integrado que é usado para configurar o injetor raiz para o bootstrap.
Angular também adiciona ComponentFactoryResolver
ao injetor do módulo raiz. Este analisador armazena entryComponents
, portanto é responsável pela criação dinâmica de componentes.
Neste ponto, podemos simplesmente resolver o relacionamento hierárquico dos injetores de módulo:
o nível superior da árvore do injetor de módulo é o injetor do módulo raiz do aplicativo (AppModule), chamado root.
Existem dois injetores acima do root, um é o injetor do módulo de plataforma (PlatformModule) e o outro é NullInjector()
.
Portanto, a hierarquia do injetor de módulo é a seguinte:
Em nossa aplicação real, é provável que seja assim:
Angular DI possui uma arquitetura de injeção em camadas, o que significa que os injetores de nível inferior também podem criar suas próprias instâncias de serviço.
Como mencionado anteriormente, existem duas hierarquias de injetores no Angular, a saber: injetor de módulo e injetor de elemento.
Quando módulos de carregamento lento começaram a ser amplamente utilizados no Angular, surgiu um problema: o sistema de injeção de dependência fez com que a instanciação de módulos de carregamento lento dobrasse.
Nesta correção foi introduzido um novo design: o injetor utiliza duas árvores paralelas, uma para elementos e outra para módulos .
Angular cria fábricas de host para todos entryComponents
, que são as visualizações raiz de todos os outros componentes.
Isso significa que toda vez que criarmos um componente Angular dinâmico, a visualização raiz ( RootData
) será criada com os dados raiz ( RootView
):
class ComponentFactory_ extends ComponentFactory<any>{ criar( injetor: Injetor, projectableNodes?: qualquer[][], rootSelectorOrNode?: string|qualquer, ngModule?: NgModuleRef<qualquer>): ComponentRef<qualquer> { if (!ngModule) { throw new Error('ngModule deve ser fornecido'); } const viewDef = resolveDefinition(this.viewDefFactory); const componentNodeIndex = viewDef.nodes[0].element!.componentProvider!.nodeIndex; //Crie a visualização raiz usando dados raiz const view = Services.createRootView( injetor, projectableNodes || [], rootSelectorOrNode, viewDef, ngModule, EMPTY_CONTEXT); // Acessador para view.nodes const component = asProviderData(view, componentNodeIndex).instance; if (rootSelectorOrNode) { view.renderer.setAttribute(asElementData(view, 0).renderElement, 'ng-version', VERSION.full); } //Cria um componente return new ComponentRef_(view, new ViewRef_(view), component); } }
Os dados raiz ( RootData
) contêm referências aos injetores elInjector
e ngModule
:
function createRootData( elInjector: Injetor, ngModule: NgModuleRef<qualquer>, rendererFactory: RendererFactory2, projectableNodes: qualquer[][], rootSelectorOrNode: qualquer): RootData { const sanitizer = ngModule.injector.get(Sanitizer); const errorHandler = ngModule.injector.get(ErrorHandler); const renderizador = rendererFactory.createRenderer(null, null); retornar { ngMódulo, injetor: elInjector, projetáveisNodes, selectorOrNode: rootSelectorOrNode, desinfetante, renderizadorFactory, renderizador, manipulador de erros, }; }
Apresentando a árvore injetora de elementos porque esse design é relativamente simples. Ao alterar a hierarquia do injetor, evite intercalar injetores de módulos e componentes, resultando em instanciação dupla de módulos carregados lentamente. Porque cada injetor tem apenas um pai e cada resolução deve encontrar exatamente um injetor para recuperar dependências.
Em Angular, uma visualização é uma representação de um modelo. Ela contém diferentes tipos de nós, entre os quais está o nó do elemento. O injetor de elemento está localizado neste nó:
export interface ElementDef {. ... // Provedores públicos de DI visíveis nesta visualização publicProviders: {[tokenKey: string]: NodeDef}|null; // O mesmo que visívelPublicProviders, mas também inclui provedores privados localizados neste elemento allProviders: {[tokenKey: string]: NodeDef}|null; }
ElementInjector
está vazio por padrão, a menos que seja configurado no atributo providers
de @Directive()
ou @Component()
.
Quando Angular cria um injetor de elemento para um elemento HTML aninhado, ele o herda do injetor de elemento pai ou atribui o injetor de elemento pai diretamente à definição do nó filho.
Se um injetor de elemento em um elemento HTML filho tiver um provedor, ele deverá ser herdado. Caso contrário, não há necessidade de criar um injetor separado para o componente filho e as dependências podem ser resolvidas diretamente a partir do injetor pai, se necessário.
Então, onde os injetores de elemento e injetores de módulo começam a se tornar árvores paralelas?
Já sabemos que o módulo raiz do aplicativo ( AppModule
) será incluído automaticamente no AppModule
raiz ao criar um novo aplicativo usando o new
comando da CLI.
Quando o aplicativo ( ApplicationRef
) é iniciado ( bootstrap
), entryComponent
é criado:
const compRef = componentFactory.create(Injector.NULL, [], selectorOrNode, ngModule
Este processo cria a visualização raiz ( RootView
) usando os dados raiz ( RootData
) e o injetor do elemento raiz será criado, onde elInjector
é Injector.NULL
.
Aqui, a árvore injetora do Angular é dividida em árvore injetora de elemento e árvore injetora de módulo, essas duas árvores paralelas.
O Angular criará injetores subordinados regularmente. Sempre que o Angular criar uma instância de componente providers
especificados em @Component()
, ele também criará um novo subinjetor para a instância. Da mesma forma, quando um novo NgModule
é carregado em tempo de execução, Angular pode criar um injetor para ele com seu próprio provedor.
Os injetores de submódulos e componentes são independentes entre si e cada um cria sua própria instância para o serviço fornecido. Quando Angular destrói NgModule
ou instância de componente, ele também destrói esses injetores e as instâncias de serviço nos injetores.
Acima, introduzimos dois tipos de árvores injetoras no Angular: árvore injetora de módulo e árvore injetora de elemento. Então, como o Angular resolve isso ao fornecer dependências?
No Angular, ao resolver tokens para obter dependências para componentes/instruções, o Angular resolve isso em dois estágios:
ElementInjector
(seu pai)ModuleInjector
(seu pai).O processo é o seguinte (consulte Multi-Level). Injector - Regras de Resolução):
Quando um componente declara uma dependência, Angular tentará satisfazer essa dependência usando seu próprio ElementInjector
.
Se o injetor de um componente não tiver um provedor, ele passará a solicitação para o ElementInjector
do componente pai.
Essas solicitações continuarão a ser encaminhadas até que o Angular encontre um injetor que possa lidar com a solicitação ou fique sem o ancestral ElementInjector
.
Se o Angular não conseguir encontrar o provedor em nenhum ElementInjector
, ele retornará ao elemento a partir do qual a solicitação foi feita e procurará ModuleInjector
.
Se o Angular ainda não conseguir encontrar o provedor, ocorrerá um erro.
Para este propósito, Angular introduz um injetor de mesclagem especial.
O merge injector em si não tem valor, é apenas uma combinação de definições de visualização e elemento.
class Injector_ implementa Injector { construtor (visualização privada: ViewData, elDef privado: NodeDef | nulo) {} get (token: qualquer, notFoundValue: qualquer = Injetor.THROW_IF_NOT_FOUND): qualquer { const permitirPrivateServices = this.elDef? (this.elDef.flags & NodeFlags.ComponentView)!== 0: falso; retornar Serviços.resolveDep( this.view, this.elDef, permitirPrivateServices, {flags: DepFlags.None, token, tokenKey: tokenKey(token)}, notFoundValue); } }
Quando o Angular resolve dependências, o injetor de mesclagem é a ponte entre a árvore do injetor de elementos e a árvore do injetor de módulo. Quando o Angular tenta resolver certas dependências em um componente ou diretiva, ele usa o injetor de mesclagem para percorrer a árvore do injetor de elementos e então, se a dependência não for encontrada, muda para a árvore do injetor de módulo para resolver a dependência.
classe ViewContainerRef_ implementa ViewContainerData { ... //Consulta para o injetor do elemento de visualização pai get parentInjector(): Injector { deixe visualizar = this._view; deixe elDef = this._elDef.parent; enquanto (!elDef && visualizar) { elDef = viewParentEl(ver); visualizar = visualizar.parent!; } retornar visualização? novo Injetor_(view, elDef): novo Injector_(this._view, null); } }Os injetores
são herdáveis, o que significa que se o injetor especificado não puder resolver uma dependência, ele solicitará ao injetor pai que a resolva. O algoritmo de análise específico é implementado no método resolveDep()
:
função de exportação resolveDep( visualizar: ViewData, elDef: NodeDef, permitirPrivateServices: booleano, depDef: DepDef, notFoundValue: qualquer = Injetor.THROW_IF_NOT_FOUND): qualquer { // //mod1 // / // el1 mod2 /// //el2 // // Ao solicitar el2.injector.get(token), verifique e retorne o primeiro valor encontrado na seguinte ordem: // - el2.injector.get(token, padrão) // - el1.injector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) -> não verifica o módulo // - mod2.injector.get(token, padrão) }
Se for o componente AppComponent
raiz de um modelo como <child></child>
, então haverá três visualizações em Angular:
<!-- HostView_AppComponent --> <meu-aplicativo></meu-aplicativo> <!-- View_AppComponent --> <filho></filho> <!-- View_ChildComponent --> Parte do conteúdo
depende do processo de análise. O algoritmo de análise será baseado na hierarquia de visualização, conforme mostrado na figura:
Se alguns tokens forem resolvidos em um componente filho, o Angular irá:
primeiro olhar para o injetor do elemento filho, verificando elRef.element.allProviders|publicProviders
.
Em seguida, itere todos os elementos da visualização pai (1) e verifique o provedor no injetor de elemento.
Se o próximo elemento da visualização pai for igual a null
(2), retorne para startView
(3) e verifique startView.rootData.elnjector
(4).
Somente se o token não for encontrado, verifique startView.rootData module.injector
(5).
Segue-se que Angular, ao percorrer componentes para resolver certas dependências, irá procurar o elemento pai de uma visão específica em vez do elemento pai de um elemento específico. O elemento pai da visualização pode ser obtido via:
// Para visualizações de componentes, este é o elemento host // Para visualizações incorporadas, este é o índice do nó pai do contêiner de visualização que contém a função de exportação viewParentEl(view: ViewData): NodeDef| nulo { const parentView = view.parent; if (parentView) { retornar view.parentNodeDef!.parent; } outro { retornar nulo; } }
Este artigo apresenta principalmente a estrutura hierárquica de injetores em Angular. Existem duas árvores de injetores paralelas em Angular: árvore de injetores de módulo e árvore de injetores de elemento.
A introdução da árvore injetora de elementos visa principalmente resolver o problema de instanciação dupla de módulos causada pela análise de injeção de dependência e carregamento lento de módulos. Após a introdução da árvore do injetor de elementos, o processo de análise de dependências do Angular também foi ajustado. Ele prioriza a busca de dependências de injetores, como injetores de elementos e injetores de elementos de visualização pai, somente quando o token não pode ser encontrado no injetor de elementos, o injetor de módulo. serão consultadas dependências.