Angular5.x動態加載組件


Angular5.x動態加載組件

前言

在我們利用Angular進行開發的時候,會碰到這一樣的一個場景,隨着業務的不斷擴大,頁面里面的組件、modal、popup、越來越多的時候,頁面占用的開銷就越大,但是很多組件雖然初始化了,但是確只有在點擊的時候才會使用到。

為了解決這種情況,因而考慮用動態創建組件的方式來加載和使用組件。

1、未優化前的頁面組件情況

這邊以項目中碰到的例子舉例,場景是用戶管理下有一大堆用戶操作彈窗組件,分別在users.component.html

table-items.component.html 下,table-itemsusers下的子組件,用來顯示用戶數據。

<!--users.component.html-->

<new-user-popup #newUserPopup></new-user-popup>
<user-detail-popup #editUserPopup></user-detail-popup>
<update-superior (onSubmitSuccess)="refreshData()"></update-superior>
<add-tags-dialog #addTagsDialog></add-tags-dialog>
<update-role-dialog #updateRoleDialog></update-role-dialog>
<tags-filter-button #tagsFilterButton></tags-filter-button>
<goods-purchases-dialog #goodsPurchasesDialog (onSaveEmitter)="setGoodsPurchases($event)"></goods-purchases-dialog>
<custom-show-column #customColumn></custom-show-column>
<!--table-items.component.html-->

<add-tags-dialog #addTagsDialog></add-tags-dialog>
<add-groups-dialog #addGroupsDialog></add-groups-dialog>
<top-up-dialog #topUpDialog></top-up-dialog>
<give-vip-dialog #giveVipDialog></give-vip-dialog>
<give-coupon-dialog #giveCouponDialog></give-coupon-dialog>
<state-update-dialog #stateUpdateDialog (onSubmit)="refreshData()"></state-update-dialog>
<user-integral-dialog #userIntegralDialog [userList]="tableItemsService.userList"></user-integral-dialog>
<balance-dialog #balanceDialog (showDialog)="showTopDialog($event)"></balance-dialog>
<integral-dialog #integralDialog (showDialog)="showIntegralDialog($event)"></integral-dialog>
<consume-dialog #consumeDialog></consume-dialog>
<user-all-export-dialog #userAllExportDialog></user-all-export-dialog>
<update-role-dialog #updateRoleDialog></update-role-dialog>
<coupon-detail-dialog #couponDetailDialog (showGiveDialog)="showGiveCouponDialog($event)"></coupon-detail-dialog>
<times-card-dialog #timesCardDialog (showGiveDialog)="showGiveCouponDialog($event)"></times-card-dialog>
<allot-guide-dialog #allotGuideDialog (onChange)="tableItemsService.getUserList()"></allot-guide-dialog>

由上面數據來看,一個用戶管理列表下的組件就達到20幾個,當我們用戶數據量多的時候,渲染起來導致的卡頓是非常嚴重的,但是很多組件確並不是一直被使用的。之后在某一些操作之后才會使用到。

2、優化后的頁面組件情況

可以看到,我們將所有的組件初始化在頁面中去除,只留下一個動態容器,用來存放到時候創建的組件位置。

<!-- 組件容器,用於動態生成組件 -->

<ng-template #componentContainer></ng-template>

1、創建ViewContainerRef,用來存放視圖容器。

import { ViewContainerRef, ViewChild } from '@angular/core';

export class TableItemsComponent {
  
  // 這邊需要利用@ViewChild去獲取視圖容器,這邊有兩個參數,第一個參數是容器名或者組件名,第二個參數如果不添加就表示直接獲取組件或者元素。
  @ViewChild('componentContainer', { read: ViewContainerRef })
  public componentContainer: ViewContainerRef
}

2、使用ComponentFactoryResolver創建ComponentFactory組件工廠。

import { ViewContainerRef, ViewChild, ComponentFactoryResolver } from '@angular/core';

export class TableItemsComponent {

  // 這邊需要利用@ViewChild去獲取視圖容器,這邊有兩個參數,第一個參數是容器名或者組件名,第二個參數如果不添加就表示直接獲取組件或者元素。
  @ViewChild('componentContainer', { read: ViewContainerRef })
  public componentContainer: ViewContainerRef;
  
  constructor(
      public resolver: ComponentFactoryResolver
  ) {}
  
