Angular4 动态加载组件杂谈

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

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

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

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

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

使用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提供了不同级别的变更检测:

  1. 变更检测策略:

    Default :ng提供的Default的检测策略,只要组件的input发生改变,就触发检测;
      OnPush :OnPush检测策略是input发生改变,并不立即触发检测,而是输入的引用发生变化时,才会触发检测。
  2. ChangeDetectorRef.detectChanges():可显式的控制变更检测,在需要的地方使用即可;
  3. NgZone.run():在整个应用中进行变更检测
  4. 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,它提供了丰富的前端组件,可以方便取用,大大节省了界面的开发速度。

参考文献:

  1. Angular 2 Change Detection, Zones and an example
  2. Angular change detection explained
  3. 深入理解Angular2变化监测和ngZone

DTeam的团队日志
请关注我们的团队Blog: [链接] 。DTeam 始建于 2017 年 1 月,是一支专注于高效研发效能的技术团队,我...
391 声望
88 粉丝
0 条评论
推荐阅读
Nginx SSL快速双向认证配置(脚本)
原文链接 目前遇到一个项目有安全性要求,要求只有个别用户有权限访问。本着能用配置解决就绝不用代码解决的原则,在Nginx上做一下限制和修改即可。 这种需求其实实现方式很多,经过综合评估考虑,觉得SSL双向认...

DTeam9阅读 7.2k

JavaScript有用的代码片段和trick
平时工作过程中可以用到的实用代码集棉。判断对象否为空 {代码...} 浮点数取整 {代码...} 注意:前三种方法只适用于32个位整数,对于负数的处理上和Math.floor是不同的。 {代码...} 生成6位数字验证码 {代码...} ...

jenemy49阅读 7.3k评论 12

再也不学AJAX了!(二)使用AJAX ① XMLHttpRequest
「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第二篇,最近更新于 2023 年 1...

libinfs42阅读 7k评论 12

封面图
「多图预警」完美实现一个@功能
一天产品大大向 boss 汇报完研发成果和产品业绩产出,若有所思的走出来,劲直向我走过来,嘴角微微上扬。产品大大:boss 对我们的研发成果挺满意的,balabala...(内心 OS:不听,讲重点)产品大大:咱们的客服 I...

wuwhs32阅读 3.5k评论 5

封面图
安全地在前后端之间传输数据 - 「3」真的安全吗?
在「2」注册和登录示例中,我们通过非对称加密算法实现了浏览器和 Web 服务器之间的安全传输。看起来一切都很美好,但是危险就在哪里,有些人发现了,有些人嗅到了,更多人却浑然不知。就像是给门上了把好锁,还...

边城29阅读 6.4k评论 5

封面图
2022大前端总结和2023就业分析
我在年前给掘金平台分享了《2022年热点技术盘点》的前端热点,算是系统性的梳理了一下我自己对前端一整年的总结。年后,在知乎上看到《前端的就业行情怎么样?》,下面都是各种唱衰前端的论调,什么裁员,外包化...

i5ting27阅读 2.3k评论 4

封面图
深入理解React Diff算法
fiber上的updateQueue经过React的一番计算之后,这个fiber已经有了新的状态,也就是state,对于类组件来说,state是在render函数里被使用的,既然已经得到了新的state,那么当务之急是执行一次render,得到持有新...

nero31阅读 11.8k评论 3

391 声望
88 粉丝
宣传栏