"대규모 프런트엔드 프로젝트용"으로 설계된 프런트엔드 프레임워크로서 Angular에는 실제로 참조하고 학습할 가치가 있는 많은 디자인이 있습니다. 이 시리즈는 주로 이러한 디자인과 기능의 구현 원리를 연구하는 데 사용됩니다. 이 글에서는 Angular의 가장 큰 특징인 의존성 주입에 초점을 맞추고 Angular의 다단계 의존성 주입 설계를 소개합니다. [추천 관련 튜토리얼: "Angular 튜토리얼"]
이전 기사에서는 Angular의 Injectot
인젝터, Provider
공급자 및 인젝터 메커니즘을 소개했습니다. 그렇다면 Angular 애플리케이션에서 구성 요소와 모듈은 어떻게 종속성을 공유합니까? 동일한 서비스를 여러 번 인스턴스화할 수 있습니까?
구성요소와 모듈의 종속성 주입 프로세스는 Angular의 다중 레벨 종속성 주입 설계와 분리될 수 없습니다.
앞서 말했듯이 Angular의 인젝터는 상속 가능하고 계층적입니다.
Angular에는 두 가지 인젝터 계층 구조가 있습니다.
ModuleInjector
모듈 인젝터: @NgModule()
또는 @Injectable()
주석을 사용하여 이 계층 구조에 ModuleInjector를 구성합니다. ModuleInjector
ElementInjector
인젝터:모든 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>, 컴파일러 옵션: (컴파일러 옵션&부트스트랩 옵션)| Array<CompilerOptions&BootstrapOptions> = []): Promise<NgModuleRef<M>> {} // 플랫폼 파괴 시 호출할 리스너를 등록합니다. onDestroy(callback: () => void): void {} // 플랫폼 인젝터 가져오기 // 플랫폼 인젝터는 페이지의 모든 Angular 애플리케이션에 대한 상위 인젝터이며 싱글턴 공급자 get injector(): Injector {}를 제공합니다. // 플랫폼에 등록된 모든 모듈과 리스너를 포함하여 현재 Angular 플랫폼과 페이지의 모든 Angular 애플리케이션을 삭제합니다. destroy() {} }
실제로 플랫폼이 시작되면( bootstrapModuleFactory
메소드에서) ngZoneInjector
가 ngZone.run
에 생성되어 인스턴스화된 모든 서비스가 Angular 영역에 생성되고 ApplicationRef
(페이지에서 실행되는 Angular 애플리케이션)가 각도 영역 외부에 생성됩니다.
브라우저에서 실행되면 브라우저 플랫폼이 생성됩니다.
import const platformBrowser: (extraProviders?: StaticProvider[]) => PlatformRef = createPlatformFactory(platformCore, 'browser', INTERNAL_BROWSER_PLATFORM_PROVIDERS); // 그 중 platformCore 플랫폼은 다른 플랫폼에 반드시 포함되어야 합니다. import const platformCore = createPlatformFactory(null, 'core', _CORE_PLATFORM_PROVIDERS);
플랫폼 팩토리(예: 위의 createPlatformFactory
)를 사용하여 플랫폼을 생성할 때 페이지의 플랫폼 암시적으로 초기화됩니다:
내보내기 함수 createPlatformFactory( parentPlatformFactory: ((extraProviders?: StaticProvider[]) => PlatformRef)|null, 이름: 문자열, 공급자: StaticProvider[] = []): (extraProviders?: StaticProvider[]) => PlatformRef { const desc = `플랫폼: ${name}`; const marker = new InsertionToken(desc); // DI 토큰 반환 (extraProviders: StaticProvider[] = []) => { 플랫폼 = getPlatform(); // 플랫폼이 생성된 경우 아무런 처리도 수행하지 않습니다. if (!platform || platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) { if (parentPlatformFactory) { // 상위 플랫폼이 있는 경우 상위 플랫폼을 직접 사용하고 해당 공급자를 업데이트합니다. parentPlatformFactory( provide.concat(extraProviders).concat({provide: marker, useValue: true})); } 또 다른 { const 주입 제공자: StaticProvider[] = 공급자.concat(extraProviders).concat({provide: marker, useValue: true}, { 제공: INJECTOR_SCOPE, useValue: '플랫폼' }); // 상위 플랫폼이 없으면 새로운 인젝터를 생성하고 플랫폼 생성 createPlatform(Injector.create({providers: 주입된공급자, 이름: desc})); } } returnassertPlatform(marker); }; }
위의 과정을 통해 우리는 Angular 애플리케이션이 플랫폼을 생성할 때 플랫폼의 모듈 인젝터인 ModuleInjector
생성한다는 것을 알 수 있습니다. 또한 이전 섹션의 Injector
정의에서 NullInjector
모든 인젝터의 최상위임을 알 수 있습니다
. 정적 NULL: 인젝터 = 새로운 NullInjector(); }
따라서 플랫폼 모듈 인젝터 위에 NullInjector()
가 있습니다. 플랫폼 모듈 인젝터 아래에는 애플리케이션 모듈 인젝터도 있습니다.
각 애플리케이션에는 최소한 하나의 Angular 모듈이 있습니다. 루트 모듈은 이 애플리케이션을 시작하는 데 사용되는 모듈입니다:
@NgModule({providers: APPLICATION_MODULE_PROVIDERS }) 내보내기 클래스 ApplicationModule { // ApplicationRef에서는 구성 요소 생성자를 제공하기 위해 부트스트랩이 필요합니다(appRef: ApplicationRef) {} }
AppModule
루트 애플리케이션 모듈은 BrowserModule
에 의해 다시 내보내지며, CLI의 new
명령을 사용하여 새 애플리케이션을 생성하면 루트 AppModule
에 자동으로 포함됩니다. 애플리케이션 루트 모듈에서 공급자는 부트스트랩에 대한 루트 인젝터를 구성하는 데 사용되는 내장 DI 토큰과 연결됩니다.
Angular는 또한 루트 모듈 인젝터에 ComponentFactoryResolver
추가합니다. 이 파서는 공장의 entryComponents
계열을 저장하므로 구성 요소를 동적으로 생성하는 일을 담당합니다.
이 시점에서 우리는 모듈 인젝터의 계층적 관계를 간단하게 정리할 수 있습니다.
모듈 인젝터 트리의 최상위 레벨은 루트라고 불리는 애플리케이션 루트 모듈(AppModule) 인젝터입니다.
루트 위에는 두 개의 인젝터가 있습니다. 하나는 플랫폼 모듈(PlatformModule) 인젝터이고 다른 하나는 NullInjector()
입니다.
따라서 모듈 인젝터 계층 구조는 다음과 같습니다.
실제 애플리케이션에서는 다음과 같을 가능성이 높습니다.
Angular DI에는 계층화된 주입 아키텍처가 있습니다. 즉, 하위 수준의 인젝터도 자체 서비스 인스턴스를 생성할 수 있습니다.
앞서 언급했듯이 Angular에는 두 가지 인젝터 계층 구조, 즉 모듈 인젝터와 요소 인젝터가 있습니다.
지연 로드 모듈이 Angular에서 널리 사용되기 시작했을 때 문제가 발생했습니다. 종속성 주입 시스템으로 인해 지연 로드 모듈의 인스턴스화가 두 배로 늘어났습니다.
이번 수정에서는 새로운 디자인이 도입되었습니다. 인젝터는 요소용과 모듈용으로 하나씩 두 개의 병렬 트리를 사용합니다 .
Angular는 다른 모든 구성요소의 루트 뷰인 모든 entryComponents
에 대한 호스트 팩토리를 생성합니다.
(
RootView
)를 사용하여 루트 뷰( RootData
)가 생성된다는 의미입니다.
만들다( 인젝터: 인젝터, projectableNodes?: any[][], rootSelectorOrNode?: string|any, ngModule?: NgModuleRef<any>): ComponentRef<any> { if (!ngModule) { throw new Error('ngModule이 제공되어야 합니다.'); } const viewDef = 해결Definition(this.viewDefFactory); const componentNodeIndex = viewDef.nodes[0].element!.comComponentProvider!.nodeIndex; //루트 데이터를 사용하여 루트 뷰 생성 const view = Services.createRootView( 인젝터, projectableNodes || [], rootSelectorOrNode, viewDef, ngModule, EMPTY_CONTEXT); // view.nodes에 대한 접근자 const component = asProviderData(view, componentNodeIndex).instance; if (rootSelectorOrNode) { view.renderer.setAttribute(asElementData(view, 0).renderElement, 'ng-version', VERSION.full); } //컴포넌트 생성 return new ComponentRef_(view, new ViewRef_(view), 컴포넌트); } }
루트 데이터( RootData
)에는 elInjector
및 ngModule
인젝터에 대한 참조가 포함되어 있습니다.
function createRootData( elInjector: 인젝터, ngModule: NgModuleRef<any>, rendererFactory: RendererFactory2, projectableNodes: any[][], rootSelectorOrNode: any): RootData { const sanitizer = ngModule.injector.get(Sanitizer); const errorHandler = ngModule.injector.get(ErrorHandler); const 렌더러 = rendererFactory.createRenderer(null, null); 반품 { ng모듈, 인젝터: 엘인젝터, 투영 가능한 노드, selectorOrNode: rootSelectorOrNode, 소독제, 렌더러공장, 렌더러, 오류 처리기, }; }
이 디자인은 상대적으로 간단하기 때문에 요소 인젝터 트리를 소개합니다. 인젝터 계층 구조를 변경하면 모듈과 구성 요소 인젝터가 인터리브되어 지연 로드된 모듈이 이중 인스턴스화되는 것을 방지할 수 있습니다. 각 인젝터에는 상위가 하나만 있고 각 해결 방법은 종속성을 검색하려면 정확히 하나의 인젝터를 찾아야 하기 때문입니다.
Angular에서 뷰는 다양한 유형의 노드를 포함하며 그 중 요소 인젝터는 다음 노드에 있습니다
. ... // 이 뷰에 표시되는 DI의 공용 공급자 publicProviders: {[tokenKey: string]: NodeDef}|null; // visiblePublicProviders와 동일하지만 이 요소에 있는 비공개 공급자도 포함합니다. allProviders: {[tokenKey: string]: NodeDef}|null; }
ElementInjector
는 @Directive()
또는 @Component()
의 providers
속성에 구성되지 않은 한 기본적으로 비어 있습니다.
Angular는 중첩된 HTML 요소에 대한 요소 인젝터를 생성할 때 이를 상위 요소 인젝터에서 상속하거나 상위 요소 인젝터를 하위 노드 정의에 직접 할당합니다.
하위 HTML 요소의 요소 인젝터에 공급자가 있는 경우 이를 상속해야 합니다. 그렇지 않으면 하위 구성 요소에 대해 별도의 인젝터를 만들 필요가 없으며 필요한 경우 상위 구성 요소의 인젝터에서 직접 종속성을 해결할 수 있습니다.
그렇다면 요소 인젝터와 모듈 인젝터는 어디에서 병렬 트리가 되기 시작할까요?
우리는 CLI의 new
명령을 사용하여 새 애플리케이션을 생성할 때 애플리케이션 루트 모듈( AppModule
)이 루트 AppModule
에 자동으로 포함된다는 것을 이미 알고 있습니다.
애플리케이션( ApplicationRef
)이 시작되면( bootstrap
), entryComponent
생성됩니다.
const compRef = componentFactory.create(Injector.NULL, [], selectorOrNode, ngModule)
이 프로세스는 루트 데이터( RootData
)를 사용하여 루트 뷰( RootView
)를 생성합니다. ) , 루트 요소 인젝터가 생성됩니다. 여기서 elInjector
는 Injector.NULL
입니다.
여기서 Angular의 인젝터 트리는 요소 인젝터 트리와 모듈 인젝터 트리로 구분되며, 이 두 개의 병렬 트리입니다.
Angular는 정기적으로 하위 인젝터를 생성합니다. Angular는 @Component()
에 지정된 providers
구성 요소 인스턴스를 생성할 때마다 해당 인스턴스에 대한 새 하위 인젝터도 생성합니다. 마찬가지로 런타임에 새 NgModule
이 로드되면 Angular는 자체 공급자를 사용하여 이에 대한 인젝터를 생성할 수 있습니다.
서브모듈과 컴포넌트 인젝터는 서로 독립적이며 각각 제공된 서비스에 대해 자체 인스턴스를 생성합니다. Angular는 NgModule
또는 구성 요소 인스턴스를 제거할 때 이러한 인젝터와 인젝터에 있는 서비스 인스턴스도 제거합니다.
위에서 우리는 Angular에서 모듈 인젝터 트리와 요소 인젝터 트리라는 두 가지 유형의 인젝터 트리를 소개했습니다. 그렇다면 Angular는 종속성을 제공할 때 이를 어떻게 해결합니까?
Angular에서 구성 요소/명령어에 대한 종속성을 얻기 위해 토큰을 확인할 때 Angular는
ElementInjector
계층(상위)ModuleInjector
계층(상위)의 두 단계로 이를 해결합니다.프로세스는 다음과 같습니다(다중 레벨 참조
).인젝터 - 해결 규칙):
구성 요소가 종속성을 선언하면 Angular는 자체 ElementInjector
사용하여 해당 종속성을 충족하려고 시도합니다.
구성 요소의 인젝터에 공급자가 없으면 상위 구성 요소의 ElementInjector
에 요청을 전달합니다.
이러한 요청은 Angular가 요청을 처리할 수 있는 인젝터를 찾거나 상위 ElementInjector
부족할 때까지 계속 전달됩니다.
Angular가 ElementInjector
에서 공급자를 찾을 수 없으면 요청이 이루어진 요소로 돌아가 ModuleInjector
계층 구조를 찾습니다.
Angular가 여전히 공급자를 찾을 수 없으면 오류가 발생합니다.
이를 위해 Angular는 특별한 병합 인젝터를 도입합니다.
병합 인젝터 자체에는 값이 없으며 단지 뷰와 요소 정의의 조합일 뿐입니다.
클래스 Injector_는 인젝터를 구현합니다. 생성자(개인 보기: ViewData, 개인 elDef: NodeDef|null) {} get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): 모든 { const makePrivateServices = this.elDef ? (this.elDef.flags & NodeFlags.ComponentView) !== 0 : false; Services.resolveDep( this.view, this.elDef, allowedPrivateServices, {flags: DepFlags.None, token, tokenKey: tokenKey(token)}, notFoundValue); } }
Angular가 종속성을 해결할 때 병합 인젝터는 요소 인젝터 트리와 모듈 인젝터 트리 사이의 브리지입니다. Angular는 구성 요소나 지시문의 특정 종속성을 해결하려고 할 때 병합 인젝터를 사용하여 요소 인젝터 트리를 순회한 다음 종속성을 찾을 수 없으면 모듈 인젝터 트리로 전환하여 종속성을 해결합니다.
ViewContainerRef_ 클래스는 ViewContainerData를 구현합니다. ... //상위 뷰 요소 인젝터에 대한 쿼리 get parentInjector(): Injector { 보기 = this._view; elDef = this._elDef.parent; while (!elDef && 보기) { elDef = viewParentEl(view); 보기 = view.parent!; } 반환 보기 ? new Injector_(view, elDef) : new Injector_(this._view, null); } }
인젝터는 상속 가능합니다. 즉, 지정된 인젝터가 종속성을 해결할 수 없으면 부모 인젝터에게 이를 해결하도록 요청합니다. 특정 구문 분석 알고리즘은 resolveDep()
메서드에서 구현됩니다.
내보내기 함수 해결Dep( 보기: ViewData, elDef: NodeDef, allowedPrivateServices: 부울, depDef: DepDef, notFoundValue: 모든 = Injector.THROW_IF_NOT_FOUND): 모든 { // // 모드1 // / // 엘1 모드2 // / //el2 // // el2.injector.get(token) 요청 시, 다음 순서로 찾은 첫 번째 값을 확인하고 반환합니다. // - el2.injector.get(토큰, 기본값) // - el1.injector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) -> 모듈을 확인하지 않음 // - mod2.injector.get(토큰, 기본값) }
<child></child>
와 같은 템플릿의 루트 AppComponent
구성 요소인 경우 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| 널 { const parentView = view.parent; if (parentView) { return view.parentNodeDef !.parent; } 또 다른 { null을 반환; } }
이 글에서는 주로 Angular의 인젝터 계층 구조를 소개합니다. Angular에는 모듈 인젝터 트리와 요소 인젝터 트리라는 두 가지 병렬 인젝터 트리가 있습니다.
요소 주입기 트리의 도입은 주로 종속성 주입 구문 분석 및 모듈의 지연 로딩으로 인해 발생하는 모듈의 이중 인스턴스화 문제를 해결하기 위한 것입니다. 요소 인젝터 트리가 도입된 후 Angular의 종속성 구문 분석 프로세스도 요소 인젝터 및 상위 뷰 요소 인젝터와 같은 인젝터의 종속성을 찾는 데 우선순위를 둡니다. 종속성을 쿼리합니다.