  // 創建組件方法
  public createComponent() {
    // 調用ComponentFactoryResolver中的resolveComponentFactory函數,這個函數會返回一個ComponentFactory對象,如下一段TS代碼所示
		const factory = this.resolver.resolveComponentFactory(Component);
	}
}
// class ComponentFactoryResolver
export declare abstract class ComponentFactoryResolver {
    static NULL: ComponentFactoryResolver;
    abstract resolveComponentFactory<T>(component: Type<T>): ComponentFactory<T>;
}

3、利用ViewContainerRef創建組件引用

import { ViewContainerRef, ViewChild, ComponentFactoryResolver } from '@angular/core';

export class TableItemsComponent {
  
  constructor(
      public resolver: ComponentFactoryResolver
  ) {}
  
  @ViewChild('componentContainer', { read: ViewContainerRef })
  public componentContainer: ViewContainerRef
  
  public createComponent() {
      const factory = this.resolver.resolveComponentFactory(Component);
      // 調用ViewContainerRef中的createComponent方法,這個方法會返回一個組件引用,如下一段TS代碼所示
      const componentRef = this.componentContainer.createComponent(factory);
   }
}
export declare abstract class ViewContainerRef {
  /**
     * Instantiates a single {@link Component} and inserts its Host View into this container at the
     * specified `index`.
     *
     * The component is instantiated using its {@link ComponentFactory} which can be obtained via
     * {@link ComponentFactoryResolver#resolveComponentFactory resolveComponentFactory}.
     *
     * If `index` is not specified, the new View will be inserted as the last View in the container.
     *
     * You can optionally specify the {@link Injector} that will be used as parent for the Component.
     *
     * Returns the {@link ComponentRef} of the Host View created for the newly instantiated Component.
     */
    abstract createComponent<C>(componentFactory: ComponentFactory<C>, index?: number, 	injector?: Injector, projectableNodes?: any[][], ngModule?: NgModuleRef<any>): ComponentRef<C>;
}

4、組合起來創建組件實例

import { ViewContainerRef, ViewChild, ComponentFactoryResolver } from '@angular/core';

// 組件導入
import { AddTagsDialogComponent } from '../add-tags-dialog/add-tags-dialog.component';
import { AddGroupsDialogComponent } from '../add-groups-dialog/add-groups-dialog.component';
import { TopUpDialogComponent } from '../top-up-dialog/top-up-dialog.component';
import { GiveVipDialogComponent } from '../give-vip-dialog/give-vip-dialog.component';
import { GiveVipCouponComponent } from '../give-coupon-dialog/give-coupon-dialog.component';
import { StateUpdateDialogComponent } from '../state-update-dialog/state-update-dialog.component';
import { UserIntegralDialogComponent } from '../user-integral-dialog/user-integral-dialog.component';
import { BalanceDialogComponent } from '../balance-dialog/balance-dialog.component';
import { IntegralDialogComponent } from '../integral-dialog/integral-dialog.component';
import { ConsumeDialogComponent } from '../consume-dialog/consume-dialog.component';
import { UserAllExportDialogComponent } from '../user-all-export-dialog/user-all-export-dialog.component';
import { UpdateRoleDialogComponent } from '../update-role-dialog/update-role-dialog.component';
import { CouponDetailComponent } from '../coupon-detail-dialog/coupon-detail-dialog.component';
import { TimesCardDialogComponent } from '../times-card-dialog/times-card-dialog.component';
import { AllotGuideDialogComponent } from '../allot-guide-dialog/allot-guide-dialog.component';
import { IncreaseTagsComponent } from '../increase-tags/increase-tags.component';

export class TableItemsComponent {
  
  @ViewChild('componentContainer', { read: ViewContainerRef })
  public componentContainer: ViewContainerRef
  
  constructor(
		public resolver: ComponentFactoryResolver
  ) {}
  
  // 以下只是個人的一些實現,並不算太好,這邊參考就好,主要提供一個思路。
  
