Sebagai kerangka front-end yang dirancang "untuk proyek front-end skala besar", Angular sebenarnya memiliki banyak desain yang layak untuk dijadikan referensi dan pembelajaran. Seri ini terutama digunakan untuk mempelajari prinsip implementasi desain dan fungsi tersebut. Artikel ini berfokus pada fitur terbesar Angular - injeksi ketergantungan, dan memperkenalkan desain injeksi ketergantungan multi-level di Angular. [Tutorial terkait yang direkomendasikan: "Tutorial Angular"]
Pada artikel sebelumnya, kami memperkenalkan injektor Injectot
, penyedia Provider
, dan mekanisme injektor di Angular. Jadi, dalam aplikasi Angular, bagaimana komponen dan modul berbagi dependensi? Bisakah layanan yang sama dibuat berkali-kali?
Proses injeksi ketergantungan komponen dan modul tidak dapat dipisahkan dari desain injeksi ketergantungan multi-level Angular. Mari kita lihat.
Seperti yang kami katakan sebelumnya, injektor di Angular dapat diwariskan dan hierarkis.
Di Angular, ada dua hierarki injektor:
ModuleInjector
Module Injector: Konfigurasikan ModuleInjector dalam hierarki ini menggunakan anotasi @NgModule()
atau @Injectable()
ModuleInjector
ElementInjector
Injector: Secara implisit membuatmodul pada setiap elemen DOM Baik injektor maupun injektor elemen memiliki struktur pohon, tetapi hierarki mereka tidak persis sama.
Struktur hierarki injektor modul tidak hanya terkait dengan desain modul dalam aplikasi, tetapi juga memiliki struktur hierarki injektor modul platform (PlatformModule) dan injektor modul aplikasi (AppModule).
Dalam terminologi Angular, platform adalah konteks di mana aplikasi Angular dijalankan. Platform paling umum untuk aplikasi Angular adalah browser web, namun bisa juga berupa sistem operasi perangkat seluler atau server web.
Ketika aplikasi Angular dimulai, itu akan membuat lapisan platform:
platform adalah titik masuk Angular di halaman web. Setiap halaman hanya memiliki satu platform. Setiap aplikasi Angular berjalan di halaman, dan semua layanan umum terikat keAngular
, terutama mencakup fungsi seperti membuat instance modul dan menghancurkannya:
@Injectable() kelas ekspor PlatformRef { // Masukkan injektor sebagai konstruktor injektor platform(pribadi _injector: Injector) {} // Buat instance @NgModule untuk platform tertentu untuk kompilasi offline bootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>, options?: BootstrapOptions): Janji<NgModuleRef<M>> {} // Menggunakan compiler runtime yang diberikan, buat instance @NgModule untuk platform bootstrapModule<M>( tipe modul: Tipe<M>, compilerOptions: (CompilerOptions&BootstrapOptions)| Array<CompilerOptions&BootstrapOptions> = []): Janji<NgModuleRef<M>> {} // Daftarkan pendengar yang akan dipanggil ketika menghancurkan platform onDestroy(callback: () => void): void {} // Dapatkan injektor platform // Injektor platform adalah injektor induk untuk setiap aplikasi Angular pada laman dan menyediakan penyedia tunggal get injector(): Injector {} // Hancurkan platform Angular saat ini dan semua aplikasi Angular di laman, termasuk musnahnya semua modul dan pendengar yang terdaftar di platform destroy() {} }
Faktanya, ketika platform dimulai (dalam metode bootstrapModuleFactory
), ngZoneInjector
dibuat di ngZone.run
sehingga semua layanan yang dipakai dibuat di zona Angular, dan ApplicationRef
(aplikasi Angular yang berjalan di halaman) akan berada di zona Angular Zona sudut Dibuat di luar.
Saat diluncurkan di browser, platform browser dibuat:
ekspor const platformBrowser: (extraProviders?: StaticProvider[]) => PlatformRef = createPlatformFactory(platformCore, 'browser', INTERNAL_BROWSER_PLATFORM_PROVIDERS); // Diantaranya, platform platformCore harus disertakan dalam platform lainnya ekspor const platformCore = createPlatformFactory(null, 'core', _CORE_PLATFORM_PROVIDERS
Saat membuat platform menggunakan pabrik platform (seperti createPlatformFactory
di atas), platform halaman akan diinisialisasi secara implisit:
fungsi ekspor createPlatformFactory( parentPlatformFactory: ((extraProviders?: StaticProvider[]) => PlatformRef)|null, nama: string, penyedia: StaticProvider[] = []): (extraProviders?: StaticProvider[]) => PlatformRef { const desc = `Platform: ${nama}`; const marker = new InjectionToken(desc); // pengembalian token DI (extraProviders: StaticProvider[] = []) => { biarkan platform = getPlatform(); // Jika platform telah dibuat, tidak ada pemrosesan yang dilakukan if (!platform || platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) { jika (parentPlatformFactory) { // Jika ada platform induk, gunakan platform induk secara langsung dan perbarui penyedia yang sesuai parentPlatformFactory( penyedia.concat(extraProviders).concat({menyediakan: penanda, useValue: true})); } kalau tidak { const disuntikkanProviders: StaticProvider[] = penyedia.concat(extraProviders).concat({menyediakan: penanda, useValue: true}, { menyediakan: INJECTOR_SCOPE, nilai penggunaan: 'platform' }); // Jika tidak ada platform induk, buat injektor baru dan buat platform createPlatform(Injector.create({providers: injectionProviders, name: desc})); } } kembali menegaskanPlatform(penanda); }; }
Melalui proses di atas, kita mengetahui bahwa saat aplikasi Angular membuat platform, ia akan membuat injektor modul platform ModuleInjector
. Kita juga dapat melihat dari definisi Injector
di bagian sebelumnya bahwa NullInjector
adalah yang teratas dari semua injector:
ekspor abstrak class Injector { NULL statis: Injektor = NullInjector baru(); }
Jadi, di atas injektor modul platform, ada NullInjector()
. Di bawah injektor modul platform, terdapat juga injektor modul aplikasi.
Setiap aplikasi memiliki setidaknya satu modul Angular. Modul root adalah modul yang digunakan untuk memulai aplikasi ini:
@NgModule({ penyedia: APPLICATION_MODULE_PROVIDERS }) ekspor kelas ApplicationModule { // ApplicationRef memerlukan bootstrap untuk menyediakan konstruktor komponen(appRef: ApplicationRef) {} }
Modul aplikasi root AppModule
diekspor ulang oleh BrowserModule
, dan ketika kita membuat aplikasi baru menggunakan perintah CLI new
, maka secara otomatis disertakan dalam root AppModule
. Dalam modul root aplikasi, penyedia dikaitkan dengan token DI bawaan yang digunakan untuk mengonfigurasi injektor root untuk bootstrap.
Angular juga menambahkan ComponentFactoryResolver
ke injektor modul root. Parser ini menyimpan keluarga entryComponents
dari pabrik, sehingga parser ini bertanggung jawab untuk membuat komponen secara dinamis.
Pada titik ini, kita cukup memilah hubungan hierarki injektor modul:
tingkat teratas pohon injektor modul adalah injektor modul akar aplikasi (AppModule), yang disebut root.
Ada dua injektor di atas root, satu adalah injektor modul platform (PlatformModule) dan yang lainnya adalah NullInjector()
.
Oleh karena itu, hierarki injektor modul adalah sebagai berikut:
Dalam penerapan kita yang sebenarnya, kemungkinannya akan seperti ini:
Angular DI memiliki arsitektur injeksi berlapis, yang berarti injektor tingkat rendah juga dapat membuat instance layanannya sendiri.
Seperti disebutkan sebelumnya, ada dua hierarki injector di Angular, yaitu module injector dan element injector.
Ketika modul yang dimuat dengan lambat mulai digunakan secara luas di Angular, sebuah masalah muncul: sistem injeksi ketergantungan menyebabkan pembuatan instance modul yang dimuat dengan lambat menjadi dua kali lipat.
Dalam perbaikan ini, desain baru diperkenalkan: injektor menggunakan dua pohon paralel, satu untuk elemen dan satu lagi untuk modul .
Angular membuat pabrik host untuk semua entryComponents
, yang merupakan tampilan root untuk semua komponen lainnya.
Artinya setiap kali kita membuat komponen Angular dinamis, tampilan root ( RootData
) akan dibuat dengan data root ( RootView
):
class ComponentFactory_ extends ComponentFactory<any>{ membuat( injektor: Injektor, projectableNodes?: apa saja[][], rootSelectorOrNode?: string|apa saja, ngModule?: NgModuleRef<any>): ComponentRef<any> { jika (!ngModule) { throw new Error('ngModule harus disediakan'); } const viewDef = resolusiDefinition(ini.viewDefFactory); const componentNodeIndex = viewDef.nodes[0].element!.componentProvider!.nodeIndex; //Buat tampilan root menggunakan data root const view = Services.createRootView( injektor, projectableNodes || [], rootSelectorOrNode, viewDef, ngModule, EMPTY_CONTEXT); // Pengakses untuk view.node const komponen = asProviderData(view, componentNodeIndex).instance; jika (rootSelectorOrNode) { view.renderer.setAttribute(asElementData(view, 0).renderElement, 'ng-version', VERSION.full); } //Membuat komponen return new ComponentRef_(view, new ViewRef_(view), komponen); } }
Data root ( RootData
) berisi referensi ke injektor elInjector
dan ngModule
:
function createRootData( elInjector: Injektor, ngModule: NgModuleRef<any>, rendererFactory: RendererFactory2, projectableNodes: apa saja[][], rootSelectorOrNode: apa saja): RootData { const pembersih = ngModule.injector.get(Sanitizer); const errorHandler = ngModule.injector.get(ErrorHandler); const renderer = rendererFactory.createRenderer(null, null); kembali { ngModul, injektor: elInjector, Node yang dapat diproyeksikan, selectorOrNode: rootSelectorOrNode, pembersih, penyajiPabrik, penyaji, penangan kesalahan, }; }
Memperkenalkan pohon elemen injektor karena desain ini relatif sederhana. Dengan mengubah hierarki injektor, hindari interleaving modul dan injektor komponen, yang mengakibatkan instanisasi ganda pada modul yang dimuat dengan lambat. Karena setiap injektor hanya memiliki satu induk, dan setiap resolusi harus menemukan satu injektor untuk mengambil dependensi.
Di Angular, tampilan adalah representasi dari template yang berisi berbagai jenis node, di antaranya adalah node elemen. Injektor elemen terletak di node ini:
antarmuka ekspor ElementDef { ... // Penyedia publik DI terlihat dalam tampilan ini publicProviders: {[tokenKey: string]: NodeDef}|null; // Sama seperti VisiblePublicProviders, tetapi juga mencakup penyedia swasta yang terletak pada elemen ini allProviders: {[tokenKey: string]: NodeDef}|null; }
ElementInjector
kosong secara default kecuali dikonfigurasi dalam atribut providers
@Directive()
atau @Component()
.
Saat Angular membuat injektor elemen untuk elemen HTML bersarang, Angular akan mewarisinya dari injektor elemen induk atau menugaskan injektor elemen induk langsung ke definisi simpul anak.
Jika injektor elemen pada elemen HTML turunan memiliki penyedia, maka elemen tersebut harus diwariskan. Jika tidak, tidak perlu membuat injektor terpisah untuk komponen turunan, dan dependensi dapat diselesaikan langsung dari injektor induk jika diperlukan.
Jadi, di manakah injektor elemen dan injektor modul mulai menjadi pohon paralel?
Kita telah mengetahui bahwa modul root aplikasi ( AppModule
) akan secara otomatis disertakan dalam root AppModule
saat membuat aplikasi baru menggunakan perintah new
CLI.
Ketika aplikasi ( ApplicationRef
) dimulai ( bootstrap
), entryComponent
dibuat:
RootData
RootView
], selectorOrNode, ngModule);
) , dan injektor elemen root akan dibuat, dengan elInjector
adalah Injector.NULL
.
Di sini, pohon injektor Angular dibagi menjadi pohon injektor elemen dan pohon injektor modul, dua pohon paralel ini.
Angular akan membuat injektor bawahan secara teratur. Setiap kali Angular membuat instance komponen providers
yang ditentukan dalam @Component()
, Angular juga akan membuat sub-injektor baru untuk instance tersebut. Demikian pula, ketika NgModule
baru dimuat saat runtime, Angular dapat membuat injektor untuknya dengan penyedianya sendiri.
Submodul dan injektor komponen tidak bergantung satu sama lain dan masing-masing membuat instance sendiri untuk layanan yang disediakan. Ketika Angular menghancurkan NgModule
atau instance komponen, Angular juga menghancurkan injektor ini dan instance layanan di injektor.
Di atas kami memperkenalkan dua jenis pohon injektor di Angular: pohon injektor modul dan pohon injektor elemen. Jadi, bagaimana cara Angular mengatasinya saat menyediakan dependensi?
Di Angular, ketika menyelesaikan token untuk mendapatkan dependensi komponen/instruksi, Angular menyelesaikannya dalam dua tahap:
untuk
ElementInjector
(induknya)ModuleInjector
(induknya).Injector - Aturan Resolusi):
Saat komponen mendeklarasikan ketergantungan, Angular akan mencoba memenuhi ketergantungan tersebut menggunakan ElementInjector
miliknya sendiri.
Jika injektor komponen tidak memiliki penyedia, ia akan meneruskan permintaan ke ElementInjector
komponen induknya.
Permintaan ini akan terus diteruskan hingga Angular menemukan injektor yang dapat menangani permintaan tersebut atau kehabisan leluhur ElementInjector
.
Jika Angular tidak dapat menemukan penyedia di ElementInjector
mana pun, Angular akan kembali ke elemen tempat permintaan dibuat dan mencari hierarki ModuleInjector
.
Jika Angular masih tidak dapat menemukan penyedianya, maka akan terjadi kesalahan.
Untuk tujuan ini, Angular memperkenalkan injektor gabungan khusus.
Injektor gabungan itu sendiri tidak memiliki nilai, ini hanya kombinasi definisi tampilan dan elemen.
kelas Injector_ mengimplementasikan Injector { konstruktor(tampilan pribadi: ViewData, elDef pribadi: NodeDef|null) {} dapatkan(token: apa saja, notFoundValue: apa saja = Injector.THROW_IF_NOT_FOUND): apa saja { const izinkan Layanan Pribadi = ini.elDef ? (ini.elDef.flags & NodeFlags.ComponentView) !== 0 : salah; kembalikan Layanan.resolveDep( this.view, this.elDef, izinkan Layanan Pribadi, {flags: DepFlags.None, token, tokenKey: tokenKey(token)}, notFoundValue); } }
Ketika Angular menyelesaikan dependensi, injektor gabungan adalah jembatan antara pohon injektor elemen dan pohon injektor modul. Ketika Angular mencoba menyelesaikan dependensi tertentu dalam suatu komponen atau arahan, ia menggunakan injektor gabungan untuk melintasi pohon injektor elemen dan kemudian, jika ketergantungan tidak ditemukan, beralih ke pohon injektor modul untuk menyelesaikan ketergantungan tersebut.
kelas ViewContainerRef_ mengimplementasikan ViewContainerData { ... //Permintaan untuk injektor elemen tampilan induk dapatkan parentInjector(): Injector { biarkan lihat = ini._lihat; biarkan elDef = this._elDef.parent; while (!elDef && lihat) { elDef = lihatParentEl(lihat); lihat = lihat.orang tua!; } kembalikan tampilan ? new Injector_(view, elDef) : new Injector_(this._view, null); } }Injektor
dapat diwariskan, artinya jika injektor yang ditentukan tidak dapat mengatasi ketergantungan, injektor induk akan meminta untuk menyelesaikannya. Algoritme penguraian spesifik diimplementasikan dalam metode resolveDep()
:
fungsi ekspor resolDep( lihat: ViewData, elDef: NodeDef, izinkanPrivateServices: boolean, depDef: DepDef, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any { // // mod1 // / // el1 mod2 // / //el2 // // Saat meminta el2.injector.get(token), periksa dan kembalikan nilai pertama yang ditemukan dengan urutan berikut: // - el2.injector.get(token, default) // - el1.injector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) -> jangan centang modul // - mod2.injector.get(token, default) }
Jika itu adalah komponen root AppComponent
dari template seperti <child></child>
, maka akan ada tiga tampilan di Angular:
<!-- HostView_AppComponent --> <aplikasi-saya></aplikasi-saya> <!-- Lihat_Komponen Aplikasi --> <anak></anak> <!-- Lihat_Komponen Anak --> Beberapa konten
bergantung pada proses parsing. Algoritme parsing akan didasarkan pada hierarki tampilan, seperti yang ditunjukkan pada gambar:
Jika beberapa token diselesaikan dalam komponen anak, Angular akan:
pertama-tama melihat injektor elemen anak, memeriksa elRef.element.allProviders|publicProviders
.
Kemudian ulangi semua elemen tampilan induk (1) dan periksa penyedia di injektor elemen.
Jika elemen tampilan induk berikutnya sama dengan null
(2), kembali ke startView
(3) dan periksa startView.rootData.elnjector
(4).
Hanya jika token tidak ditemukan, periksa startView.rootData module.injector
(5).
Oleh karena itu, Angular, ketika melintasi komponen untuk menyelesaikan dependensi tertentu, akan mencari elemen induk dari tampilan tertentu, bukan elemen induk dari elemen tertentu. Elemen induk tampilan dapat diperoleh melalui:
// Untuk tampilan komponen, ini adalah elemen host // Untuk tampilan tersemat, ini adalah indeks node induk dari fungsi ekspor wadah tampilan yang memuat viewParentEl(view: ViewData): NodeDef| nol { const parentView = lihat.parent; if (tampilan induk) { kembalikan view.parentNodeDef !.parent; } kalau tidak { kembalikan nol; } }
Artikel ini terutama memperkenalkan struktur hierarki injektor di Angular. Ada dua pohon injektor paralel di Angular: pohon injektor modul dan pohon injektor elemen.
Pengenalan pohon injektor elemen terutama untuk memecahkan masalah pembuatan instance ganda modul yang disebabkan oleh penguraian injeksi ketergantungan dan pemuatan modul yang lambat. Setelah pengenalan pohon injektor elemen, proses penguraian ketergantungan Angular juga telah disesuaikan. Ini memprioritaskan pencarian dependensi injektor seperti injektor elemen dan injektor elemen tampilan induk akan ditanyakan.