Angular - 组件交互



组件之间的交互

在基于组件的架构中,组件之间的交互是一个重要且必要的特性。Angular 应用基本上是由组件组成的。Angular 提供了多种选项来在组件之间传递和接收数据。它还允许组件与其层次结构中的其他组件进行高级交互。让我们在本节中了解如何在代码中实现组件之间的交互。

组件交互的技术

在组件之间进行通信的技术列表如下:

  • 父组件向子组件传递数据。

  • 父组件从子组件接收数据。

  • 父组件操作子组件。

  • 父组件和子组件使用公共服务进行交互。

让我们在接下来的章节中学习这些内容。

父组件向子组件传递数据

父组件可以通过子组件的属性向子组件传递数据。子组件可以通过 @Input() 装饰器接收数据。子组件可以在其整个生命周期中(从初始化到销毁)接收数据。

Angular 的设计使得子组件可以自动拦截来自父组件的数据变化,并在子组件中进行必要的更新。Angular 还提供钩子来拦截数据变化并编写自定义处理逻辑。总的来说,子组件有三种选项可以拦截来自父组件的数据。

  • 自动拦截
  • Setter 拦截
  • ngOnChanges 钩子

让我们在接下来的章节中详细了解所有技术。

自动拦截

自动拦截简化了从父组件接收数据。Angular 提供了一个 @Input 装饰器来接收来自父组件的数据。它会在父组件更新数据时接收数据。配置 Input 装饰器非常简单。只需将 input 装饰器附加到子组件中的一个属性上,然后通过模板中子组件的属性传递来自父组件的数据。

假设我们想要从父组件传递一个计数器到子组件。

步骤 1:在子组件中创建一个属性 counter,并用 @Input 装饰它

@Input() counter: number = 0;

这里:

  • @Input() 是装饰器
  • counter 是输入属性
  • number 是输入属性的数据类型,它是可选的
  • 0 是 counter 的初始值。如果未提供输入,则将使用此值。

步骤 2:在父组件中初始化一个变量,例如 counterValue。

counterValue: number = 10

步骤 3:使用子组件属性 (counter) 从父组件传递 counter 输入。

<app-child-component [counter]="counterValue" />

步骤 4:最后,根据需要在子组件模板中使用 counter 值。

counter: {{counter}}

让我们创建两个组件,父组件和子组件,然后尝试将数据从父组件传递到子组件并在子组件中呈现它。

步骤 1:使用 angular CLI 创建一个父组件 InOutSample,如下所示:

$ ng generate component InOutSample
CREATE src/app/in-out-sample/in-out-sample.component.css (0 bytes)
CREATE src/app/in-out-sample/in-out-sample.component.html (28 bytes)
CREATE src/app/in-out-sample/in-out-sample.component.spec.ts (596 bytes)
CREATE src/app/in-out-sample/in-out-sample.component.ts (228 bytes)
UPDATE src/app/app.module.ts (1289 bytes)

步骤 2:在组件中添加一个 counter 变量,如下所示:

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

@Component({
   selector: 'app-in-out-sample',
   templateUrl: './in-out-sample.component.html',
   styleUrls: ['./in-out-sample.component.css']
})
export class InOutSampleComponent {
   counter: number = 10;
}

步骤 3:使用 angular CLI 创建一个新的子组件 InOutChildSample,如下所示:

$ ng generate component InOutChildSample
CREATE src/app/in-out-child-sample/in-out-child-sample.component.css (0 bytes)
CREATE src/app/in-out-child-sample/in-out-child-sample.component.html (34 bytes)
CREATE src/app/in-out-child-sample/in-out-child-sample.component.spec.ts (632 bytes)
CREATE src/app/in-out-child-sample/in-out-child-sample.component.ts (251 bytes)
UPDATE src/app/app.module.ts (1417 bytes)

步骤 4:在子组件中添加一个 counter 属性,并用 @Input() 装饰器装饰它,如下所示:

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

@Component({
   selector: 'app-in-out-child-sample',
   templateUrl: './in-out-child-sample.component.html',
   styleUrls: ['./in-out-child-sample.component.css']
})
export class InOutChildSampleComponent {
   @Input() counter : number = 0;
}

步骤 5:打开子组件模板 in-out-child-sample.component.html,并使用 counter 属性,如下所示:

<div>
   <p>Counter: {{counter}}</p>
</div>

步骤 6:打开父组件模板 in-out-sample.component.html,并呈现子组件以及 counter 属性,如下所示:

<app-in-out-child-sample [counter]="counter" />

步骤 7:打开 app 组件的模板,并呈现父组件,如下所示:

<app-in-out-sample />

<router-outlet></router-outlet>

步骤 8:最后,运行应用程序并检查计数器是否显示从父组件传递的值,如下所示:

counter

让我们尝试使用按钮和点击事件更改父组件中的 counter 变量,然后查看它是否会影响子组件。

步骤 1:在父组件中添加一个函数来增加 counter 值,如下所示:

inc() {
   this.counter++
}

步骤 2:在父组件的模板中添加一个按钮,并绑定函数,如下所示:

<button (click)="inc()">Increment counter</button>

<app-in-out-child-sample [counter]="counter" />

步骤 3:最后,运行应用程序并检查父组件中变量的变化是否反映在子组件中。

Increment Counter

Setter 拦截

基于 Setter 的拦截只是先前技术的扩展。它基本上是为 @Input 装饰器中使用的属性使用 getter 和 setter。例如,counter 示例中的 counter 属性可以扩展为支持 getter 和 setter,如下所示:

@Input()
get counter(): number { return this._counter; }
set counter(val: number) {
   this._counter = val || -1;
}
private _counter: number = 0;

这里,如果未设置 counter,则将其设置为 -1。

让我们更改 counter 示例 in-out-child-sample.component.ts 以使用 setter 拦截 counter,并在 counter 值超过 25 时将其重置为零。

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

@Component({
   selector: 'app-in-out-child-sample',
   templateUrl: './in-out-child-sample.component.html',
   styleUrls: ['./in-out-child-sample.component.css']
})
export class InOutChildSampleComponent {
   @Input()
   get counter(): number { return this._counter; }
   set counter(val: number) {
      this._counter = val || 0;
      if(val > 25) this._counter = val % 25;
   }
   private _counter: number = 0;
}

运行应用程序,您会看到一旦 counter 达到 25,它将重置为 0。

Counter Reset

ngOnChanges 钩子拦截

正如我们在组件的生命周期及其钩子方法中学到的那样,ngOnChanges 是一个钩子方法,每当 Angular 检测到其输入发生变化时,它都会运行。

ngOnChanges 钩子接受一个 SimpleChanges 类型的对象。SimpleChanges 是一个字典,包含所有发生变化的属性。我们可以遍历所有属性并找到属性的最新值。遍历所有已更改属性的伪代码如下:

ngOnChanges(changes: SimpleChanges) {
   for (const key in changes) {
      const prop = changes[key];
      
      const prevVal = prop.previousValue
      const currentVal = prop.currentValue
      cont isFirstChange = pop.isFirstChange()
      
      if (prop.isFirstChange()) {
         console.log("The current value is ${prop.currentValue}")
      } else {
         console.log(`${key} changed from ${prop.previousValue} to
         ${prop.currentValue}`);
      }
   
   }
}

父组件从子组件接收数据

子组件可以通过 @Output 装饰器将数据发送到父组件。Output 装饰器与 Input 装饰器非常相似,只是输出实际上是一个事件发射器,它将数据(输出)与事件一起传递。父组件可以在子组件中的事件上订阅,并在子组件中的数据发生变化时从子组件获取发射的值。接收来自子组件的数据的步骤如下:

步骤 1:使用 Output 装饰器在子组件中创建一个事件发射器。

@Output() counterEvent = new EventEmitter<number>();

步骤 2:在子组件中的数据发生变化时发射 counter 事件

this.counterEvent.emit(changedValue)

步骤 3:在父组件中捕获事件,并从回调函数中获取数据。

<parent-component (counterEvent)="get($event)" />

步骤 4:在父组件中对捕获的值执行任何操作。

让我们在子组件 InOutChildSample 组件中编写一个 output 装饰器,并尝试从父组件 InOutSample 组件获取输出。

步骤 1:在子组件 in-out-child-sample.component.ts 中创建一个输出事件发射器,如下所示:

@Output() counterEvent = new EventEmitter<number>();

步骤 2:创建一个方法,通过在子组件 In-out-child-sample.component.ts 中发射事件并带有 counter 数据来传递 counter 的值。

passCounterToParent() {
   this.counterEvent.emit(this.counter)
}

步骤 3:子组件的完整列表如下:

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

@Component({
   selector: 'app-in-out-child-sample',
   templateUrl: './in-out-child-sample.component.html',
   styleUrls: ['./in-out-child-sample.component.css']
})
export class InOutChildSampleComponent {
   @Input() counter : number = 0;
   @Output() counterEvent = new EventEmitter<number>();
   
   passCounterToParent() {
      this.counterEvent.emit(this.counter)
   }

}

步骤 4:打开子组件模板 in-out-child-sample.component.html,并添加一个按钮,以便在用户单击按钮时调用 counter 事件

<div>
  <p>Counter: {{counter}}</p>
  <button (click)="passCounterToParent()">Pass Counter to Parent</button>
</div>

这里:

  • click 是按钮点击事件,当它被点击时,它被配置为运行 passCounterToParent() 函数。

步骤5:在父组件中添加一个变量来保存从子组件通过事件传递的输出数据。

childCounter: number = 0;

步骤6:在父组件中添加一个函数来获取从子组件通过事件传递的输出数据。

get(val: number) {
   this.childCounter = val;
}

步骤7:父组件的完整代码如下所示:

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

@Component({
   selector: 'app-in-out-sample',
   templateUrl: './in-out-sample.component.html',
   styleUrls: ['./in-out-sample.component.css']
})
export class InOutSampleComponent {
   counter: number = 10;
   childCounter: number = 0;   
   inc() {
      this.counter++
   }   
   get(val: number) {
      this.childCounter = val;
   }   
}

步骤8:打开父组件模板文件,in-out-sample.component.html,并订阅子组件的事件 counterEvent,并将 get 方法设置为回调函数,如下所示:

<button (click)="inc()">Increment counter</button>
<p>Data from child: {{childCounter}}</p>
<app-in-out-child-sample [counter]="counter" (counterEvent)="get($event)" />

这里:

  • counterEvent 是来自子组件的事件。

  • get($event) 是回调函数。$event 将保存当前计数器的值。

  • childContent 是来自子组件的数据。

步骤9:最后,运行应用程序,您将看到当单击子组件中的按钮时,子组件会将更新后的计数器值发送到父组件。

child component

父组件操作子组件

父组件可以通过局部变量和 @ViewChild 装饰器完全访问子组件。这两种技术的关键区别在于,在局部变量的概念中,父组件仅在其模板中访问子组件。但在 @ViewChild 概念中,父组件也将在其类环境中访问子组件。这使得父组件也可以在其方法中利用子组件的功能,产生了巨大的差异。

使用局部变量

让我们创建两个组件,ParentCounterComponent 和 ChildCounterComponent,来理解这个概念。ParentCounterComponent 的目的是通过两个按钮(增加和减少按钮)提供计数器功能。增加按钮将增加计数器,减少按钮将减少计数器。父组件将从子组件获取增加和减少功能,而不是自己实现。

步骤1:使用 Angular CLI 创建父组件 ParentCounterComponent,如下所示:

$ ng generate component ParentCounter
CREATE src/app/parent-counter/parent-counter.component.css (0 bytes)
CREATE src/app/parent-counter/parent-counter.component.html (29 bytes)
CREATE src/app/parent-counter/parent-counter.component.spec.ts (609 bytes)
CREATE src/app/parent-counter/parent-counter.component.ts (233 bytes)
UPDATE src/app/app.module.ts (1701 bytes)

步骤2:使用 Angular CLI 创建子组件 ChildCounterComponent,如下所示:

ng generate component ChildCounter
CREATE src/app/child-counter/child-counter.component.css (0 bytes)
CREATE src/app/child-counter/child-counter.component.html (28 bytes)
CREATE src/app/child-counter/child-counter.component.spec.ts (602 bytes)
CREATE src/app/child-counter/child-counter.component.ts (229 bytes)
UPDATE src/app/app.module.ts (1809 bytes)

步骤3:在子组件中声明一个变量 counter。

counter: number = 0

步骤4:在子组件中实现增加功能。

inc() { this.counter++ }

这里,inc() 方法只是使用自增运算符增加计数器。

步骤5:在子组件中实现减少功能。

dec() { this.counter-- }

这里,dec() 方法只是使用自减运算符减少计数器。

步骤6:子组件的完整代码如下所示:

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

@Component({
   selector: 'app-child-counter',
   templateUrl: './child-counter.component.html',
   styleUrls: ['./child-counter.component.css']
})
export class ChildCounterComponent {
   counter: number = 0
   
   inc() { this.counter++ }
   dec() { this.counter-- }
}

步骤7:接下来,打开父组件的模板文件 parent-counter.component.html,并添加子组件以及一个 id(#child)来访问子组件。

<app-child-counter #child></app-child-counter>

步骤8:接下来,添加两个按钮,并将点击事件绑定到通过子标识符访问的子组件的 inc() 和 dec() 方法。此外,使用子标识符显示计数器的当前值。

<p>counter: {{child.counter}}</p>

<button (click)="child.inc()">Increment</button>
<button (click)="child.dec()">Decrement</button>

<app-child-counter #child></app-child-counter>

步骤9:接下来,将父组件包含在 app 组件的模板中。

<app-parent-counter />
<router-outlet></router-outlet>

步骤10:最后,运行应用程序并检查计数器是否正常工作。

counter working

使用 @ViewChild 装饰器

让我们通过创建一个新的组件 VcParentCounterComponent 来扩展上面的应用程序,该组件将使用 @ViewChild 概念而不是局部变量。此方法对子组件没有任何更改。

步骤1:使用 Angular CLI 创建一个新组件 VcParentCounterComponent,如下所示:

$ ng generate component VcParentCounter
CREATE src/app/vc-parent-counter/vc-parent-counter.component.css (0 bytes)
CREATE src/app/vc-parent-counter/vc-parent-counter.component.html (32 bytes)
CREATE src/app/vc-parent-counter/vc-parent-counter.component.spec.ts (624 bytes)
CREATE src/app/vc-parent-counter/vc-parent-counter.component.ts (244 bytes)
UPDATE src/app/app.module.ts (1931 bytes)

步骤2:从 @angular/core 模块导入必要的类。

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

步骤3:导入子组件。

import { ChildCounterComponent } from '../child-counter/child-counter.component'

步骤4:实现 AfterViewInit 生命周期钩子,如下所示。

export class VcParentCounterComponent implements AfterViewInit {
   ngAfterViewInit() {
      // ...
   }
}

步骤5:使用 @ViewChild 访问子组件,如下所示:

@ViewChild(ChildCounterComponent)
private child! : ChildCounterComponent;

这里,@ViewChild 装饰器接受子组件的类型,该类型位于组件的模板中。

步骤6:通过访问子组件来实现增加和减少功能。

inc() { this.child.inc() }
dec() { this.child.dec() }

这里,我们使用 this.child 变量来访问子组件的功能。

步骤7:实现一个计数器功能来检索当前计数器的值,如下所示:

counter() { return 0; }

ngAfterViewInit() {
   setTimeout(() => this.counter = () => this.child.counter, 0)
}

这里,我们在 ngAfterViewInit 生命周期钩子中创建了一个计数器方法。只有在这个生命周期之后,子组件才可用。因此,我们在组件初始化期间创建了一个虚拟计数器方法(需要访问子组件的计数器值),并在钩子方法中更新了计数器方法。

步骤8:组件的完整代码如下所示:

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

import { ChildCounterComponent } from '../child-counter/child-counter.component'

@Component({
   selector: 'app-vc-parent-counter',
   templateUrl: './vc-parent-counter.component.html',
   styleUrls: ['./vc-parent-counter.component.css']
})
export class VcParentCounterComponent implements AfterViewInit {
   @ViewChild(ChildCounterComponent)
   private child! : ChildCounterComponent;
   
   inc() { this.child.inc() }
   dec() { this.child.dec() }
   
   counter() { return 0; }
   
   ngAfterViewInit() {
   setTimeout(() => this.counter = () => this.child.counter, 0)
   }
}

步骤9:接下来,打开组件的模板文件 vc-parent-counter-component.html,并添加子组件以及按钮和方法绑定,如下所示:

<p>counter: {{ counter() }}</p>

<button (click)="inc()">Increment</button>
<button (click)="dec()">Decrement</button>

<app-child-counter></app-child-counter>

这里,我们没有包含标识符,而是使用了父组件的功能而不是子组件的功能(我们在前面的示例中使用局部变量的概念)。父组件将从子变量获取所需的功能,该变量是通过 @ViewChild 装饰器获取的。

步骤10:接下来,打开 app 组件的模板并呈现父组件,如下所示:

<app-vc-parent-counter />

<router-outlet></router-outlet>

步骤11:最后,运行应用程序并检查计数器是否显示从父组件传递的值,如下所示:

parent component

父组件和子组件使用公共服务进行交互

服务是 Angular 框架不可分割的一部分。我们可以创建服务来实现特定的功能,然后在任何组件中使用它。服务的最佳用例如下:

  • API 调用
  • 实用程序函数
  • 在组件之间共享数据

在本节中,让我们学习如何使用服务在组件之间共享数据。

通过服务共享数据的常规分步过程如下:

步骤1:在服务中设置任意数量的可观察变量。

import { Subject } from 'rxjs';

export class MyCounterService {
   private source = new Subject<number>();
   public data$ = this.source.asObservable()
}

这里:

  • source 变量使用 Subject 类型创建,Subject 是来自 rxjs 模块的可观察对象。

  • 调用 asObservable 方法来隐藏源序列的身份。

步骤2:在组件中注入所需的服务。

constructor(private myService: MyService) {
}

步骤3:在组件中,订阅服务中可用的共享数据。

constructor(private myService: MyService) {
   this.myService.data$.subscribe( data => {
      this.data = data;
   })
}

步骤4:使用可观察对象在服务中实现功能,以便订阅的组件可以接收更新的数据。

export class MyCounterService {
   update(val: number) { this.data.next(val) }
}

步骤5:像往常一样在组件及其模板中使用服务数据和方法。当服务数据更新时,Angular 将更新组件。

让我们通过使用服务实现我们的计数器组件来理解这个概念。

步骤1:使用 Angular CLI 创建一个服务 MyCounterService,如下所示:

$ ng generate service MyCounter
CREATE src/app/my-counter.service.spec.ts (373 bytes)
CREATE src/app/my-counter.service.ts (138 bytes)

步骤2:使用 Angular CLI 创建一个组件 MyCounterServiceComponent,如下所示:

ng generate component MyCounterService
CREATE src/app/my-counter-service/my-counter-service.component.css (0 bytes)
CREATE src/app/my-counter-service/my-counter-service.component.html (33 bytes)
CREATE src/app/my-counter-service/my-counter-service.component.spec.ts (631 bytes)
CREATE src/app/my-counter-service/my-counter-service.component.ts (248 bytes)
UPDATE src/app/app.module.ts (2057 bytes)

步骤3:在服务中创建一个可观察对象来跟踪计数器的值,如下所示:

private counterSource = new Subject<number>();
public counter$ = this.counterSource.asObservable()

这里:

  • counterSource 是 Subject 类型的变量。Subject 是由 rxjs 库提供的一个可观察对象。Subject 可以发射和接收值。

  • 在 counterSource 上调用 asObservable 方法来隐藏源序列的身份。

步骤4:实现增加和减少方法,如下所示:

inc(val: number) { this.counterSource.next(val + 1) }
dec(val: number) { this.counterSource.next(val - 1) }

这里:

  • counterSource 的 next() 方法用于更新计数器的值。

步骤5:服务 MyCounterService 的完整代码如下所示:

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

@Injectable({
   providedIn: 'root'
})
export class MyCounterService {
   constructor() { }
   
   private counterSource = new Subject<number>();
   public counter$ = this.counterSource.asObservable()
   
   inc(val: number) { this.counterSource.next(val + 1) }
   dec(val: number) { this.counterSource.next(val - 1) }
}

步骤6:通过构造函数在组件中注入服务。

export class MyCounterServiceComponent {
   constructor(private counterService: MyCounterService) {
   }
}

步骤7:通过组件构造函数订阅服务中可用的可观察对象,如下所示:

this.counterService.counter$.subscribe( counter => {
   this.counter = counter;
})

这里,订阅将在可观察对象发生更改时更新计数器的值。

步骤8:通过调用计数器服务方法来实现增加 (inc()) 和减少 (dec()) 方法,如下所示:

inc() { this.counterService.inc(this.counter) }
dec() { this.counterService.dec(this.counter) }

步骤9:组件 MyCounterServiceComponent 的完整代码如下所示:

import { Component } from '@angular/core';
import { MyCounterService } from '../my-counter.service'

@Component({
   selector: 'app-my-counter-service',
   templateUrl: './my-counter-service.component.html',
   styleUrls: ['./my-counter-service.component.css'],
})
export class MyCounterServiceComponent {
   counter: number = 0;
   
   constructor(private counterService: MyCounterService) {
      this.counterService.counter$.subscribe( counter => {
         this.counter = counter;
      })
   }   
   inc() { this.counterService.inc(this.counter) }
   dec() { this.counterService.dec(this.counter) }
}

步骤10:接下来,打开组件的模板 my-counter-service.component.html 并编写模板标记以显示当前计数器的值,然后添加另外两个按钮来增加和减少计数器的值。将 inc() 和 dec() 方法分别绑定到增加和减少按钮的点击事件。

<p>counter: {{counter}}</p>

<button (click)="inc()">Increment</button>
<button (click)="dec()">Decrement</button>

步骤11:接下来,打开 app 组件的模板并将我们的组件包含在其中,如下所示:

<app-my-counter-service />
<router-outlet></router-outlet>

步骤12:运行应用程序并检查输出。

App Component

步骤13:接下来,在 app 组件的模板中添加另一个组件,如下所示:

<app-my-counter-service />
<app-my-counter-service />
<router-outlet></router-outlet>

步骤14:运行应用程序,您将看到增加一个组件也会反映在另一个组件中。这是因为它基于同一个服务。

Two Component

结论

Angular 为组件在其自身内部交互提供了丰富的选项。开发人员可以根据给定的场景选择合适的方法并获得所需的输出。

广告

© . All rights reserved.