10_组件通信
组件通信:数据交互与状态共享
在Angular应用中,组件并非孤立存在,它们需要相互协作、共享数据才能构建完整的功能。组件通信是Angular开发的核心技能,不同组件关系(父子、跨级、兄弟)需要采用不同的通信策略。本章将系统讲解Angular中组件通信的多种方式,包括输入输出绑定、服务共享、依赖注入等,并结合Angular v20的Signals特性,展示现代化的状态管理方案。
1. 父子组件通信:@Input与@Output
父子组件是最常见的组件关系(如页面与子组件、容器与内容组件),这种关系下的通信通过Angular的输入输出机制实现,简单直接且类型安全。
1.1 父传子:@Input装饰器传递数据
@Input()装饰器用于定义组件的输入属性,允许父组件向子组件传递数据。
子组件定义输入属性
// child.component.ts
import { Component, Input } from '@angular/core';
// 定义接收的数据类型
interface Product {
id: number;
name: string;
price: number;
}
@Component({
selector: 'app-product-card',
standalone: true,
template: `
<div class="card">
<h3>{{ product?.name }}</h3>
<p>价格:{{ product?.price | currency }}</p>
@if (isOnSale) {
<span class="sale">促销中</span>
}
</div>
`,
styles: [`
.card { border: 1px solid #ccc; padding: 1rem; margin: 1rem; }
.sale { color: red; font-weight: bold; }
`]
})
export class ProductCardComponent {
// 基础输入属性
@Input() product?: Product;
// 带默认值的输入属性
@Input() isOnSale = false;
// 重命名输入属性(模板中使用differentName,类中使用inputName)
@Input('differentName') inputName = '默认值';
}
父组件传递数据
父组件通过模板绑定将数据传递给子组件的输入属性:
// parent.component.ts
import { Component } from '@angular/core';
import { ProductCardComponent } from './child.component';
@Component({
selector: 'app-product-list',
standalone: true,
imports: [ProductCardComponent],
template: `
<h2>商品列表</h2>
<!-- 传递对象 -->
<app-product-card
[product]="currentProduct"
[isOnSale]="true"
></app-product-card>
<!-- 传递基本类型 -->
<app-product-card
[product]="{ id: 2, name: '耳机', price: 299 }"
[differentName]="'自定义名称'"
></app-product-card>
`
})
export class ProductListComponent {
currentProduct = { id: 1, name: '笔记本电脑', price: 5999 };
}
监听输入属性变化
使用ngOnChanges生命周期钩子监听输入属性的变化:
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
@Component({ /* ... */ })
export class ProductCardComponent implements OnChanges {
@Input() product?: Product;
// 监听输入属性变化
ngOnChanges(changes: SimpleChanges) {
// 检查product属性的变化
if (changes['product']) {
const oldValue = changes['product'].previousValue;
const newValue = changes['product'].currentValue;
console.log('商品变化:', oldValue, '→', newValue);
// 首次接收值时初始化
if (changes['product'].firstChange && newValue) {
this.initializeProduct(newValue);
}
}
}
private initializeProduct(product: Product) {
// 初始化逻辑
}
}
1.2 子传父:@Output与EventEmitter发送事件
@Output()装饰器结合EventEmitter允许子组件向父组件发送事件,传递数据或通知状态变化。
子组件定义输出事件
// child.component.ts
import { Component, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-quantity-selector',
standalone: true,
template: `
<div class="quantity-controls">
<button (click)="decrement()" [disabled]="quantity <= 1">-</button>
<span>{{ quantity }}</span>
<button (click)="increment()">+</button>
<button (click)="confirm()">确认</button>
</div>
`
})
export class QuantitySelectorComponent {
quantity = 1;
// 定义输出事件(传递数量变化)
@Output() quantityChange = new EventEmitter<number>();
// 定义自定义事件(传递确认信息)
@Output() confirmSelection = new EventEmitter<{ quantity: number, timestamp: number }>();
increment() {
this.quantity++;
this.quantityChange.emit(this.quantity); // 发送当前数量
}
decrement() {
if (this.quantity > 1) {
this.quantity--;
this.quantityChange.emit(this.quantity); // 发送当前数量
}
}
confirm() {
// 发送复杂数据
this.confirmSelection.emit({
quantity: this.quantity,
timestamp: Date.now()
});
}
}
父组件监听事件
父组件通过模板绑定监听子组件的输出事件:
// parent.component.ts
import { Component } from '@angular/core';
import { QuantitySelectorComponent } from './child.component';
@Component({
selector: 'app-order-form',
standalone: true,
imports: [QuantitySelectorComponent],
template: `
<h2>订单表单</h2>
<p>当前数量:{{ selectedQuantity }}</p>
<app-quantity-selector
(quantityChange)="onQuantityChange($event)"
(confirmSelection)="onConfirm($event)"
></app-quantity-selector>
@if (lastConfirm) {
<p>上次确认:{{ lastConfirm.quantity }}个({{ lastConfirm.timestamp | date:'medium' }})</p>
}
`
})
export class OrderFormComponent {
selectedQuantity = 1;
lastConfirm?: { quantity: number; timestamp: number };
// 处理数量变化事件
onQuantityChange(newQuantity: number) {
this.selectedQuantity = newQuantity;
console.log('数量变为:', newQuantity);
}
// 处理确认事件
onConfirm(data: { quantity: number; timestamp: number }) {
this.lastConfirm = data;
alert(`已确认购买${data.quantity}个`);
}
}
双向绑定简化
对于xxx输入和xxxChange输出的组合,可以使用[(xxx)]语法实现双向绑定:
// 子组件保持输出事件命名为quantityChange
@Output() quantityChange = new EventEmitter<number>();
// 父组件使用双向绑定
<app-quantity-selector [(quantity)]="selectedQuantity"></app-quantity-selector>
// 等价于
<app-quantity-selector
[quantity]="selectedQuantity"
(quantityChange)="selectedQuantity = $event"
></app-quantity-selector>
2. 父组件调用子组件方法:@ViewChild
当父组件需要直接调用子组件的方法或访问其属性时,可以使用@ViewChild()装饰器获取子组件实例。
2.1 基础用法
// child.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-message-box',
standalone: true,
template: `
<div class="message">{{ message }}</div>
`
})
export class MessageBoxComponent {
message = '初始消息';
// 子组件方法
showMessage(text: string, duration: number = 3000) {
this.message = text;
setTimeout(() => this.message = '', duration);
}
clear() {
this.message = '';
}
}
父组件通过@ViewChild()获取子组件实例:
// parent.component.ts
import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { MessageBoxComponent } from './child.component';
@Component({
selector: 'app-parent',
standalone: true,
imports: [MessageBoxComponent],
template: `
<button (click)="showSuccess()">显示成功消息</button>
<button (click)="clearMessage()">清除消息</button>
<app-message-box></app-message-box>
`
})
export class ParentComponent implements AfterViewInit {
// 获取子组件实例(在视图初始化后可用)
@ViewChild(MessageBoxComponent) messageBox?: MessageBoxComponent;
// 视图初始化完成后才能访问子组件
ngAfterViewInit() {
// 初始调用
this.messageBox?.showMessage('组件加载完成', 2000);
}
showSuccess() {
// 调用子组件方法
this.messageBox?.showMessage('操作成功!');
}
clearMessage() {
// 调用子组件方法
this.messageBox?.clear();
}
}
2.2 多个子组件与选择器
当存在多个相同类型的子组件时,可通过模板引用变量区分:
// 父组件模板
<app-message-box #msg1></app-message-box>
<app-message-box #msg2></app-message-box>
// 父组件类
@ViewChild('msg1') messageBox1?: MessageBoxComponent;
@ViewChild('msg2') messageBox2?: MessageBoxComponent;
// 使用
showDifferentMessages() {
this.messageBox1?.showMessage('消息1');
this.messageBox2?.showMessage('消息2');
}
对于动态生成的多个子组件,使用@ViewChildren()获取所有实例:
import { Component, ViewChildren, QueryList, AfterViewInit } from '@angular/core';
import { MessageBoxComponent } from './child.component';
@Component({
selector: 'app-parent',
standalone: true,
imports: [MessageBoxComponent],
template: `
@for (i of [1,2,3]; track i) {
<app-message-box></app-message-box>
}
<button (click)="updateAll()">更新所有消息</button>
`
})
export class ParentComponent implements AfterViewInit {
// 获取所有子组件实例
@ViewChildren(MessageBoxComponent) messageBoxes?: QueryList<MessageBoxComponent>;
updateAll() {
this.messageBoxes?.forEach((box, index) => {
box.showMessage(`消息 ${index + 1} 已更新`);
});
}
}
3. 跨级组件通信:服务与依赖注入
对于非父子关系的组件(如兄弟组件、祖孙组件、任意层级组件),使用服务(Service)结合依赖注入是最优雅的通信方式。通过单例服务共享状态,实现组件解耦。
3.1 基于Service的状态共享
创建共享服务存储状态,并提供方法修改状态和订阅状态变化:
// theme.service.ts
import { Injectable, signal, computed } from '@angular/core';
@Injectable({ providedIn: 'root' }) // 根注入器提供单例服务
export class ThemeService {
// 使用Signal存储主题状态(v20推荐)
private darkMode = signal(false);
// 计算属性:主题名称
themeName = computed(() => this.darkMode() ? 'dark' : 'light');
// 切换主题的方法
toggleTheme() {
this.darkMode.update(mode => !mode);
}
// 设置主题的方法
setDarkMode(enable: boolean) {
this.darkMode.set(enable);
}
}
任意组件都可以注入该服务,读取或修改主题状态:
// header.component.ts(修改状态)
import { Component, inject } from '@angular/core';
import { ThemeService } from './theme.service';
@Component({
selector: 'app-header',
standalone: true,
template: `
<header>
<h1>我的应用</h1>
<button (click)="toggleTheme()">
切换到{{ themeName() === 'light' ? '深色' : '浅色' }}模式
</button>
</header>
`
})
export class HeaderComponent {
private themeService = inject(ThemeService);
themeName = this.themeService.themeName; // 共享计算属性
toggleTheme() {
this.themeService.toggleTheme(); // 调用服务方法修改状态
}
}
// content.component.ts(读取状态)
import { Component, inject } from '@angular/core';
import { ThemeService } from './theme.service';
@Component({
selector: 'app-content',
standalone: true,
template: `
<div class="content" [class.dark]="isDarkMode()">
<p>当前主题:{{ themeName() }}</p>
<p>这是页面内容...</p>
</div>
`,
styles: [`
.content { padding: 1rem; }
.dark { background: #333; color: white; }
`]
})
export class ContentComponent {
private themeService = inject(ThemeService);
themeName = this.themeService.themeName;
// 派生状态
isDarkMode = computed(() => this.themeService.themeName() === 'dark');
}
3.2 基于Observable的事件总线
对于复杂的跨组件通信场景,可使用RxJS的Subject创建事件总线,实现多组件间的事件发布与订阅:
// event-bus.service.ts
import { Injectable } from '@angular/core';
import { Subject, Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
// 定义事件结构
interface EventBusEvent<T = any> {
type: string;
data: T;
}
@Injectable({ providedIn: 'root' })
export class EventBusService {
private eventBus = new Subject<EventBusEvent>();
// 发布事件
publish<T>(type: string, data: T) {
this.eventBus.next({ type, data });
}
// 订阅特定类型的事件
subscribe<T>(type: string): Observable<T> {
return this.eventBus.pipe(
filter(event => event.type === type),
map(event => event.data as T)
);
}
}
组件A发布事件:
// component-a.ts
import { Component, inject } from '@angular/core';
import { EventBusService } from './event-bus.service';
@Component({
selector: 'app-component-a',
standalone: true,
template: `
<button (click)="sendMessage()">发送消息到组件B</button>
`
})
export class ComponentA {
private eventBus = inject(EventBusService);
sendMessage() {
// 发布事件
this.eventBus.publish('user-message', {
text: '来自组件A的问候',
timestamp: Date.now()
});
}
}
组件B订阅事件:
// component-b.ts
import { Component, inject, OnInit, OnDestroy } from '@angular/core';
import { EventBusService } from './event-bus.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-component-b',
standalone: true,
template: `
<div class="messages">
<h3>收到的消息</h3>
@for (msg of messages; track msg.timestamp) {
<p>{{ msg.text }} ({{ msg.timestamp | date:'medium' }})</p>
}
</div>
`
})
export class ComponentB implements OnInit, OnDestroy {
private eventBus = inject(EventBusService);
private subscription?: Subscription;
messages: Array<{ text: string; timestamp: number }> = [];
ngOnInit() {
// 订阅事件
this.subscription = this.eventBus.subscribe<{ text: string; timestamp: number }>('user-message')
.subscribe(message => {
this.messages.push(message);
});
}
// 组件销毁时取消订阅,防止内存泄漏
ngOnDestroy() {
this.subscription?.unsubscribe();
}
}
4. 内容投影中的通信:@ContentChild
内容投影(Content Projection)允许父组件向子组件插入内容(通过ng-content),这种场景下的通信可通过@ContentChild()获取投影内容中的组件或元素。
4.1 基础用法
子组件(容器组件)通过@ContentChild()获取投影内容中的组件:
// card.component.ts(容器组件)
import { Component, ContentChild, AfterContentInit } from '@angular/core';
import { CardHeaderComponent } from './card-header.component';
@Component({
selector: 'app-card',
standalone: true,
template: `
<div class="card">
<!-- 内容投影插槽 -->
<ng-content select="app-card-header"></ng-content>
<div class="card-body">
<ng-content></ng-content>
</div>
<div class="card-footer">
<button (click)="onReset()">重置标题</button>
</div>
</div>
`
})
export class CardComponent implements AfterContentInit {
// 获取投影内容中的CardHeaderComponent
@ContentChild(CardHeaderComponent) header?: CardHeaderComponent;
// 内容投影初始化完成后访问
ngAfterContentInit() {
console.log('投影的标题组件:', this.header);
}
onReset() {
// 调用投影组件的方法
this.header?.resetTitle();
}
}
被投影的子组件:
// card-header.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-card-header',
standalone: true,
template: `
<h2>{{ title }}</h2>
<button (click)="changeTitle()">修改标题</button>
`
})
export class CardHeaderComponent {
title = '默认标题';
changeTitle() {
this.title = '修改后的标题';
}
resetTitle() {
this.title = '默认标题';
}
}
父组件使用:
// parent.component.ts
import { Component } from '@angular/core';
import { CardComponent } from './card.component';
import { CardHeaderComponent } from './card-header.component';
@Component({
selector: 'app-parent',
standalone: true,
imports: [CardComponent, CardHeaderComponent],
template: `
<app-card>
<!-- 投影内容 -->
<app-card-header></app-card-header>
<p>这是卡片内容</p>
</app-card>
`
})
export class ParentComponent {}
4.2 多个投影内容:@ContentChildren
当投影多个组件时,使用@ContentChildren()获取所有实例:
import { Component, ContentChildren, QueryList, AfterContentInit } from '@angular/core';
import { TabComponent } from './tab.component';
@Component({
selector: 'app-tabs',
standalone: true,
template: `
<div class="tabs">
<div class="tab-buttons">
@for (tab of tabs; track tab) {
<button (click)="selectTab(tab)" [class.active]="tab.isActive">
{{ tab.title }}
</button>
}
</div>
<div class="tab-content">
<ng-content></ng-content>
</div>
</div>
`
})
export class TabsComponent implements AfterContentInit {
// 获取所有投影的TabComponent
@ContentChildren(TabComponent) tabs?: QueryList<TabComponent>;
ngAfterContentInit() {
// 默认激活第一个标签
this.tabs?.first?.activate();
}
selectTab(selectedTab: TabComponent) {
// 先禁用所有标签
this.tabs?.forEach(tab => tab.deactivate());
// 激活选中的标签
selectedTab.activate();
}
}
5. 路由参数与查询参数:跨页面通信
在单页应用中,不同路由页面间的通信可通过路由参数、查询参数或路由状态实现,这是一种无耦合的通信方式。
5.1 路由参数传递
定义带参数的路由:
// app.routes.ts
export const routes = [
{ path: 'user/:id', component: UserProfileComponent }
];
导航时传递参数:
// 组件中通过路由导航传递参数
this.router.navigate(['/user', 123]); // 导航到/user/123
目标组件接收参数:
// user-profile.component.ts
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs';
@Component({ /* ... */ })
export class UserProfileComponent {
private route = inject(ActivatedRoute);
// 将路由参数转换为Signal
userId = toSignal(
this.route.paramMap.pipe(map(params => params.get('id'))),
{ initialValue: null }
);
}
5.2 查询参数传递
导航时添加查询参数:
// 导航到/products?category=electronics&sort=price
this.router.navigate(['/products'], {
queryParams: { category: 'electronics', sort: 'price' }
});
接收查询参数:
// product-list.component.ts
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({ /* ... */ })
export class ProductListComponent {
private route = inject(ActivatedRoute);
// 获取查询参数
queryParams = toSignal(this.route.queryParamMap, { initialValue: new Map() });
ngOnInit() {
const category = this.queryParams().get('category');
const sort = this.queryParams().get('sort');
// 根据参数加载数据
}
}
6. Angular v20通信新特性:Signals与独立组件
Angular v20引入的Signals为组件通信提供了更简洁、更高效的方式,尤其适合独立组件架构。
6.1 基于Signals的双向绑定
结合Signals实现更简洁的双向通信:
// child.component.ts
import { Component, input, output } from '@angular/core'; // v20输入输出新语法
@Component({
selector: 'app-counter',
standalone: true,
template: `
<button (click)="decrement()">-</button>
<span>{{ count() }}</span>
<button (click)="increment()">+</button>
`
})
export class CounterComponent {
// v20新语法:input()替代@Input()
count = input(0);
// v20新语法:output()替代@Output()
countChange = output<number>();
increment() {
this.countChange.emit(this.count() + 1);
}
decrement() {
this.countChange.emit(this.count() - 1);
}
}
父组件使用:
import { Component, signal } from '@angular/core';
import { CounterComponent } from './counter.component';
@Component({
selector: 'app-parent',
standalone: true,
imports: [CounterComponent],
template: `
<p>父组件计数:{{ parentCount() }}</p>
<app-counter
[count]="parentCount()"
(countChange)="parentCount.set($event)"
></app-counter>
`
})
export class ParentComponent {
parentCount = signal(0);
}
6.2 独立组件的服务共享
独立组件无需模块即可直接注入服务,简化了跨组件通信:
// 独立组件直接注入全局服务
import { Component, inject } from '@angular/core';
import { DataService } from './data.service';
@Component({
selector: 'app-standalone-component',
standalone: true,
template: `{{ data() | json }}`
})
export class StandaloneComponent {
private dataService = inject(DataService);
data = this.dataService.sharedData; // 直接使用服务中的Signal
}
7. 组件通信方式对比与选型
| 通信方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| @Input/@Output | 父子组件 | 简单直接,类型安全 | 只适用于直接父子关系,多层级传递繁琐 |
| @ViewChild/@ContentChild | 父子组件/内容投影 | 可直接调用方法 | 强耦合,依赖组件实例 |
| 共享服务(Signal) | 任意组件,状态共享 | 解耦,适合全局状态 | 需要设计服务接口,单例可能导致状态混乱 |
| 事件总线(Subject) | 任意组件,事件通知 | 解耦,适合复杂交互 | 需要管理订阅,可能导致事件泛滥 |
| 路由参数 | 路由页面间 | 无耦合,支持刷新 | 只适合简单数据,暴露在URL中 |
选型建议:
- 直接父子关系:优先使用
@Input/@Output - 复杂状态共享:使用
Signal+共享服务 - 松散耦合的跨组件通信:使用事件总线
- 路由页面间通信:使用路由参数或查询参数
- 避免过度使用
@ViewChild(增加耦合度)
总结
组件通信是Angular应用开发的核心环节,本章介绍了多种通信方式,从简单的父子组件交互到复杂的跨级组件通信,每种方式都有其适用场景。
Angular v20的Signals特性为组件通信带来了新的可能,通过响应式信号实现状态共享,既保持了组件解耦,又简化了数据流管理。在实际开发中,应根据组件关系、数据复杂度和应用规模选择合适的通信方式,优先采用服务共享和Signals等现代化方案,构建可维护、可扩展的Angular应用。

浙公网安备 33010602011771号