در نسخههای قبل از Angular CLI 6.0، صرفا امکان Bundle کردن جداگانهی ماژولهایی که در قسمت loadChildren مرتبط با تنظیمات مسیریابی ذکر شده بودند، وجود داشت. بنابراین در برخی از شرایط اگر نیاز به امکان بارگذاری ماژولی به صورت Lazy load بود، باید از سیستم مسیریابی استفاده میشد یا اینکه با یکسری ترفند، CLI و Webpack را مجبور به ساخت فایل chunk جداگانه برای ماژول مورد نظر میکردید. از زمان انتشار Angular CLI 6.0 امکان Lazy loading پویا نیز مهیا میباشد؛ به این ترتیب بدون وابستگی به سیستم مسیریابی، باز هم میتوان از مزایای Lazy loading بهره برد. در این مطلب روش استفاده از این قابلیت و همچنین نحوهی بارگذاری پویای یک کامپوننت مرتبط با یک ماژول Lazy load شده را بررسی خواهیم کرد. برای این منظور در ادامه با ایجاد یک TabLayout با استفاده از Angular Material Tabs با یکی از موارد پر استفادهی قابلیت مذکور آشنا خواهیم شد.
پیش نیازها
- مسیر آموزشی «+AngularJS 2.0»
- مسیر آموزشی «سری آموزشی Angular CLI»
- مسیر آموزشی «مسیریابی در Angular»
- مسیر آموزشی «کتابخانه Angular Material 6x»
کار را با طراحی و پیاده سازی TabService شروع میکنیم. برای این منظور یک سرویس را در فولدر services موجود در کنار CoreModule ایجاد خواهیم کرد؛ به این جهت ابتدا مدلهای زیر را خواهیم داشت:
import { Type, ValueProvider } from '@angular/core'; export interface OpenNewTabModel { label: string; componentType: Type<any>; iconName: string; modulePath?: string; data?: ValueProvider[]; }
import { TabItemComponent } from './tab-item-component'; export interface TabItem { label: string; iconName: string; component: TabItemComponent; }
OpenNewTabModel برای ارسال داده توسط مصرف کننده از این سرویس در نظر گرفته شده است. ولی واسط TabItem دارای خصوصیاتی میباشد که ما برای نمایش یک برگهی جدید نیازمندیم. TabItemComponent نیز دارای خصوصیاتی است که مورد نیاز دایرکتیو« NgComponentOutlet» است.
import { Injector, NgModuleFactory, Type } from '@angular/core'; export interface TabItemComponent { componentType: Type<any>; moduleFactory?: NgModuleFactory<any>; injector: Injector; }
import { BehaviorSubject, Observable } from 'rxjs'; import { Injectable, Injector, NgModuleFactory, NgModuleFactoryLoader } from '@angular/core'; import { OpenNewTabModel } from '../models/open-new-tab-model'; import { TabItem } from '../models/tab-item'; @Injectable({ providedIn: 'root' }) export class TabService { private tabItemSubject: BehaviorSubject<TabItem[]> = new BehaviorSubject< TabItem[] >([]); constructor( private loader: NgModuleFactoryLoader, private injector: Injector ) {} get tabItems$(): Observable<TabItem[]> { return this.tabItemSubject.asObservable(); } open(newTab: OpenNewTabModel) { if (newTab.modulePath) { this.loader .load(newTab.modulePath) .then((moduleFactory: NgModuleFactory<any>) => { this.openInternal(newTab, moduleFactory); }); } else { this.openInternal(newTab); } } private openInternal(newTab: OpenNewTabModel, moduleFactory?: NgModuleFactory<any>) { const newTabItem: TabItem = { label: newTab.label, iconName: newTab.iconName, component: { componentType: newTab.componentType, moduleFactory: moduleFactory, injector: newTab.data ? Injector.create(newTab.data, this.injector) : this.injector } }; this.tabItemSubject.getValue().push(newTabItem); this.tabItemSubject.next(this.tabItemSubject.getValue()); } close(index: number) { this.tabItemSubject.getValue().splice(index, 1); this.tabItemSubject.next(this.tabItemSubject.getValue()); } }
{ provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader }
import { Component, OnInit } from '@angular/core'; import { TabService } from './../../../services/tab.service'; @Component({ selector: 'app-tabs', templateUrl: './tabs.component.html', styleUrls: ['./tabs.component.scss'] }) export class TabsComponent implements OnInit { constructor(public service: TabService) {} ngOnInit() {} }
<mat-tab-group> <mat-tab *ngFor="let tabItem of (service.tabItems$ | async); let index = index" > <ng-template mat-tab-label> <mat-icon class="icon" aria-label="icon for tab" >{{tabItem.iconName}}</mat-icon> <span class="full">{{ tabItem.label }}</span> <mat-icon class="close" (click)="service.close(index)" aria-label="close tab button" >close</mat-icon > <!-- </button> --> </ng-template> <ng-container *ngIf="tabItem.component.moduleFactory"> <ng-container *ngComponentOutlet=" tabItem.component.componentType; ngModuleFactory: tabItem.component.moduleFactory; injector: tabItem.component.injector " > </ng-container> </ng-container> <ng-container *ngIf="!tabItem.component.moduleFactory"> <ng-container *ngComponentOutlet=" tabItem.component.componentType; injector: tabItem.component.injector " > </ng-container> </ng-container> </mat-tab> </mat-tab-group>
در تکه کد بالا، ابتدا با استفاده از وهله تزریق شده TabService در کامپوننت مذکور، به شکل زیر، مشترک تغییرات لیست برگهها شدهایم و با استفاده از دایرکتیو *ngFor به ازای تک تک tabItemهای درخواست شده برای گشوده شدن، به شکل زیر کار وهله سازی پویا از کامپوننت مشخص شده انجام میشود:
<ng-container *ngComponentOutlet="tabItem.component.componentType; ngModuleFactory: tabItem.component.moduleFactory; injector: tabItem.component.injector"> </ng-container>
خوب، با استفاده از آنچه تا اینجای مطلب بررسی شد، میتوان یک سیستم راهبری مبتنی بر Tab را نیز برپا کرد که مطلب جدایی را میطلبد. برای تکمیل مکانیزم بارگذاری پویای ماژولها، نیاز است تا مسیر ماژول مورد نظر را در فایل angular.json و بخش lazyModules به شکل زیر معرفی کنید:
"build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist/MaterialAngularTabLayout", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "src/tsconfig.app.json", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", "src/styles.scss" ], "lazyModules": [ "src/app/lazy/lazy.module" ], "scripts": [] },
به عنوان مثال قصد داریم ماژول LazyModule را به صورت پویا بارگذاری کرده و LazyComponent موجود در این ماژول را به صورت پویا در برگهی جدیدی نمایش دهیم. برای این منظور کدهای فایل AppComponent.ts را به شکل زیر تغییر خواهیم داد:
import { Component } from '@angular/core'; import { IdModel } from './core/models/id-model'; import { LazyComponent } from './lazy/lazy.component'; import { OpenNewTabModel } from './core/models/open-new-tab-model'; import { TabService } from './core/services/tab.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent { title = 'MaterialAngularTabLayout'; constructor(private tabService: TabService) {} loadLazyComponent() { this.tabService.open(<OpenNewTabModel>{ label: 'Loaded Lazy Component', iconName: 'thumb_up', componentType: LazyComponent, modulePath: 'src/app/lazy/lazy.module#LazyModule', data: [{ provide: IdModel, useValue: <IdModel>{ id: 1 } }] }); } }
در تکه کد بالا با تزریق TabService به سازندهی آن، قصد گشودن برگهی جدیدی را توسط متد open آن، داریم. در بدنهی متد loadLazyComponent یک شیء با قرارداد OpenNewTabModel ایجاد شده و به عنوان آرگومان به متد open ارسال شده است. توجه داشته باشید که modulePath اینجا نیز به مانند خصوصیت loadChildren مرتبط با اشیاء مسیریابی، باید مقدار دهی شود. همچنین قصد داشتیم اطلاعاتی را نیز به کامپوننت مورد نظر ارسال کنیم؛ همانند مکانیزم مسیریابی که با پارامترها این چنین کارهایی صورت میپذیرد. در اینجا از یک کلاس به شکل زیر استفاده شدهاست:
export class IdModel { constructor(public id: number) {} }
در این صورت پیاده سازی LazyComponent نیز به شکل زیر خواهد بود:
import { Component, OnInit } from '@angular/core'; import { IdModel } from './../core/models/id-model'; @Component({ selector: 'app-lazy', templateUrl: './lazy.component.html', styleUrls: ['./lazy.component.scss'] }) export class LazyComponent implements OnInit { constructor(private model: IdModel) {} ngOnInit() { console.log(this.model); } }
البته فراموش نکنید کامپوننتی را که نیاز است به صورت پویا بارگذاری شود، در قسمت entryComponents مرتبط با NgModule متناظر به شکل نیز معرفی کنید:
import { CommonModule } from '@angular/common'; import { LazyComponent } from './lazy.component'; import { NgModule } from '@angular/core'; @NgModule({ imports: [CommonModule], declarations: [LazyComponent], entryComponents: [LazyComponent] }) export class LazyModule {}
با خروجی زیر:
و chunk تولید شده برای ماژول مورد نظر:
در صورتیکه در حالت production پروژه را بیلد کنید، هش مرتبط برای chunk تولید شده نیز ایجاد خواهد شد.
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید.