2

Angular4 动态加载组件杂谈

 1 year ago
source link: https://blog.dteam.top/posts/2017-07/angular4-%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD%E7%BB%84%E4%BB%B6%E6%9D%82%E8%B0%88.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Angular4 动态加载组件杂谈

胡伟红 Posted at — Jul 6, 2017 阅读 134

最近接手了一个项目,客户提出了一个高大上的需求:要求只有一个主界面,所有组件通过 Tab 来显示。其实这个需求并不诡异,不喜欢界面跳转的客户都非常热衷于这种展现形式。

好吧,客户至上,搞定它!这种实现方式在传统的 HTML 应用中,非常简单,只是在这 Angular4(以下简称 ng)中,咋个弄呢?

我们先来了解下 ng 中动态加载组件的两种方式:

  1. 加载已经声明的组件: 使用ComponentFactoryResolver,将一个组件实例呈现到另一个组件视图上;
  2. 动态创建组件并加载:使用 ComponentFactory 和 Compiler,创建和呈现组件

根据我们的需求,各个组件是事先开发好的,需要在同一个组件上显示出来。所以第一种方式符合我们的要求。

使用 ComponentFactoryResolver 动态加载组件,需要先了解如下概念:

  1. ViewChild:属性装饰器,通过它可以获得视图上对应的元素;
  2. ViewContainerRef:视图容器,可在其上创建、删除组件;
  3. ComponentFactoryResolver:组件解析器,可以将一个组件呈现在另一个组件的视图上。

搞明白了概念,看看代码吧!

HTML 代码:

<dynamic-container [componentName]="'RoleComponent'"> </dynamic-container>

ts 代码:

import {Component, Input, ViewContainerRef, ViewChild,ComponentFactoryResolver,ComponentRef,OnDestroy,OnInit} from '@angular/core';
import {RoleComponent} from "./role/role.component";
@Component({
  selector: 'dynamic-container',
  entryComponents: [RoleComponent,....],  //需要动态加载的组件名,这里一定要指定,否则报错
  template: "<ng-template #container></ng-template>"
})
export class DynamicComponent implements OnDestroy,OnInit {
  @ViewChild('container', { read: ViewContainerRef }) container: ViewContainerRef;
    @Input() componentName      //需要加载的组件名
    compRef: ComponentRef<any>; //  加载的组件实例
    constructor(private resolver: ComponentFactoryResolver) {}
    loadComponent() {
      let factory = this.resolver.resolveComponentFactory(this.componentName);
      if (this.compRef) {
        this.compRef.destroy();
        }
      this.compRef = this.container.createComponent(factory) //创建组件
    }
    ngAfterContentInit() {
      this.loadComponent()
    }
    ngOnDestroy() {
      if(this.compRef){
          this.compRef.destroy();
      }
    }
}

代码的确不复杂!

可是,如果加载的组件有传入的参数,比如修改角色组件,需要传入角色 id,该怎么办呢?有办法解决,使用 ReflectiveInjector(依赖注入),在加载组件时将需要传入的参数注入到组件中。代码调整如下:

HTML 代码,增加了 inputs 参数,其值为参数值对:

<dynamic-container
  [componentName]="'RoleComponent'"
  [inputs]="{'myName':'dynamic'}"
></dynamic-container>

ts 代码:

import { ReflectiveInjector} from '@angular/core';
......
export class DynamicComponent implements OnDestroy,OnInit {

    @Input() inputs:any         //加载组件需要传入的参数组
    .......
    loadComponent() {
      let factory = this.resolver.resolveComponentFactory(this.componentName);
      if(!this.inputs)
          this.inputs={}

      let inputProviders = Object.keys(this.inputs).map((inputName) => {
          return {provide: inputName, useValue: this.inputs[inputName]};});
      let resolvedInputs = ReflectiveInjector.resolve(inputProviders);
      let injector = ReflectiveInjector.fromResolvedProviders(resolvedInputs, this.container.parentInjector);
      if (this.compRef) {
        this.compRef.destroy();
        }
      this.compRef = factory.create(injector) //创建带参数的组件
      this.container.insert(this.compRef.hostView);//呈现组件的视图
  }
  ngAfterContentInit() {
    this.loadComponent()
  }
  ......
}
////RoleComponent代码如下
export class RoleComponent implements OnInit {
    myName:string
    ........
    constructor(){
        //this.myName的值为dynamic
  }
}

到此,动态加载组件的界面骄傲滴显示在界面上。等等,貌似哪里不对!为什么界面上从后台获取的数据没有加载?

获取数据的代码如下:

export class RoleComponent implements OnInit {
  roleList=[];
  ......
  constructor(private _roleService.list:RoleService) {
      this._roleService.list().subscribe(res=>{
          this.roleList=res.roleList;
      });
  }
  ......
}

经过反复测试,得出结论如下:从后台通过 HTTP 获取的数据已经获得,只是没有触发 ng 进行变更检测,所以界面没有渲染出数据。

抱着“遇坑填坑”的信念,研习 ng 的文档,发现 ng 支持手动触发变更检测,只要在适当的位置调用变更检测即可。同时,ng 提供了不同级别的变更检测:

  • 变更检测策略:
    • Default :ng 提供的 Default 的检测策略,只要组件的 input 发生改变,就触发检测;
    • OnPush :OnPush 检测策略是 input 发生改变,并不立即触发检测,而是输入的引用发生变化时,才会触发检测。
  • ChangeDetectorRef.detectChanges():可显式的控制变更检测,在需要的地方使用即可;
  • NgZone.run():在整个应用中进行变更检测
  • ApplicationRef.tick():在整个应用中进行变更检测,侦听 NgZone 的 onTurnDone 事件,来触发检测

根据文档显示,ng 应用缺省就在使用 NgZone 来检测变更,这对于正常加载的组件是没有问题的,但是对于动态加载的组件却不起作用。几次试验下来,唯有第二种方法起作用:显式调用 ChangeDetectorRef.detectChanges()

于是修改 ts 代码:

interval:any
loadComponent() {
    ......
    this.interval=setInterval(() => {
      this.compRef.changeDetectorRef.detectChanges();
    }, 50);  //50毫秒检测一次变更
}
ngOnDestroy() {
    ......
    clearInterval(this.interval)
}

鉴于本人的 ng 技能尚浅,就用这种笨拙的方法解决了数据加载问题,但是如鲠在喉,总觉应该还有更优雅的解决方法,待我再花时日研究下。

啰嗦至此,文中如有不妥之处,欢迎各位看官指正。

补充一句,强烈推荐PrimeNG,它提供了丰富的前端组件,可以方便取用,大大节省了界面的开发速度。

参考文献:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK