MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Angular组件间通信方式

2022-02-171.3k 阅读

父子组件通信

在 Angular 应用中,父子组件通信是较为常见的场景。通常是父组件向子组件传递数据,以及子组件向父组件传递事件。

父组件向子组件传递数据

  1. 通过 @Input() 装饰器
    • 原理:在子组件中使用 @Input() 装饰器来定义一个输入属性,父组件通过绑定该属性来传递数据。@Input() 装饰器允许外部(父组件)向组件内部传入数据,就像是给组件开了一扇接收数据的“窗户”。
    • 代码示例
      • 子组件(child.component.ts)
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app - child',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.css']
})
export class ChildComponent {
  @Input() childData: string;
}
 - **子组件模板(child.component.html)**
<p>父组件传递的数据: {{ childData }}</p>
 - **父组件(parent.component.ts)**
import { Component } from '@angular/core';

@Component({
  selector: 'app - parent',
  templateUrl: './parent.component.html',
  styleUrls: ['./parent.component.css']
})
export class ParentComponent {
  parentData = '这是来自父组件的数据';
}
 - **父组件模板(parent.component.html)**
<app - child [childData]="parentData"></app - child>

在上述代码中,父组件 ParentComponent 定义了 parentData 变量,并通过 [childData]="parentData" 将其传递给子组件 ChildComponentchildData 属性。子组件通过 @Input() 装饰器接收该数据并在模板中显示。

  1. 传递复杂对象
    • 原理:同样使用 @Input() 装饰器,只不过传递的数据类型是对象。这种方式在需要传递多个相关数据时非常有用,比如传递一个包含用户信息的对象。
    • 代码示例
      • 子组件(child.component.ts)
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app - child',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.css']
})
export class ChildComponent {
  @Input() user: { name: string; age: number };
}
 - **子组件模板(child.component.html)**
<p>用户名: {{ user.name }}</p>
<p>用户年龄: {{ user.age }}</p>
 - **父组件(parent.component.ts)**
import { Component } from '@angular/core';

@Component({
  selector: 'app - parent',
  templateUrl: './parent.component.html',
  styleUrls: ['./parent.component.css']
})
export class ParentComponent {
  userInfo = { name: '张三', age: 25 };
}
 - **父组件模板(parent.component.html)**
<app - child [user]="userInfo"></app - child>

这里父组件传递了一个包含用户姓名和年龄的对象 userInfo 给子组件,子组件通过 @Input() 接收并在模板中展示对象的属性。

子组件向父组件传递数据

  1. 通过 @Output() 装饰器和 EventEmitter
    • 原理:在子组件中使用 @Output() 装饰器和 EventEmitter 类来定义一个事件输出属性。当子组件内部发生特定事件时,通过 EventEmitteremit() 方法触发该事件,并传递相关数据。父组件通过监听这个事件来接收子组件传递的数据。EventEmitter 就像是一个“发射器”,子组件可以用它发射事件信号给父组件。
    • 代码示例
      • 子组件(child.component.ts)
import { Component, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app - child',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.css']
})
export class ChildComponent {
  @Output() childEvent = new EventEmitter<string>();

  sendDataToParent() {
    const data = '这是子组件传递的数据';
    this.childEvent.emit(data);
  }
}
 - **子组件模板(child.component.html)**
<button (click)="sendDataToParent()">点击传递数据给父组件</button>
 - **父组件(parent.component.ts)**
import { Component } from '@angular/core';

@Component({
  selector: 'app - parent',
  templateUrl: './parent.component.html',
  styleUrls: ['./parent.component.css']
})
export class ParentComponent {
  receivedData: string;

  handleChildEvent(data: string) {
    this.receivedData = data;
  }
}
 - **父组件模板(parent.component.html)**
<app - child (childEvent)="handleChildEvent($event)"></app - child>
<p>从子组件接收到的数据: {{ receivedData }}</p>

在这个例子中,子组件 ChildComponent 定义了 childEvent 事件输出属性,并在按钮点击时通过 emit() 方法传递数据。父组件通过 (childEvent)="handleChildEvent($event)" 监听该事件,并在 handleChildEvent 方法中处理接收到的数据。

非父子组件通信

当组件之间没有直接的父子关系时,通信方式会有所不同。常见的非父子组件通信方式有通过服务和 @ViewChild() 装饰器。

通过服务进行通信

  1. 创建共享服务
    • 原理:创建一个 Angular 服务,该服务作为一个全局单例对象,多个组件可以注入这个服务,并通过服务中的属性和方法来共享数据和传递事件。就好比是在多个组件之间搭建了一座“桥梁”,大家都可以通过这座桥来交换信息。
    • 代码示例
      • 共享服务(shared.service.ts)
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class SharedService {
  private dataSource = new Subject<string>();
  data$ = this.dataSource.asObservable();

  sendData(data: string) {
    this.dataSource.next(data);
  }
}

在上述代码中,SharedService 使用 Subject 创建了一个可观察对象 dataSource,并将其转换为可观察流 data$ 供其他组件订阅。sendData 方法用于向这个流中推送数据。

  1. 在组件中使用共享服务
    • 组件 A(component - a.component.ts)
import { Component } from '@angular/core';
import { SharedService } from './shared.service';

@Component({
  selector: 'app - component - a',
  templateUrl: './component - a.component.html',
  styleUrls: ['./component - a.component.css']
})
export class ComponentAComponent {
  constructor(private sharedService: SharedService) {}

  sendDataToService() {
    const data = '这是来自组件 A 的数据';
    this.sharedService.sendData(data);
  }
}
  • 组件 A 模板(component - a.component.html)
<button (click)="sendDataToService()">点击向服务发送数据</button>
  • 组件 B(component - b.component.ts)
import { Component, OnInit } from '@angular/core';
import { SharedService } from './shared.service';

@Component({
  selector: 'app - component - b',
  templateUrl: './component - b.component.html',
  styleUrls: ['./component - b.component.css']
})
export class ComponentBComponent implements OnInit {
  receivedData: string;

  constructor(private sharedService: SharedService) {}

  ngOnInit() {
    this.sharedService.data$.subscribe(data => {
      this.receivedData = data;
    });
  }
}
  • 组件 B 模板(component - b.component.html)
<p>从服务接收到的数据: {{ receivedData }}</p>

在这个示例中,组件 A 通过调用共享服务的 sendData 方法发送数据,组件 B 在 ngOnInit 生命周期钩子中订阅共享服务的 data$ 可观察流,从而接收到组件 A 发送的数据。

通过 @ViewChild() 装饰器通信(适用于兄弟组件或有共同祖先组件的情况)

  1. 原理@ViewChild() 装饰器允许在父组件中获取子组件的实例,从而可以访问子组件的属性和方法。如果两个非父子组件有共同的祖先组件,祖先组件可以通过 @ViewChild() 获取到这两个子组件的实例,进而实现它们之间的间接通信。这就像是在一个大家庭中,家长可以通过掌握各个孩子(子组件)的情况,来让孩子们间接交流。
  2. 代码示例
    • 子组件 1(child - 1.component.ts)
import { Component } from '@angular/core';

@Component({
  selector: 'app - child - 1',
  templateUrl: './child - 1.component.html',
  styleUrls: ['./child - 1.component.css']
})
export class Child1Component {
  child1Data = '这是子组件 1 的数据';
}
  • 子组件 1 模板(child - 1.component.html)
<p>子组件 1 的数据: {{ child1Data }}</p>
  • 子组件 2(child - 2.component.ts)
import { Component } from '@angular/core';

@Component({
  selector: 'app - child - 2',
  templateUrl: './child - 2.component.html',
  styleUrls: ['./child - 2.component.css']
})
export class Child2Component {
  receivedData: string;

