Будучи интерфейсной платформой, разработанной «для крупномасштабных интерфейсных проектов», Angular на самом деле имеет множество проектов, заслуживающих внимания и изучения. Эта серия в основном используется для изучения принципов реализации этих проектов и функций. В этой статье основное внимание уделяется самой важной особенности Angular — внедрению зависимостей, а также представлена конструкция многоуровневого внедрения зависимостей в Angular. [Рекомендуемые соответствующие учебные пособия: «Учебное пособие по Angular»]
В предыдущей статье мы познакомили вас с инжектором Injectot
, поставщиком Provider
и механизмом инжектора в Angular. Итак, как в приложениях Angular компоненты и модули разделяют зависимости. Можно ли создавать экземпляры одной и той же службы несколько раз?
Процесс внедрения зависимостей компонентов и модулей неотделим от многоуровневого внедрения зависимостей Angular. Давайте посмотрим.
Как мы уже говорили ранее, инжектор в Angular является наследуемым и иерархическим.
В Angular существует две иерархии инжекторов:
ModuleInjector
Module Injector: настройте ModuleInjector в этой иерархии с помощью аннотации @NgModule()
или @Injectable()
ModuleInjector
ElementInjector
Injector: неявно создайтемодули для каждого элемента DOM. И инжекторы, и инжекторы элементов имеют древовидную структуру. но их иерархии не совсем одинаковы.
Иерархическая структура инжектора модуля связана не только с конструкцией модуля в приложении, но также имеет иерархическую структуру инжектора модуля платформы (PlatformModule) и инжектора модуля приложения (AppModule).
В терминологии Angular платформа — это контекст, в котором выполняются приложения Angular. Наиболее распространенной платформой для приложений Angular является веб-браузер, но это также может быть операционная система мобильного устройства или веб-сервер.
При запуске приложения Angular создается уровень платформы:
платформа является точкой входа Angular на веб-странице. На каждой странице работает только одна платформа. Каждое приложение Angular работает на странице, и все общие службы привязаны кAngular
, в основном включая такие функции, как создание экземпляров модулей и их уничтожение:
@Injectable(). класс экспорта PlatformRef { // Передаем инжектор в качестве конструктора инжектора платформы (private _injector: Injector) {} // Создаем экземпляр @NgModule для данной платформы для автономной компиляции bootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>, options?: BootstrapOptions): Обещание<NgModuleRef<M>> {} // Используя данный компилятор среды выполнения, создайте экземпляр @NgModule для данной платформы bootstrapModule<M>( Тип модуля: Тип<M>, Параметры компилятора: (CompilerOptions&BootstrapOptions)| Array<CompilerOptions&BootstrapOptions> = []): Promise<NgModuleRef<M>> {} // Регистрируем прослушиватель, который будет вызываться при уничтожении платформы onDestroy(callback: () => void): void {} // Получаем инжектор платформы // Инжектор платформы является родительским инжектором для каждого приложения Angular на странице и предоставляет одноэлементный провайдер get injector(): Injector {} // Уничтожаем текущую платформу Angular и все приложения Angular на странице, включая уничтожение всех модулей и прослушивателей, зарегистрированных на платформе Destroy() {} }
Фактически, когда платформа запускается (в методе bootstrapModuleFactory
), в ngZone.run
создается ngZoneInjector
, так что все созданные сервисы создаются в зоне Angular, а ApplicationRef
(приложение Angular, работающее на странице) будет находиться в зоне Angular. Угловая зона Создана снаружи.
При запуске в браузере создается платформа браузера:
Export const PlatformBrowser: (extraProviders?: StaticProvider[]) => PlatformRef = createPlatformFactory(platformCore, 'браузер', INTERNAL_BROWSER_PLATFORM_PROVIDERS); // Среди них платформа PlatformCore должна быть включена в любую другую платформу
createPlatformFactory
будет неявно инициализирован:
функция экспорта createPlatformFactory( parentPlatformFactory: ((extraProviders?: StaticProvider[]) => PlatformRef)|null, name: string, провайдеры: StaticProvider[] = []): (extraProviders?: StaticProvider[]) => PlatformRef { const desc = `Платформа: ${name}`; const маркер = новый InjectionToken(desc); // возврат токена DI (extraProviders: StaticProvider[] = []) => { пусть платформа = getPlatform(); // Если платформа создана, обработка не выполняется if (!platform || Platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) { если (parentPlatformFactory) { // Если существует родительская платформа, используйте родительскую платформу напрямую и обновите соответствующий поставщик родительской платформыparentPlatformFactory( поставщики.concat(extraProviders).concat({обеспечивают: маркер, useValue: true})); } еще { константные введенные поставщики: StaticProvider[] = поставщики.concat(extraProviders).concat({обеспечивают: маркер, useValue: true}, { предоставить: INJECTOR_SCOPE, useValue: 'платформа' }); // Если родительской платформы нет, создаем новый инжектор и создаем платформу createPlatform(Injector.create({providers:jectiveProviders, name: desc})); } } вернуть AssertPlatform (маркер); }; }
Благодаря описанному выше процессу мы знаем, что когда приложение Angular создает платформу, оно создает инжектор модуля платформы ModuleInjector
.
,
Injector
NullInjector
является лучшим среди всех инжекторов:
статический NULL: Injector = новый NullInjector(); }
Итак, поверх инжектора модуля платформы есть NullInjector()
. Под инжектором модуля платформы также находится инжектор модуля приложения.
Каждое приложение имеет как минимум один модуль Angular. Корневой модуль — это модуль, используемый для запуска этого приложения:
@NgModule({ поставщики: APPLICATION_MODULE_PROVIDERS }). класс экспорта ApplicationModule { // ApplicationRef требует, чтобы загрузчик предоставил конструктор компонента (appRef: ApplicationRef) {} }
Корневой модуль приложения AppModule
реэкспортируется BrowserModule
, и когда мы создаем новое приложение с помощью new
команды CLI, оно автоматически включается в корневой AppModule
. В корневом модуле приложения поставщик связан со встроенным токеном DI, который используется для настройки корневого инжектора для начальной загрузки.
Angular также добавляет ComponentFactoryResolver
в инжектор корневого модуля. Этот синтаксический анализатор хранит семейство фабрик entryComponents
, поэтому он отвечает за динамическое создание компонентов.
На этом этапе мы можем просто разобраться в иерархических отношениях инжекторов модулей:
верхний уровень дерева инжекторов модулей — это инжектор корневого модуля приложения (AppModule), называемый корневым.
Над корнем есть два инжектора: один — инжектор модуля платформы (PlatformModule), а другой — NullInjector()
.
Таким образом, иерархия модулей-инжекторов выглядит следующим образом:
В нашем реальном приложении это может быть так:
Angular DI имеет многоуровневую архитектуру внедрения, что означает, что инжекторы более низкого уровня также могут создавать свои собственные экземпляры сервисов.
Как упоминалось ранее, в Angular существует две иерархии инжекторов, а именно инжектор модулей и инжектор элементов.
Когда в Angular начали широко использоваться лениво загружаемые модули, возникла проблема: система внедрения зависимостей приводила к удвоению количества экземпляров лениво загружаемых модулей.
В этом исправлении был представлен новый дизайн: инжектор использует два параллельных дерева, одно для элементов и одно для модулей .
Angular создает фабрики хостов для всех entryComponents
, которые являются корневыми представлениями для всех остальных компонентов.
Это означает, что каждый раз, когда мы создаем динамический компонент Angular, корневое представление ( RootData
) будет создано с корневыми данными ( RootView
):
class ComponentFactory_ расширяет ComponentFactory<any>{ создавать( инжектор: Инжектор, projectableNodes?: Any[][], rootSelectorOrNode?: строка|любой, ngModule?: NgModuleRef<любой>): ComponentRef<любой> { если (!ngModule) { throw new Error('Необходимо предоставить ngModule'); } const viewDef =solveDefinition(this.viewDefFactory); const компонентNodeIndex = viewDef.nodes[0].element!.comComponentProvider!.nodeIndex; //Создаем корневое представление, используя корневые данные const view = Services.createRootView( инжектор, projectableNodes || rootSelectorOrNode, viewDef, ngModule, EMPTY_CONTEXT); // Аксессор для view.nodes const компонент = asProviderData(view, компонентNodeIndex).instance; если (rootSelectorOrNode) { view.renderer.setAttribute(asElementData(view, 0).renderElement, 'ng-version', VERSION.full); } //Создаем компонент return new ComponentRef_(view, new ViewRef_(view), компонент); } }
Корневые данные ( RootData
) содержат ссылки на инжекторы elInjector
и ngModule
:
функция createRootData( elInjector: Injector, ngModule: NgModuleRef<любой>, rendererFactory: RendererFactory2, projectableNodes: любой[][], rootSelectorOrNode: любой): RootData { const sanitizer = ngModule.injector.get(Sanitizer); const errorHandler = ngModule.injector.get(ErrorHandler); const renderer = rendererFactory.createRenderer(null, null); возвращаться { нгМодуль, инжектор: элИнжектор, проектируемые узлы, селекторОрноде: rootSelectorOrNode, дезинфицирующее средство, рендерерФабрика, рендерер, обработчик ошибок, }; }
Представляем дерево внедрения элементов, поскольку его конструкция относительно проста. Изменяя иерархию инжекторов, избегайте чередования инжекторов модулей и компонентов, что приводит к двойному созданию экземпляров лениво загружаемых модулей. Потому что у каждого инжектора есть только один родительский элемент, и каждое разрешение должно найти ровно один инжектор для получения зависимостей.
В Angular представление — это представление шаблона. Оно содержит различные типы узлов, среди которых есть узел элемента. Инжектор элемента расположен на этом узле:
экспортный интерфейс ElementDef {. ... // Публичные поставщики DI, видимые в этом представлении publicProviders: {[tokenKey: string]: NodeDef}|null; // То же, что иvisiblePublicProviders, но также включает в себя частные поставщики, расположенные в этом элементе allProviders: {[tokenKey: string]: NodeDef}|null; }
ElementInjector
по умолчанию пуст, если он не настроен в атрибуте providers
@Directive()
или @Component()
.
Когда Angular создает инжектор элемента для вложенного элемента HTML, он либо наследует его от инжектора родительского элемента, либо назначает инжектор родительского элемента непосредственно определению дочернего узла.
Если у инжектора элемента дочернего HTML-элемента есть поставщик, он должен быть унаследован. В противном случае нет необходимости создавать отдельный инжектор для дочернего компонента, а при необходимости зависимости можно разрешить непосредственно из родительского инжектора.
Итак, где инжекторы элементов и инжекторы модулей начинают превращаться в параллельные деревья?
Мы уже знаем, что корневой модуль приложения ( AppModule
) будет автоматически включен в корневой AppModule
при создании нового приложения с помощью new
команды CLI.
Когда приложение ( ApplicationRef
) запускается ( bootstrap
), создается entryComponent
:
const compRef = компонентFactory.create(Injector.NULL, [], selectorOrNode, ngModule
Этот процесс создает корневое представление ( RootView
) с использованием корневых данных ( RootData
) , и будет создан инжектор корневого элемента, где elInjector
— это Injector.NULL
.
Здесь дерево инжекторов Angular разделено на дерево инжекторов элементов и дерево инжекторов модулей, эти два параллельных дерева.
Angular будет регулярно создавать подчиненные инжекторы. Всякий раз, когда Angular создает экземпляр компонента providers
указанными в @Component()
, он также создает новый субинжектор для этого экземпляра. Аналогично, когда новый NgModule
загружается во время выполнения, Angular может создать для него инжектор со своим собственным провайдером.
Инжекторы субмодулей и компонентов независимы друг от друга и каждый создает свой собственный экземпляр для предоставляемого сервиса. Когда Angular уничтожает экземпляр NgModule
или компонента, он также уничтожает эти инжекторы и экземпляры служб в инжекторах.
Выше мы представили два типа деревьев инжекторов в Angular: дерево инжекторов модулей и дерево инжекторов элементов. Итак, как Angular решает эту проблему при предоставлении зависимостей?
В Angular при разрешении токенов для получения зависимостей для компонентов/инструкций Angular разрешает это в два этапа:
ElementInjector
(ее родительского элемента)ModuleInjector
(его родительского элемента).Процесс выглядит следующим образом (см. Многоуровневый уровень). Инжектор — правила разрешения):
когда компонент объявляет зависимость, Angular попытается удовлетворить эту зависимость, используя свой собственный ElementInjector
.
Если у инжектора компонента отсутствует поставщик, он передаст запрос ElementInjector
своего родительского компонента.
Эти запросы будут продолжать пересылаться до тех пор, пока Angular не найдет инжектор, который может обработать запрос, или пока не закончится предок ElementInjector
.
Если Angular не может найти поставщика ни в одном ElementInjector
, он вернется к элементу, из которого был сделан запрос, и выполнит поиск в иерархии ModuleInjector
.
Если Angular по-прежнему не может найти поставщика, он выдаст ошибку.
Для этой цели в Angular представлен специальный инжектор слияния.
Сам по себе инжектор слияния не имеет никакой ценности, это просто комбинация определений представления и элемента.
класс Injector_ реализует Injector { конструктор (частное представление: ViewData, частное elDef: NodeDef | null) {} get(токен: любой, notFoundValue: любой = Injector.THROW_IF_NOT_FOUND): любой { constallowPrivateServices = this.elDef ? (this.elDef.flags & NodeFlags.ComponentView) !== 0: false; вернуть Services.resolveDep( this.view, this.elDef,allowPrivateServices, {флаги: DepFlags.None, токен, tokenKey: tokenKey(токен)}, notFoundValue); } }
Когда Angular разрешает зависимости, инжектор слияния является мостом между деревом инжектора элементов и деревом инжектора модулей. Когда Angular пытается разрешить определенные зависимости в компоненте или директиве, он использует инжектор слияния для обхода дерева инжектора элементов, а затем, если зависимость не найдена, переключается на дерево инжектора модулей для разрешения зависимости.
класс ViewContainerRef_ реализует ViewContainerData { ... //Запрос для инжектора родительского элемента представления getparentInjector(): Injector { пусть просмотр = this._view; пусть elDef = this._elDef.parent; while (!elDef && представление) { elDef = viewParentEl(представление); просмотр = просмотр.родитель!; } вернуть представление? новый Injector_(view, elDef): новый Injector_(this._view, null); } }Инжекторы
наследуются. Это означает, что если указанный инжектор не может разрешить зависимость, он попросит родительский инжектор разрешить ее. Конкретный алгоритм синтаксического анализа реализован в resolveDep()
:
экспортная функцияsolveDep( представление: ViewData, elDef: NodeDef,allowPrivateServices: логическое значение, depDef: DepDef, notFoundValue: любой = Injector.THROW_IF_NOT_FOUND): любой { // // мод1 // / // el1 mod2 /// //el2 // // При запросе el2.injector.get(token) проверяем и возвращаем первое найденное значение в следующем порядке: // - el2.injector.get(токен, по умолчанию) // - el1.injector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) -> не проверять модуль // - mod2.injector.get(токен, по умолчанию) }
Если это корневой компонент AppComponent
шаблона типа <child></child>
, то в Angular будет три представления:
<!-- HostView_AppComponent --> <мое-приложение></мое-приложение> <!-- View_AppComponent --> <ребенок></ребенок> <!-- View_ChildComponent --> Некоторый контент
зависит от процесса синтаксического анализа. Алгоритм синтаксического анализа будет основан на иерархии представлений, как показано на рисунке:
Если некоторые токены разрешены в дочернем компоненте, Angular:
сначала проверит инжектор дочернего элемента, проверив elRef.element.allProviders|publicProviders
.
Затем переберите все родительские элементы представления (1) и проверьте поставщика в инжекторе элементов.
Если следующий родительский элемент представления равен null
(2), вернитесь к startView
(3) и проверьте startView.rootData.elnjector
(4).
Только если токен не найден, проверьте startView.rootData module.injector
(5).
Отсюда следует, что Angular при обходе компонентов для разрешения определенных зависимостей будет искать родительский элемент определенного представления, а не родительский элемент определенного элемента. Родительский элемент представления можно получить через:
// Для компонентных представлений это главный элемент // Для встроенных представлений это индекс родительского узла содержащего контейнер представления. Функция экспорта viewParentEl(view: ViewData): NodeDef| нулевой { константный родительский просмотр = view.parent; если (родительский просмотр) { return view.parentNodeDef !.parent; } еще { вернуть ноль; } }
В этой статье в основном представлена иерархическая структура инжекторов в Angular. В Angular существует два параллельных дерева инжекторов: дерево инжекторов модулей и дерево инжекторов элементов.
Введение дерева внедрения элементов в основном предназначено для решения проблемы двойного создания модулей, вызванной анализом внедрения зависимостей и ленивой загрузкой модулей. После введения дерева инжекторов элементов процесс анализа зависимостей Angular также был скорректирован. Он отдает приоритет поиску зависимостей инжекторов, таких как инжекторы элементов и инжекторы элементов родительского представления. Только когда токен не может быть найден в инжекторе элемента, инжектор модуля. будут запрошены зависимости.