باعتبارها إطارًا أماميًا مصممًا "لمشاريع الواجهة الأمامية واسعة النطاق"، فإن Angular لديها في الواقع العديد من التصميمات الجديرة بالمرجع والتعلم. تُستخدم هذه السلسلة بشكل أساسي لدراسة مبادئ تنفيذ هذه التصميمات والوظائف. تركز هذه المقالة على أكبر ميزة في Angular - حقن التبعية، وتقدم تصميم حقن التبعية متعدد المستويات في Angular. [برامج تعليمية ذات صلة موصى بها: "Angular Tutorial"]
في المقالة السابقة، قدمنا حاقن 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
، بما في ذلك بشكل أساسي وظائف مثل إنشاء مثيلات الوحدة النمطية وتدميرها:
@Injectable(). فئة التصدير PlatformRef { // قم بتمرير الحاقن كمنشئ حاقن النظام الأساسي (حاقن خاص: حاقن) {} // قم بإنشاء مثيل @NgModule للنظام الأساسي المحدد للتجميع دون الاتصال بالإنترنت bootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>, options?: BootstrapOptions): وعد<NgModuleRef<M>> {} // باستخدام مترجم وقت التشغيل المحدد، قم بإنشاء مثيل @NgModule للنظام الأساسي المحدد bootstrapModule<M>( نوع الوحدة النمطية: النوع<M>، خيارات المترجم: (CompilerOptions&BootstrapOptions)| Array<CompilerOptions&BootstrapOptions> = []): وعد<NgModuleRef<M>> {} // سجل المستمع ليتم استدعاؤه عند تدمير النظام الأساسي onDestroy(callback: () => void): void {} // احصل على حاقن النظام الأساسي // حاقن النظام الأساسي هو الحاقن الأصلي لكل تطبيق Angular على الصفحة ويوفر لموفر المفرد الحصول على injector(): Injector {} // تدمير النظام الأساسي الحالي Angular وجميع التطبيقات Angular الموجودة على الصفحة، بما في ذلك تدمير جميع الوحدات والمستمعين المسجلين على النظام الأساسي Destroy() {} }
في الواقع، عندما يبدأ النظام الأساسي (في طريقة bootstrapModuleFactory
)، يتم إنشاء ngZoneInjector
في ngZone.run
بحيث يتم إنشاء جميع الخدمات التي تم إنشاء مثيل لها في المنطقة Angular، وسيكون ApplicationRef
(التطبيق الزاوي الذي يعمل على الصفحة) في المنطقة المنطقة الزاوية تم إنشاؤها بالخارج.
عند تشغيله في المتصفح، يتم إنشاء النظام الأساسي للمتصفح:
Export const PlatformBrowser: (extraProviders?: StaticProvider[]) => PlatformRef = createPlatformFactory(platformCore, 'browser', INTERNAL_BROWSER_PLATFORM_PROVIDERS); // من بينها، يجب تضمين النظام الأساسي الأساسي في أي نظام أساسي آخر للتصدير const PlatformCore = createPlatformFactory(null, 'core', _CORE_PLATFORM_PROVIDERS)
createPlatformFactory
ستتم تهيئتها ضمنيًا:
وظيفة التصدير createPlatformFactory( parentPlatformFactory: ((extraProviders?: StaticProvider[]) => PlatformRef)|null, name: string, موفرو الخدمة: StaticProvider[] = []): (extraProviders?: StaticProvider[]) => PlatformRef { const desc = `النظام الأساسي: ${name}`; علامة const = new حقنToken(desc); // DI token return (extraProviders: StaticProvider[] = []) => { دع المنصة = getPlatform(); // إذا تم إنشاء النظام الأساسي، فلن يتم تنفيذ أي معالجة if (!platform || platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) { إذا (الوالد بلاتفورمفاكتوري) { // إذا كان هناك نظام أساسي أصلي، فاستخدم النظام الأساسي الأصلي مباشرةً وقم بتحديث الموفر المقابلparentPlatformFactory( Providers.concat(extraProviders).concat({provide: Marker, useValue: true})); } آخر { const injectedProviders: StaticProvider[] = Providers.concat(extraProviders).concat({provide: Marker, useValue: true}, { تقديم: INJECTOR_SCOPE، قيمة الاستخدام: "المنصة" }); // إذا لم يكن هناك نظام أساسي، فقم بإنشاء حاقن جديد وإنشاء نظام أساسي createPlatform(Injector.create({providers: injectedProviders, name: desc})); } } إرجاعتأكيدPlatform(marker); }; }
من خلال العملية المذكورة أعلاه، نعلم أنه عندما يقوم تطبيق Angular بإنشاء النظام الأساسي، فإنه يقوم بإنشاء حاقن الوحدة النمطية للنظام الأساسي ModuleInjector
. يمكننا أيضًا أن نرى من تعريف Injector
في القسم السابق أن NullInjector
هو الجزء العلوي من جميع الحاقنات:
export Abstract class Injector { static NULL: Injector = new NullInjector(); }
لذلك، يوجد NullInjector()
أعلى حاقن وحدة النظام الأساسي. تحت حاقن وحدة النظام الأساسي، يوجد أيضًا حاقن وحدة التطبيق.
يحتوي كل تطبيق على وحدة Angular واحدة على الأقل. الوحدة الجذرية هي الوحدة المستخدمة لبدء هذا التطبيق:
@NgModule({providers: 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_ Extends ComponentFactory<any>{ يخلق( الحاقن: الحاقن، العقد القابلة للعرض؟: أي [] []، rootSelectorOrNode ؟: سلسلة | أي، ngModule ؟: NgModuleRef<any>): ComponentRef<any> { إذا (!ngModule) { رمي خطأ جديد ("يجب توفير ngModule")؛ } const viewDef = ResolveDefinition(this.viewDefFactory); const ComponentNodeIndex = viewDef.nodes[0].element!.componentProvider!.nodeIndex; // أنشئ عرض الجذر باستخدام بيانات الجذر const view = Services.createRootView( injector, projectableNodes ||. [], rootSelectorOrNode, viewDef, ngModule, EMPTY_CONTEXT); // الوصول إلى view.nodes const Compon = asProviderData(view, ComponentNodeIndex).instance; إذا (rootSelectorOrNode) { view.renderer.setAttribute(asElementData(view, 0).renderElement, 'ng-version', VERSION.full); } // إنشاء مكون return new ComponentRef_(view, new ViewRef_(view),component); } }
تحتوي البيانات الجذرية ( RootData
) على إشارات إلى حاقنات elInjector
و ngModule
:
الوظيفة createRootData( الحاقن: الحاقن، ngModule: NgModuleRef<any>، rendererFactory: RendererFactory2، ProjectableNodes: أي [] []، rootSelectorOrNode: أي): RootData { const sanitizer = ngModule.injector.get(Sanitizer); const errorHandler = ngModule.injector.get(ErrorHandler); const renderer = rendererFactory.createRenderer(null, null); يعود { ngModule, حاقن: الحاقن، عقد قابلة للعرض, SelectorOrNode: rootSelectorOrNode، المطهر, مصنع العارض, العارض, معالج الأخطاء, }; }
تقديم شجرة حاقن العناصر لأن هذا التصميم بسيط نسبيًا. عن طريق تغيير التسلسل الهرمي للحاقن، تجنب تشذير حاقنات الوحدة والمكونات، مما يؤدي إلى إنشاء مثيل مزدوج للوحدات النمطية المحملة ببطء. لأن كل حاقن له أصل واحد فقط، ويجب أن يجد كل حل حاقنًا واحدًا بالضبط لاسترداد التبعيات.
في Angular، العرض هو تمثيل لقالب، ويحتوي على أنواع مختلفة من العقد، من بينها عقدة العنصر، ويوجد حاقن العنصر في هذه العقدة:
واجهة التصدير ElementDef {. ... // موفرو DI العامون المرئيون في هذا العرض publicProviders: {[tokenKey: string]: NodeDef}|null; // مثل visualPublicProviders، ولكنه يتضمن أيضًا موفري الخدمات الخاصين الموجودين في هذا العنصر allProviders: {[tokenKey: string]: NodeDef}|null; }
يكون ElementInjector
فارغًا بشكل افتراضي ما لم يتم تكوينه في سمة providers
@Directive()
أو @Component()
.
عندما يقوم Angular بإنشاء حاقن عنصر لعنصر HTML متداخل، فإنه إما يرثه من حاقن العنصر الأصلي أو يقوم بتعيين حاقن العنصر الأصلي مباشرة إلى تعريف العقدة الفرعية.
إذا كان حاقن العنصر في عنصر HTML الفرعي يحتوي على موفر، فيجب توريثه. بخلاف ذلك، ليست هناك حاجة لإنشاء حاقن منفصل للمكون الفرعي، ويمكن حل التبعيات مباشرة من الحاقن الأصلي إذا لزم الأمر.
إذن، أين تبدأ حاقنات العناصر وحاقن الوحدة في أن تصبح أشجارًا متوازية؟
نحن نعلم بالفعل أن وحدة جذر التطبيق ( AppModule
) سيتم تضمينها تلقائيًا في وحدة جذر AppModule
عند إنشاء تطبيق جديد باستخدام أمر CLI new
.
عندما يبدأ التطبيق ( ApplicationRef
) ( bootstrap
)، يتم إنشاء entryComponent
:
RootData
RootView
, ngModule);
) ، وسيتم إنشاء حاقن العنصر الجذري، حيث يكون elInjector
هو Injector.NULL
.
هنا، تنقسم شجرة حاقن Angular إلى شجرة حاقن العناصر وشجرة حاقن الوحدة، هاتان الشجرتان المتوازيتان.
سيقوم Angular بإنشاء حاقنات ثانوية بشكل منتظم عندما يقوم Angular بإنشاء مثيل مكون providers
المحددين في @Component()
، فإنه سيقوم أيضًا بإنشاء حاقن فرعي جديد للمثيل. وبالمثل، عندما يتم تحميل NgModule
جديد في وقت التشغيل، يمكن لـ Angular إنشاء حاقن لها مع المزود الخاص بها.
تعتبر حاقنات الوحدات الفرعية والمكونات مستقلة عن بعضها البعض ويقوم كل منها بإنشاء مثيل خاص به للخدمة المقدمة. عندما يقوم Angular بتدمير NgModule
أو مثيل المكون، فإنه يدمر أيضًا هذه الحاقنات ومثيلات الخدمة تلك الموجودة في الحاقنات.
قدمنا أعلاه نوعين من أشجار الحاقن في Angular: شجرة حاقن الوحدة النمطية وشجرة حاقن العناصر. إذًا، كيف يمكن لـ Angular حل المشكلة عند توفير التبعيات؟
في Angular، عند حل الرموز المميزة للحصول على تبعيات للمكونات/التعليمات، تحل Angular المشكلة على مرحلتين:
ElementInjector
(الأصل)ModuleInjector
(الأصل)،تكون العملية كما يلي (راجع Multi-Level قواعد حل الحاقن):
عندما يعلن أحد المكونات عن تبعية، سيحاول Angular تلبية تلك التبعية باستخدام ElementInjector
الخاص به.
إذا كان حاقن أحد المكونات يفتقر إلى موفر، فسوف يقوم بتمرير الطلب إلى ElementInjector
الخاص بالمكون الأصلي.
ستستمر إعادة توجيه هذه الطلبات حتى تعثر Angular على حاقن يمكنه التعامل مع الطلب أو ينفد من ElementInjector
الأصلي.
إذا لم تتمكن Angular من العثور على الموفر في أي ElementInjector
، فسوف تعود إلى العنصر الذي تم تقديم الطلب منه وتبحث عن التسلسل الهرمي ModuleInjector
.
إذا لم يتمكن Angular من العثور على الموفر، فسوف يلقي خطأً.
لهذا الغرض، تقدم Angular حاقن دمج خاصًا.
ليس لحاقن الدمج في حد ذاته أي قيمة، فهو مجرد مزيج من تعريفات العرض والعناصر.
فئة Injector_ تنفذ الحاقن { مُنشئ (عرض خاص: ViewData، elDef خاص: NodeDef|null) {} الحصول على (الرمز المميز: أي، notFoundValue: أي = Injector.THROW_IF_NOT_FOUND): أي { constallowPrivateServices = this.elDef ? (this.elDef.flags & NodeFlags.ComponentView) !== 0 : false; إرجاع الخدمات.resolveDep( هذا.عرض، this.elDef، تسمح الخدمات الخاصة، {الأعلام: DepFlags.None, token, tokenKey: tokenKey(token)}, notFoundValue); } }
عندما يقوم Angular بحل التبعيات، يكون حاقن الدمج هو الجسر بين شجرة حاقن العنصر وشجرة حاقن الوحدة النمطية. عندما يحاول Angular حل تبعيات معينة في مكون أو توجيه، فإنه يستخدم حاقن الدمج لاجتياز شجرة حاقن العناصر، ثم، إذا لم يتم العثور على التبعية، يقوم بالتبديل إلى شجرة حاقن الوحدة النمطية لحل التبعية.
فئة ViewContainerRef_ تنفذ ViewContainerData { ... // الاستعلام عن حاقن عنصر العرض الأصلي للحصول علىparentInjector(): Injector { دع العرض = this._view; دع elDef = this._elDef.parent; بينما (!elDef && عرض) { elDef = viewParentEl(view); عرض = view.parent!; } عرض الإرجاع new Injector_(view, elDef): new Injector_(this._view, null); } }تكون حاقنات
قابلة للتوريث، مما يعني أنه إذا لم يتمكن الحاقن المحدد من حل التبعية، فسوف يطلب من الحاقن الأصلي حلها. يتم تنفيذ خوارزمية التحليل المحددة في طريقة resolveDep()
:
وظيفة التصدير ResolveDep ( عرض: ViewData، elDef: NodeDef،allowPrivateServices: منطقي، depDef: DepDef، notFoundValue: Any = Injector.THROW_IF_NOT_FOUND): أي { // // mod1 // / // 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| باطل { constparentView = view.parent; إذا (عرض الوالدين) { return view.parentNodeDef!.parent; } آخر { عودة فارغة؛ } }
تقدم هذه المقالة بشكل أساسي البنية الهرمية للحقن في Angular. هناك نوعان من أشجار الحاقن المتوازية في Angular: شجرة حاقن الوحدة وشجرة حاقن العنصر.
يهدف إدخال شجرة حاقن العناصر بشكل أساسي إلى حل مشكلة الإنشاء المزدوج للوحدات النمطية الناتجة عن تحليل حقن التبعية والتحميل البطيء للوحدات النمطية. بعد تقديم شجرة حاقن العنصر، تم أيضًا تعديل عملية تحليل التبعية في Angular، وهي تعطي الأولوية للبحث عن تبعيات الحاقنات مثل حاقنات العناصر وحاقن عناصر العرض الأصلية فقط عندما لا يمكن العثور على الرمز المميز في حاقن العنصر، حاقن الوحدة سيتم الاستعلام عن التبعيات في.