  receiveData(data: string) {
    this.receivedData = data;
  }
}
  • 子组件 2 模板(child - 2.component.html)
<p>从子组件 1 接收到的数据: {{ receivedData }}</p>
  • 父组件(parent.component.ts)
import { Component, ViewChild } from '@angular/core';
import { Child1Component } from './child - 1.component';
import { Child2Component } from './child - 2.component';

@Component({
  selector: 'app - parent',
  templateUrl: './parent.component.html',
  styleUrls: ['./parent.component.css']
})
export class ParentComponent {
  @ViewChild(Child1Component) child1: Child1Component;
  @ViewChild(Child2Component) child2: Child2Component;

  ngAfterViewInit() {
    const dataFromChild1 = this.child1.child1Data;
    this.child2.receiveData(dataFromChild1);
  }
}
  • 父组件模板(parent.component.html)
<app - child - 1></app - child - 1>
<app - child - 2></app - child - 2>

在这个例子中,父组件通过 @ViewChild() 获取到 Child1ComponentChild2Component 的实例,在 ngAfterViewInit 生命周期钩子中,从 Child1Component 获取数据并传递给 Child2Component,实现了两个子组件之间的通信。

跨多级组件通信(祖先与后代组件通信)

在一些复杂的应用结构中,可能需要在祖先组件和深层嵌套的后代组件之间进行通信。

使用 @Input() 和 @Output() 逐级传递

  1. 原理:通过在每一级子组件中使用 @Input()@Output() 装饰器,将数据或事件从祖先组件一级一级地传递到后代组件。这就像是接力赛,数据或事件在组件链中依次传递。
  2. 代码示例
    • 祖先组件(ancestor.component.ts)
import { Component } from '@angular/core';

@Component({
  selector: 'app - ancestor',
  templateUrl: './ancestor.component.html',
  styleUrls: ['./ancestor.component.css']
})
export class AncestorComponent {
  ancestorData = '这是祖先组件的数据';

  handleDescendantEvent(data: string) {
    console.log('从后代组件接收到的数据:', data);
  }
}
  • 祖先组件模板(ancestor.component.html)
<app - parent [parentData]="ancestorData" (parentEvent)="handleDescendantEvent($event)"></app - parent>
  • 父组件(parent.component.ts)
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app - parent',
  templateUrl: './parent.component.html',
  styleUrls: ['./parent.component.css']
})
export class ParentComponent {
  @Input() parentData: string;
  @Output() parentEvent = new EventEmitter<string>();

  sendEventToAncestor() {
    const data = '这是父组件传递给祖先组件的数据';
    this.parentEvent.emit(data);
  }
}
  • 父组件模板(parent.component.html)
<p>从祖先组件接收到的数据: {{ parentData }}</p>
<button (click)="sendEventToAncestor()">点击传递数据给祖先组件</button>
<app - child [childData]="parentData" (childEvent)="sendEventToAncestor()"></app - child>
  • 子组件(child.component.ts)
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app - child',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.css']
})
export class ChildComponent {
  @Input() childData: string;
  @Output() childEvent = new EventEmitter<string>();

  sendEventToParent() {
    const data = '这是子组件传递给父组件的数据';
    this.childEvent.emit(data);
  }
}
  • 子组件模板(child.component.html)
<p>从父组件接收到的数据: {{ childData }}</p>
<button (click)="sendEventToParent()">点击传递数据给父组件</button>

在这个示例中,祖先组件 AncestorComponent 通过 @Input() 将数据传递给父组件 ParentComponent,父组件再传递给子组件 ChildComponent。子组件通过 @Output() 将事件传递给父组件,父组件再传递给祖先组件。

使用 InjectionToken 和 Inject 进行跨级注入

  1. 原理InjectionToken 是一个用于依赖注入的唯一标识符。通过 InjectionToken 定义一个值,并在祖先组件中提供这个值,后代组件可以通过 Inject 装饰器注入这个值,从而实现跨级通信。这就像是在整个应用的“容器”中放置了一个大家都能找到的物品,后代组件可以直接从这个“容器”中获取该物品(数据)。
  2. 代码示例
    • 定义 InjectionToken(data.token.ts)
import { InjectionToken } from '@angular/core';

export const SHARED_DATA = new InjectionToken<string>('shared - data');
  • 祖先组件(ancestor.component.ts)
import { Component, Inject } from '@angular/core';
import { SHARED_DATA } from './data.token';

@Component({
  selector: 'app - ancestor',
  templateUrl: './ancestor.component.html',
  styleUrls: ['./ancestor.component.css'],
  providers: [{ provide: SHARED_DATA, useValue: '这是祖先组件提供的共享数据' }]
})
export class AncestorComponent {
  constructor(@Inject(SHARED_DATA) public sharedData: string) {}
}
  • 后代组件(descendant.component.ts)
import { Component, Inject } from '@angular/core';
import { SHARED_DATA } from './data.token';

@Component({
  selector: 'app - descendant',
  templateUrl: './descendant.component.html',
  styleUrls: ['./descendant.component.css']
})
export class DescendantComponent {
  constructor(@Inject(SHARED_DATA) public sharedData: string) {}
}
  • 后代组件模板(descendant.component.html)
<p>从祖先组件获取的共享数据: {{ sharedData }}</p>

在这个例子中,祖先组件通过 providers 提供了 SHARED_DATA 的值,后代组件通过 @Inject(SHARED_DATA) 注入这个值,从而实现了跨级获取数据。

总结各种通信方式的适用场景

  1. 父子组件通信
    • 父组件向子组件传递数据:当子组件需要依赖父组件提供的数据来展示或进行某些操作时,使用 @Input() 装饰器是最佳选择。比如子组件是一个显示用户信息的卡片,父组件提供用户的具体信息。
    • 子组件向父组件传递数据:当子组件发生某些事件,需要通知父组件并传递相关数据时,@Output() 装饰器和 EventEmitter 配合使用。例如子组件中的一个按钮点击事件,需要将点击后的结果传递给父组件。
  2. 非父子组件通信
    • 通过服务:适用于多个不相关组件之间需要共享数据或传递事件的场景。比如在一个电商应用中,购物车组件和商品列表组件可以通过共享服务来同步购物车的商品数量等信息。
    • 通过 @ViewChild():适用于有共同祖先组件的兄弟组件或其他相关组件之间通信。例如在一个表单页面,有两个子组件分别是输入框和按钮,按钮需要获取输入框的值,而它们有共同的父组件,此时父组件可以通过 @ViewChild() 来实现两者之间的通信。
  3. 跨多级组件通信
    • 使用 @Input() 和 @Output() 逐级传递:当数据或事件需要在组件链中按顺序传递,并且各级子组件都可能对传递的数据或事件有一定处理时,这种方式比较合适。比如一个树形结构的组件,祖先组件的操作需要逐级通知到各个子节点组件。
    • 使用 InjectionToken 和 Inject 进行跨级注入:当后代组件只需要获取祖先组件提供的某些共享数据,而不需要关心中间组件的处理时,这种方式更简洁高效。例如在一个应用的全局配置信息传递中,深层嵌套的组件可能只需要直接获取全局配置,而不需要经过中间组件的层层传递。

通过合理选择这些组件间通信方式,可以构建出结构清晰、易于维护的 Angular 应用。在实际开发中,需要根据应用的具体需求和架构来灵活运用这些技术,以实现高效的组件交互。同时,也要注意避免过度复杂的通信逻辑,以免增加代码的维护难度。在使用共享服务进行通信时,要注意数据的一致性和可维护性,避免多个组件对共享数据的随意修改导致难以调试的问题。在使用 @ViewChild() 时,要注意组件的生命周期,确保在正确的时机获取和使用子组件实例,避免空指针等错误。总之,熟练掌握这些组件间通信方式是 Angular 前端开发的重要技能之一。