  /**
   * @desc 創建組件
   * @param {string} type - 組件名
   */
  public createComponent(type: string): any {
    // 清空容器
    this.componentContainer.clear();
    const COMPONENT_OBJ = {
      addTagsDialog: {
        instance: AddTagsDialogComponent,
        input: [],
        output: []
      },
      addGroupsDialog: {
        instance: AddGroupsDialogComponent,
        input: [],
        output: []
      },
      topUpDialog: {
        instance: TopUpDialogComponent,
        input: [],
        output: []
      },
      giveVipDialog: {
        instance: GiveVipDialogComponent,
        input: [],
        output: []
      },
      giveCouponDialog: {
        instance: GiveVipCouponComponent,
        input: [],
        output: []
      },
      stateUpdateDialog: {
        instance: StateUpdateDialogComponent,
        input: [],
        output: [{
          origin: 'onSubmit',
          callFn: this.refreshData
        }]
      },
      userIntegralDialog: {
        instance: UserIntegralDialogComponent,
        input: [{
          origin: 'userList',
          to: this.tableItemsService.userList
        }],
        output: []
      },
      balanceDialog: {
        instance: BalanceDialogComponent,
        input: [],
        output: [{
          origin: 'showDialog',
          callFn: this.showTopDialog
        }]
      },
      integralDialog: {
        instance: IntegralDialogComponent,
        input: [],
        output: [{
          origin: 'showDialog',
          callFn: this.showIntegralDialog
        }]
      },
      consumeDialog: {
        instance: ConsumeDialogComponent,
        input: [],
        output: []
      },
      userAllExportDialog: {
        instance: UserAllExportDialogComponent,
        input: [],
        output: []
      },
      updateRoleDialog: {
        instance: UpdateRoleDialogComponent,
        input: [],
        output: []
      },
      couponDetailDialog: {
        instance: CouponDetailComponent,
        input: [],
        output: [{
          origin: 'showGiveDialog',
          callFn: this.showGiveCouponDialog
        }]
      },
      timesCardDialog: {
        instance: TimesCardDialogComponent,
        input: [],
        output: [{
          origin: 'showGiveDialog',
          callFn: this.showGiveCouponDialog
        }]
      },
      allotGuideDialog: {
        instance: AllotGuideDialogComponent,
        input: [],
        output: [{
          origin: 'onChange',
          callFn: this.tableItemsService.getUserList
        }]
      },
      increaseTags: {
        instance: IncreaseTagsComponent,
        input: [],
        output: [{
          origin: 'onSaveSuccess',
          callFn: this.refreshData
        }]
      }
    }[type];

    const factory = this.resolver.resolveComponentFactory(COMPONENT_OBJ.instance);
    const componentRef = this.componentContainer.createComponent(factory);

    // @Input 輸入屬性處理
    if (COMPONENT_OBJ['input'].length) {
      COMPONENT_OBJ['input'].forEach((item: any) => {
        componentRef.instance[item.origin] = item.to;
      });
    }
    // @Output 輸出方法處理
    if (COMPONENT_OBJ['output'].length) {
      COMPONENT_OBJ['output'].forEach((item: any) => {
        componentRef.instance[item.origin].subscribe(($event: any) => {
          item.callFn.bind(this)($event); // bind解決this指向問題
        });
      });
    }
    // 返回組件實例
    return componentRef.instance;
  }
}

5、NgModule添加entryComponents

@NgModule({
  entryComponents: [
    NewUserPopupComponent,
    UserDetailPopupComponent,
    UpdateSuperiorComponent,
    GoodsPurchasesDialogComponent,
    CustomShowColumnComponent,
    TagsManagementComponent,
    TagsChooseDialogComponent,
    AddTagsDialogComponent,
    AddGroupsDialogComponent,
    TopUpDialogComponent,
    GiveVipDialogComponent,
    GiveVipCouponComponent,
    StateUpdateDialogComponent,
    UserIntegralDialogComponent,
    BalanceDialogComponent,
    IntegralDialogComponent,
    ConsumeDialogComponent,
    UserAllExportDialogComponent,
    UpdateRoleDialogComponent,
    CouponDetailComponent,
    TimesCardDialogComponent,
    AllotGuideDialogComponent,
    IncreaseTagsComponent
  ]
})

6、創建並且動態生成組件再調用,減少頁面開銷

<!--調用createComponent函數返回函數實例后調用其pop方法用來創建並且展示組件-->
<li nz-menu-item (click)="createComponent('giveCouponDialog').pop(true)">
	<span>贈送優惠券</span>
</li>

3、總結

  1. 通過ViewContainerRef創建容器視圖。
  2. 使用ComponentFactoryResolver創建ComponentFactory組件工廠。
  3. 利用ViewContainerRef.createComponent創建組件引用,通過instance引用組件實例。

通過動態組件組件,在視圖容器中每次只存在一個組件,每次點擊生成組件的時候才在容器里面插入組件,並不是一開始就把所有的組件渲染之后等着調用。這樣大大的提升了頁面加載的速度和使用的性能。

但是要注意的是,通過這樣方式動態生成的組件,Input是不會隨着OnChanges監聽輸入屬性和的生命周期去檢測輸入屬性變化的,因此在使用這種動態組件的時候,要考慮組件是否有經常變動的Input,在變動的時候要手動去更新里面的值。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM