angular10系列2:基本概念-组件

1,033 阅读8分钟

组件

组件构成要素

  • html模板
  • typescript,定义行为
  • css组,可以引入多个css文件,因此可以在一个组件里面定义多个css文件
//从angular主模块中引入Component(组件装饰器或组件注解)
import { Component } from '@angular/core';

//装饰器中以json的形式声明元数据
@Component({
  //它指定了一个叫 <app-root> 的元素。 该元素是 index.html 文件里的一个占位符
  //为什么这个组件跟入口index建立了联系呢?因为入口main.ts中绑定了主模块为appModule
  selector: 'app-root',   //在模板中找对应的标签,找到后创建并插入该组件实例
  templateUrl: './app.component.html',  // html模板
  styleUrls: ['./app.component.css'],   // css样式,可以引入多个css文件
  // 这个属性(内联模板)和templateUrl(外联模板)二选一,template后面可以直接跟html字符串
  // 注意在模板语法(反引号)中是使用插值表达式,不能使用${}插入值
  template: `<h1>{{title}}</h1>`   
})
//组件控制器,写逻辑代码的地方
export class AppComponent {
  title = 'myAngular';
  //构造函数可以用来进行属性的声明和初始化语句
  //在angular里面有个特别重要的点要记住:只能通过构造函数注入依赖
  constructor() {}
}

先决条件

  • 安装 Angular CLI (npm install -g @angular/cli)
  • 建立一个 Angular 项目 (ng new )

如果不满足这两个条件,请参考 搭建环境。

如何创建一个组件

使用 Angular CLI 创建一个组件

ng generate component <project-name> // 创建一个组件

ng g c <project-name> // 缩写

常用的创建组件的一些其他选项

ng g c <project-name>  --skip-tests // 创建一个组件,且不用安装测试文件

ng g c <project-name> --inline-style // 缩写-s,内联样式

ng g c <project-name> --inline-template // 缩写-t,内联模板

ng g c <project-name> --module= <module-name> // 指定创建的组件在哪个模块引用,在多个模块的项目中会用到

除了通过angular cli自动生成组件外,也可以手动创建组件(不推荐),这里不介绍。

组件的生命周期(高阶内容,掌握ngOnInit、ngOnDestroy、ngOnChanges、ngAfterViewInit())

在工作中经常能够用到的生命周期就两个(ngOninit和ngOnDestroy),其他的生命周期很少用到;但是如果能够掌握组件的生命周期,对angular会掌握的得更加深刻。

生命周期含义

当 Angular 实例化组件类并渲染组件视图及其子视图时,组件实例的生命周期就开始了。生命周期一直伴随着变更检测,Angular 会检查数据绑定属性何时发生变化,并按需更新视图和组件实例。当 Angular 销毁组件实例并从 DOM 中移除它渲染的模板时,生命周期就结束了。当 Angular 在执行过程中创建、更新和销毁实例时,指令就有了类似的生命周期。

应用:

你的应用可以使用生命周期钩子方法来触发组件或指令生命周期中的关键事件,以初始化新实例,需要时启动变更检测,在变更检测过程中响应更新,并在删除实例之前进行清理。

如何实现生命周期事件

每个组件或指令都可以实现一个或多个生命周期钩子,这些生命周期钩子可以在适当的时候对组件或指令实例进行操作。

每个接口都有唯一的一个钩子方法,它们的名字是由接口名再加上 ng 前缀构成的。比如,OnInit 接口的钩子方法叫做 ngOnInit()。如果你在组件或指令类中实现了这个方法,Angular 就会在首次检查完组件或指令的输入属性后,紧接着调用它

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  styleUrls: ['./app.component.css'],  
  template: `<h1>{{title}}</h1>`   
})
// 实现OnInit生命周期,可以实现多个生命周期
export class AppComponent implements OnInit{ 
  title = 'myAngular';
  constructor() {}
  ngOnInit(){
      console.log("Angular在首次检查完组件的输入属性后,然后调用它,只调用一次")
  }
}

生命周期概览

钩子时机用途注意
ngOnChanges()当被绑定的输入属性的值发生变化时调用,首次调用一定会发生在 ngOnInit() 之前当 Angular 设置或重新设置数据绑定的输入属性时响应。 该方法接受当前和上一属性值的 SimpleChanges 对象这发生的非常频繁,所以你在这里执行的任何操作都会显著影响性能。
ngOnInit()在第一轮 ngOnChanges() 完成之后调用,只调用一次。在 Angular 第一次显示数据绑定和设置指令/组件的输入属性之后,初始化指令/组件。组件获取初始数据的好地方很重要,只调用一次
ngDoCheck()紧跟在每次执行变更检测时的 ngOnChanges() 和 首次执行变更检测时的 ngOnInit() 后调用。检测,并在发生 Angular 无法或不愿意自己检测的变化时作出反应。它和ngOnChanges一样发生的很频繁
ngAfterContentInit()第一次 ngDoCheck() 之后调用,只调用一次。当 Angular 把外部内容投影进组件视图或指令所在的视图之后调用。只调用一次
ngAfterContentChecked()ngAfterContentInit() 和每次 ngDoCheck() 之后调用每当 Angular 检查完被投影到组件或指令中的内容之后调用。
ngAfterViewInit()第一次 ngAfterContentChecked() 之后调用,只调用一次。初始化完组件视图及其子视图之后调用。只调用一次
ngAfterViewChecked()ngAfterViewInit() 和每次 ngAfterContentChecked() 之后调用。每次做完组件视图和子视图的变更检测之后调用。
ngOnDestroy()在 Angular 销毁指令/组件之前调用。当 Angular 每次销毁指令/组件之前调用并清扫。 在这儿反订阅可观察对象和分离事件处理器,以防内存泄漏。
取消订阅可观察对象、
清除定时器、
反注册该指令在全局或应用服务中注册过的所有回调。
很重要

重点生命周期详解

初始化组件和指令 ngOnInit

  • 在构造函数外部执行复杂的初始化。组件的构造应该既便宜又安全。比如,你不应该在组件构造函数中获取数据。当在测试中创建组件时或者决定显示它之前,你不应该担心新组件会尝试联系远程服务器。ngOnInit() 是组件获取初始数据的好地方
  • 在 Angular 设置好输入属性之后设置组件。构造函数应该只把初始局部变量设置为简单的值。请记住,只有在构造完成之后才会设置指令的数据绑定输入属性。如果要根据这些属性对指令进行初始化,请在运行 ngOnInit() 时设置它们

在实例销毁时进行清理 ngOnDestroy

这里是释放资源的地方,这些资源不会自动被垃圾回收。如果你不这样做,就存在内存泄漏的风险。

  • 取消订阅可观察对象和 DOM 事件。
  • 停止 interval 计时器。
  • 反注册该指令在全局或应用服务中注册过的所有回调。
  • ngOnDestroy() 方法也可以用来通知应用程序的其它部分,该组件即将消失

页面埋点

可以在组件中通过ngOnInit和ngOnDestroy方法统计页面的时长,

更好的做法可以通过指令实现ngOnInit和ngOnDestroy生命周期,用来统计页面时长

也可以通过路由守卫来记录页面出入栈所需要的时间

@Directive({selector: '[appSpy]'})
export class SpyDirective implements OnInit, OnDestroy {

  constructor(private logger: LoggerService) { }

  ngOnInit()    { this.logIt(`onInit`); }

  ngOnDestroy() { this.logIt(`onDestroy`); }

  private logIt(msg: string) {
    this.logger.log(`Spy #${nextId++} ${msg}`);
  }
}

使用变更检测钩子 ngOnchanges

一旦检测到该组件或指令的输入属性发生了变化,Angular 就会调用它的 ngOnChanges() 方法。

因为这个方法会频繁执行,所以要注意判断的计算量,会影响性能。

// 可以监听输入属性的变化
ngOnChanges(changes: SimpleChanges) {
  for (const propName in changes) {
    const chng = changes[propName];
    const cur  = JSON.stringify(chng.currentValue);
    const prev = JSON.stringify(chng.previousValue);
    this.changeLog.push(`${propName}: currentValue = ${cur}, previousValue = ${prev}`);
  }
}

组件之间的通信(重要,必须掌握)

一、父子组件的通信(基础)

父传子

1、父组件通过属性绑定向子组件传值

import { Component } from '@angular/core';
@Component({
  selector: 'app-parent',
  template: `
    <app-child [msg]="msg"></app-child>
  `
})
export class ParentComponent {
  msg = '父组件传的值';
}

2、子组件通过@Input接收数据

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-child',
  template: `
    <p>{{msg}}</p>
  `
})
export class ChildComponent {
  @Input() msg: String;
}
子传父

1、子组件通过自定义事件,向父组件发送数据

import { Component, Input, EventEmitter, Output} from '@angular/core';
,
@Component({
  selector: 'app-child',
  template: `
    <p>{{msg}}</p>
    <button (click)="vote()" >发送</button>
  `
})
export class ChildComponent {
  @Input() msg: String;
  @Output() voted = new EventEmitter<boolean>();
  
  vote() {
    this.voted.emit("子组件传的值");
  }
}

2、父组件通过监听自定义事件,接收子组件的传值

import { Component } from '@angular/core';
@Component({
  selector: 'app-parent',
  template: `
    <app-child [msg]="msg" (voted)="voted($event)"></app-child>
  `
})
export class ParentComponent {
  msg = '父组件传的值';
  voted(val){ //监听自定义事件的传值
      console.log(val)
  }
}
子组件怎么监听输入属性值的变化?(2种方法)

1、可以使用一个输入属性@Input()的 setter,以拦截父组件中值的变化。

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-child',
  template: '<h3>"{{name}}"</h3>'
})
export class ChildComponent {
  @Input()
  get name(): string { return this._name; }
  set name(name: string) {
    this._name = (name && name.trim()) || '<no name set>';
  }
  private _name = '';
}

2、通过ngOnChange()来截听输入属性值的变化

当需要监视多个、交互式输入属性的时候,本方法比用属性的 setter 更合适。

import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';

@Component({
  selector: 'app-version-child',
  template: `
    <h3>Version {{major}}.{{minor}}</h3>
    <h4>Change log:</h4>
    <ul>
      <li *ngFor="let change of changeLog">{{change}}</li>
    </ul>
  `
})
export class VersionChildComponent implements OnChanges {
  @Input() major: number;
  @Input() minor: number;
  changeLog: string[] = [];

  ngOnChanges(changes: SimpleChanges) { //ngOnchange适合监听子组件多个输入属性的变化
    const log: string[] = [];
    for (const propName in changes) {
      const changedProp = changes[propName];
      const to = JSON.stringify(changedProp.currentValue);
      if (changedProp.isFirstChange()) {
        log.push(`Initial value of ${propName} set to ${to}`);
      } else {
        const from = JSON.stringify(changedProp.previousValue);
        log.push(`${propName} changed from ${from} to ${to}`);
      }
    }
    this.changeLog.push(log.join(', '));
  }
}
父组件怎么读取子组件的属性和调用子组件的方法?(2种方法)

1、通过本地变量代表子组件

父组件不能使用数据绑定来读取子组件的属性或调用子组件的方法。但可以在父组件模板里,新建一个本地变量来代表子组件,然后利用这个变量来读取子组件的属性和调用子组件的方法,如下例所示。

思考:父组件可以通过这种方式读取子组件的私有属性和私有方法吗?

父组件
import { Component } from '@angular/core';
import { CountdownTimerComponent } from './countdown-timer.component';

@Component({
  selector: 'app-countdown-parent-lv',
  template: `
  <h3>Countdown to Liftoff (via local variable)</h3>
  <button (click)="timer.start()">Start</button> //调用子组件方法
  <button (click)="timer.stop()">Stop</button>
  <div class="seconds">{{timer.seconds}}</div> //读取子组件属性
  <app-countdown-timer #timer></app-countdown-timer>
  `,
  styleUrls: ['../assets/demo.css']
})
export class CountdownLocalVarParentComponent { }
子组件
import { Component, OnDestroy } from '@angular/core';

@Component({
  selector: 'app-countdown-timer',
  template: '<p>{{message}}</p>'
})
export class CountdownTimerComponent implements OnDestroy {

  intervalId = 0;
  message = '';
  seconds = 11;

  ngOnDestroy() { this.clearTimer(); }

  start() { this.countDown(); }
  stop()  {
    this.clearTimer();
    this.message = `Holding at T-${this.seconds} seconds`;
  }

  private clearTimer() { clearInterval(this.intervalId); }

  private countDown() {
    this.clearTimer();
    this.intervalId = window.setInterval(() => {
      this.seconds -= 1;
      if (this.seconds === 0) {
        this.message = 'Blast off!';
      } else {
        if (this.seconds < 0) { this.seconds = 10; } // reset
        this.message = `T-${this.seconds} seconds and counting`;
      }
    }, 1000);
  }
}

2、父组件调用@viewChild() (基础,推荐使用)

这个本地变量方法是个简单便利的方法。但是它也有局限性(只能在模板html中使用),因为父组件-子组件的连接必须全部在父组件的模板中进行。父组件本身的ts代码对子组件没有访问权。

当父组件类需要访问子组件时,可以把子组件作为 ViewChild,注入到父组件里面。

父组件类中访问子组件的属性和方法:

import { AfterViewInit, ViewChild } from '@angular/core';
import { Component } from '@angular/core';
import { CountdownTimerComponent } from './countdown-timer.component'; //引入子组件

@Component({
  selector: 'app-countdown-parent-vc',
  template: `
  <h3>Countdown to Liftoff (via ViewChild)</h3>
  <button (click)="start()">Start</button>
  <button (click)="stop()">Stop</button>
  <div class="seconds">{{ seconds() }}</div>
  <app-countdown-timer></app-countdown-timer>
  `,
  styleUrls: ['../assets/demo.css']
})
export class CountdownViewChildParentComponent implements AfterViewInit {

  //通过 @ViewChild 属性装饰器,将子组件 CountdownTimerComponent 注入到私有属性 timerComponent 里面。
  @ViewChild(CountdownTimerComponent) 
  private timerComponent: CountdownTimerComponent;

  seconds() { return 0; }

  // angular创建了组件的子视图后会调用它,注意获取子组件的属性,要在子组件视图加载之后
  ngAfterViewInit() {
    // 访问子组件属性
    setTimeout(() => this.seconds = () => this.timerComponent.seconds, 0);
  }

  start() { this.timerComponent.start(); } // 访问子组件的方法
  stop() { this.timerComponent.stop(); }
}

注意:(使用场景很多,必须掌握)

ngAfterViewInit() 生命周期钩子是非常重要的一步。被注入的计时器组件只有在 Angular 显示了父组件视图之后才能访问,所以它先把秒数显示为 0.

然后 Angular 会调用 ngAfterViewInit 生命周期钩子,但这时候再更新父组件视图的倒计时就已经太晚了。Angular 的单向数据流规则会阻止在同一个周期内更新父组件视图。应用在显示秒数之前会被迫再等一轮。

使用 setTimeout() 来等下一轮,然后改写 seconds() 方法,这样它接下来就会从注入的这个计时器组件里获取秒数的值。

二、组件通过服务来通信(发布订阅者模式,基础,必须掌握)

父组件和它的子组件共享同一个服务,利用该服务在组件家族内部实现双向通信。

不仅局限于父子组件,只要组件与组件共享同一个服务,就可以实现数据通信。

<!--parent.component.html-->
<p style="width: 1000px;margin: auto">
<p class="card" style="width: 500px;float: left">
 <p class="card-header"> 父组件</p>

 <p class="card-body">
  <h5 class="card-title">父组件</h5>
  <p class="form-group">
   <label for="serviceoutput">父组件服务输入:</label>
   <input type="text"
       class="form-control"
       id="serviceoutput"
       placeholder="服务输入"
       [(ngModel)]="serviceInput"
   >
  </p>

  <button class="btn btn-primary" (click)="clickService()">Service方式</button>
 </p>

</p>
<app-child></app-child>
</p>
<!--child.component.html-->
<p class="card" style="width: 500px;">
 <p class="card-header">子组件</p>

 <p class="card-body">
  <h5 class="card-title">子组件</h5>
  <p class="form-group">
   <label for="serviceoutput">子组件服务输入:</label>
   <input type="text"
       class="form-control"
       id="serviceoutput"
       placeholder="服务输入"
       [(ngModel)]="serviceInput"
   >
  </p>

  <button class="btn btn-primary" (click)="clickService()">Service方式</button>
 </p>
</p>
//服务重点
//meditor.service.ts

import {Injectable} from '@angular/core';
import {Subject} from 'rxjs/Subject';
import {Observable} from 'rxjs/Observable';

@Injectable()
export class MeditorService {
 private subject = new Subject<MeditorMsg>();
 constructor() {}
 // 获取订阅者
 public getObservable(): Observable<MeditorMsg> {
  return this.subject.asObservable();
 }

 // 推送信息
 public push(msg: MeditorMsg) {
  this.subject.next(msg);
 }
}

// 中间者信息
export interface MeditorMsg {
 id: string;
 body: any;
}
subscription: Subscription = null; //初始化一个订阅对象

//子组件构造函数,用于监听数据推送
constructor(private meditor: MeditorService) {
  this.subscription = meditor.getObservable().subscribe(
   msg => {
    console.log(msg);
    if (msg.id === 'parent') {   //id为parent,获取父组件数据
     this.serviceInput = msg.body;
    }
   }
  );
 }
// 子组件将数据推送到中间着,给订阅者
clickService() {
  this.meditor.push({id: 'parent', body: this.serviceInput});
 }

//父组件构造函数,用于监听数据推送
constructor(private meditor: MeditorService) {
  this.subscription = meditor.getObservable().subscribe(
   msg => {
    console.log(msg);
    if (msg.id === 'child') {    //id为child,获取子组件数据
     this.serviceInput = msg.body;
    }
   }
  );
 }

// 父组件将数据推送到中间着,给订阅者
clickService() {
  this.meditor.push({id: 'parent', body: this.serviceInput});
 }
 
// 注意:订阅一个对象,就是在生命周期结束前,要取消订阅。
ngOnDestroy() {
    this.subscription.unsubscribe();
}

思考: 这种发布订阅者模式适合全局状态管理吗?

三、可以通过本地缓存来实现通信(Cookie,LocalStorage、SessionStorage)

<!-- catch_namae_type.ts -->
// 目的是好维护
// 项目当中用到的页面缓存,需要在这里进行声明;key-value保持一致
// 声明规则,不同类型的缓存使用前缀 session_/ local_ / cookie_
// 动态设置缓存不用在这里声明,但是需要在key后面加'_noSetType_'标识
export const CatchNameType = {
  session_userInfo: 'session_userInfo', // 用户信息
  session_toekn: 'session_token', // token
  local_loginInfo: 'local_loginInfo', // 本地缓存用户名密码
};

<!--catch.ts-->
import { Injectable } from '@angular/core';
// 定义这个类,主要是看全局定义了哪些本地缓存
import { CatchNameType } from './catch_namae_type';

// -------------------------------------------------------缓存工具类(三类方法)
// Cookie           (方法有:set/get/remove)
// SStorage(sessionStorage)  (方法有:set/get/remove/clear)
// LStorage(localStorage)    (方法有:set/get/remove/clear)
@Injectable({
  providedIn: 'root',
})
export class Catch {
  // cookie
  public static Cookie = {
    /**
     * cookie 存贮
     * @param key  属性
     * @param value  值
     * @param String expire  过期时间,单位天
     */
    set(key: string, value: any, expire: any): void {
      if (Catch.is_set_catch_name_type(key)) {
        const d = new Date();
        d.setDate(d.getDate() + expire);
        document.cookie = `${key}=${value};expires=${d.toDateString()}`;
      }
    },
    get(key: string): string {
      const cookieStr = unescape(document.cookie);
      const arr = cookieStr.split('; ');
      let cookieValue = '';
      // tslint:disable-next-line: prefer-for-of
      for (let i = 0; i < arr.length; i++) {
        const temp = arr[i].split('=');
        if (temp[0] === key) {
          cookieValue = temp[1];
          break;
        }
      }
      return cookieValue;
    },
    remove(key: string): void {
      document.cookie = `${encodeURIComponent(key)}=;expires=${new Date()}`;
    },
  };

  // sessionStorage
  public static SStorage = {
    set(key: string, value: any): void {
      if (Catch.is_set_catch_name_type(key)) {
        sessionStorage.setItem(key, JSON.stringify(value));
      }
    },
    get(key: string): any {
      const jsonString =
        sessionStorage.getItem(key) === 'undefined'
          ? undefined
          : sessionStorage.getItem(key);
      return jsonString ? JSON.parse(jsonString) : null;
    },

    remove(key: string): void {
      sessionStorage.removeItem(key);
    },
    clear(): void {
      sessionStorage.clear();
    },
  };

  // localStorage
  public static LStorage = {
    set(key: string, value: any): void {
      if (Catch.is_set_catch_name_type(key)) {
        localStorage.setItem(key, JSON.stringify(value));
      }
    },
    get(key: string): any {
      const jsonString =
        localStorage.getItem(key) === 'undefined'
          ? undefined
          : localStorage.getItem(key);
      return jsonString ? JSON.parse(jsonString) : null;
    },
    remove(key: string): void {
      localStorage.removeItem(key);
    },
    clear(): void {
      localStorage.clear();
    },
  };

  // 设置缓存的时候是否在catch_name_type里面声明
  static is_set_catch_name_type(key: string): boolean {
    let allow = false;
    // 对动态设置缓存不进行检查
    if (key.indexOf('_noSetType_') !== -1) {
      allow = true;
      console.log('动态设置缓存', key);
      return allow;
    }
    // 对命名规则进行检查
    const nameRule =
      key.indexOf('session_') !== -1 ||
      key.indexOf('local_') !== -1 ||
      key.indexOf('cookie_') !== -1;
    if (!nameRule) {
      allow = false;
      console.log('命名规则错误', key);
      return allow;
    }
    // 静态设置的缓存需要配置类型
    Object.values(CatchNameType).forEach((item) => {
      if (item === key) {
        allow = true;
      }
    });
    if (!allow) {
      console.log('缓存操作失败,请检查配置缓存类型');
    }
    return allow;
  }
}

四、页面路由传参也可以实现单向通信

这部分内容,我会在路由章节整理。

组件通信总结

所以组件通信大概有如下几种:

1、父子组件通信(1、@Input() @output 2、本地变量#val 3、@viewChild())

2、通过服务

3、页面缓存

4、页面级组件传参(两个页面等同于两个组件)

动态组件

组件的模板不会永远是固定的。应用可能会需要在运行期间按需加载一些新的组件。 通过下面的例子可以了解动态组件的基本使用

1、创建组件,被引入的组件

@Component({
  template: `<span>hello world</span>`
})
export class DynamicComponent { }

2、创建容器组件,用于加载动态组件

@Component({
  selector: 'app-container',
  template: `
    <div #dynamicContainer></div>
    <button (click)="createComponent()">Create</button>
  `
})
export class AppContainerComponent {
  // 声明容器
  @ViewChild("dynamicContainer", { read: ViewContainerRef }) container: ViewContainerRef;
}

在AppContainerComponent组件中,通过@ViewChild装饰器来获取视图中的模板元素,如果没有指定第二个查询参数,则默认返回 组件实例或相应的DOM元素,但这个示例中,我们需要获取ViewContainerRef实例也就是视图容器。可以在视图容器中创建、插入、删除组件等。

3、动态创建组件

在创建组件之前,需要注入ComponentFactoryResolver服务对象,该服务是动态加载组件的核心,可以将一个组件实例呈现到另一个 组件视图上。

  • 使用ComponentFactoryResolve将已声明但未实例化的组件解析成为可以动态加载的component
//依赖组件类型获取对应的factory,从名字上可以看出该factory是用来初始化组件的
const componentFactory = this.ComponentFactoryResolver(DynamicComponent);
//调用视图容器的createComponent方法并将组件添加到容器上。该方法内部会调用factory的create方法来初始化组件。
const modalContainerRef = this.container.createComponent(componentFactory);

4、为组件属性赋值

通过如下的方式为组件属性进行赋值

modalContainerRef.instance.property = ***;

5、销毁组件

在使用组件后,记得要销毁组件。

modalContainerRef.destroy();

6、组件注册

为了能够动态创建组件需要将组件在模块的entryComponents中声明。因为在entryComponents中声明的组件Angular都会创建一个 ComponentFactory并将其存储在ComponentFactoryResolver中,这是动态加载必需的步骤。

一篇更好的博客,介绍动态组件加载数据交互:blog.csdn.net/qq_30101131…