Angular组件开发Skill angular-component

Angular组件开发技能,专注于使用Angular v20+最新特性构建现代化、高性能、可访问的UI组件。涵盖信号式输入输出、OnPush变更检测、宿主绑定、内容投影、生命周期钩子等核心概念,遵循最佳实践,提升前端开发效率与代码质量。关键词:Angular, 组件开发, 信号, 前端框架, UI组件, 可访问性, 性能优化。

前端开发 0 次安装 0 次浏览 更新于 3/1/2026

name: angular-component description: 创建符合 Angular v20+ 最佳实践的现代独立组件。用于构建具有基于信号的输入/输出、OnPush变更检测、宿主绑定、内容投影和生命周期钩子的UI组件。在创建组件、将基于类的输入重构为信号、添加宿主绑定或实现可访问的交互式组件时触发。

Angular 组件

为 Angular v20+ 创建独立组件。组件默认是独立的——不要设置 standalone: true

组件结构

import { Component, ChangeDetectionStrategy, input, output, computed } from '@angular/core';

@Component({
  selector: 'app-user-card',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    'class': 'user-card',
    '[class.active]': 'isActive()',
    '(click)': 'handleClick()',
  },
  template: `
    <img [src]="avatarUrl()" [alt]="name() + ' avatar'" />
    <h2>{{ name() }}</h2>
    @if (showEmail()) {
      <p>{{ email() }}</p>
    }
  `,
  styles: `
    :host { display: block; }
    :host.active { border: 2px solid blue; }
  `,
})
export class UserCard {
  // 必需输入
  name = input.required<string>();
  
  // 可选输入,带默认值
  email = input<string>('');
  showEmail = input(false);
  
  // 带转换的输入
  isActive = input(false, { transform: booleanAttribute });
  
  // 根据输入计算
  avatarUrl = computed(() => `https://api.example.com/avatar/${this.name()}`);
  
  // 输出
  selected = output<string>();
  
  handleClick() {
    this.selected.emit(this.name());
  }
}

信号输入

// 必需 - 必须由父组件提供
name = input.required<string>();

// 可选,带默认值
count = input(0);

// 可选,无默认值(允许undefined)
label = input<string>();

// 带模板绑定别名
size = input('medium', { alias: 'buttonSize' });

// 带转换函数
disabled = input(false, { transform: booleanAttribute });
value = input(0, { transform: numberAttribute });

信号输出

import { output, outputFromObservable } from '@angular/core';

// 基本输出
clicked = output<void>();
selected = output<Item>();

// 带别名
valueChange = output<number>({ alias: 'change' });

// 从 Observable 创建(用于 RxJS 互操作)
scroll$ = new Subject<number>();
scrolled = outputFromObservable(this.scroll$);

// 发射值
this.clicked.emit();
this.selected.emit(item);

宿主绑定

@Component 中使用 host 对象——不要使用 @HostBinding@HostListener 装饰器。

@Component({
  selector: 'app-button',
  host: {
    // 静态属性
    'role': 'button',
    
    // 动态类绑定
    '[class.primary]': 'variant() === "primary"',
    '[class.disabled]': 'disabled()',
    
    // 动态样式绑定
    '[style.--btn-color]': 'color()',
    
    // 属性绑定
    '[attr.aria-disabled]': 'disabled()',
    '[attr.tabindex]': 'disabled() ? -1 : 0',
    
    // 事件监听器
    '(click)': 'onClick($event)',
    '(keydown.enter)': 'onClick($event)',
    '(keydown.space)': 'onClick($event)',
  },
  template: `<ng-content />`,
})
export class Button {
  variant = input<'primary' | 'secondary'>('primary');
  disabled = input(false, { transform: booleanAttribute });
  color = input('#007bff');
  
  clicked = output<void>();
  
  onClick(event: Event) {
    if (!this.disabled()) {
      this.clicked.emit();
    }
  }
}

内容投影

@Component({
  selector: 'app-card',
  template: `
    <header>
      <ng-content select="[card-header]" />
    </header>
    <main>
      <ng-content />
    </main>
    <footer>
      <ng-content select="[card-footer]" />
    </footer>
  `,
})
export class Card {}

// 用法:
// <app-card>
//   <h2 card-header>标题</h2>
//   <p>主要内容</p>
//   <button card-footer>操作</button>
// </app-card>

生命周期钩子

import { OnDestroy, OnInit, afterNextRender, afterRender } from '@angular/core';

export class My implements OnInit, OnDestroy {
  constructor() {
    // 用于渲染后的DOM操作(SSR安全)
    afterNextRender(() => {
      // 在首次渲染后运行一次
    });

    afterRender(() => {
      // 在每次渲染后运行
    });
  }

  ngOnInit() { /* 组件初始化 */ }
  ngOnDestroy() { /* 清理 */ }
}

可访问性要求

组件必须:

  • 通过 AXE 可访问性检查
  • 满足 WCAG AA 标准
  • 为交互元素包含适当的 ARIA 属性
  • 支持键盘导航
  • 保持可见的焦点指示器
@Component({
  selector: 'app-toggle',
  host: {
    'role': 'switch',
    '[attr.aria-checked]': 'checked()',
    '[attr.aria-label]': 'label()',
    'tabindex': '0',
    '(click)': 'toggle()',
    '(keydown.enter)': 'toggle()',
    '(keydown.space)': 'toggle(); $event.preventDefault()',
  },
  template: `<span class="toggle-track"><span class="toggle-thumb"></span></span>`,
})
export class Toggle {
  label = input.required<string>();
  checked = input(false, { transform: booleanAttribute });
  checkedChange = output<boolean>();
  
  toggle() {
    this.checkedChange.emit(!this.checked());
  }
}

模板语法

使用原生控制流——不要使用 *ngIf*ngFor*ngSwitch

<!-- 条件语句 -->
@if (isLoading()) {
  <app-spinner />
} @else if (error()) {
  <app-error [message]="error()" />
} @else {
  <app-content [data]="data()" />
}

<!-- 循环 -->
@for (item of items(); track item.id) {
  <app-item [item]="item" />
} @empty {
  <p>未找到项目</p>
}

<!-- 选择语句 -->
@switch (status()) {
  @case ('pending') { <span>待处理</span> }
  @case ('active') { <span>活跃</span> }
  @default { <span>未知</span> }
}

类和样式绑定

不要使用 ngClassngStyle。使用直接绑定:

<!-- 类绑定 -->
<div [class.active]="isActive()">单个类</div>
<div [class]="classString()">类字符串</div>

<!-- 样式绑定 -->
<div [style.color]="textColor()">样式化文本</div>
<div [style.width.px]="width()">带单位</div>

图片

对静态图片使用 NgOptimizedImage

import { NgOptimizedImage } from '@angular/common';

@Component({
  imports: [NgOptimizedImage],
  template: `
    <img ngSrc="/assets/hero.jpg" width="800" height="600" priority />
    <img [ngSrc]="imageUrl()" width="200" height="200" />
  `,
})
export class Hero {
  imageUrl = input.required<string>();
}

有关详细模式,请参阅 references/component-patterns.md