Chapter 02

组件与模板

Angular 的核心构建块:@Component 装饰器、模板绑定语法、新控制流指令与样式封装机制

1. @Component 装饰器

Angular 组件由三部分构成:TypeScript 类(逻辑)、HTML 模板(视图)、CSS 样式。它们通过 @Component 装饰器绑定在一起。

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

@Component({
  selector: 'app-user-card',      // CSS 选择器,在模板中用 <app-user-card>
  standalone: true,               // Standalone 模式(Angular 17+ 默认)
  imports: [CommonModule],          // 声明此组件使用的依赖
  templateUrl: './user-card.component.html',
  styleUrl:    './user-card.component.scss',
})
export class UserCardComponent implements OnInit {
  @Input() name: string = '';       // 父 → 子 输入属性
  @Input() age: number = 0;
  @Output() clicked = new EventEmitter<string>(); // 子 → 父 输出事件

  ngOnInit(): void {
    console.log(`UserCard 初始化:${this.name}`);
  }

  onClickHandler(): void {
    this.clicked.emit(this.name);
  }
}

@Component 关键元数据

2. 模板语法:绑定一览

Angular 模板语法是对标准 HTML 的扩展,提供了四种核心绑定方式:

绑定类型语法说明方向
插值{{ expression }}将表达式结果插入文本组件 → 视图
属性绑定[property]="expression"绑定 DOM 属性组件 → 视图
事件绑定(event)="handler()"监听 DOM 事件视图 → 组件
双向绑定[(ngModel)]="property"属性+事件组合双向
<!-- 插值:把组件属性渲染为文本 -->
<h1>{{ title }}</h1>
<p>总价:{{ price * quantity | currency:'CNY' }}</p>

<!-- 属性绑定:设置 DOM 属性(方括号) -->
<img [src]="user.avatarUrl" [alt]="user.name">
<button [disabled]="isLoading">提交</button>
<div [class.active]="isActive">状态</div>
<div [style.color]="isDanger ? 'red' : 'green'">颜色</div>

<!-- 事件绑定:监听事件(圆括号) -->
<button (click)="onSubmit()">提交</button>
<input (input)="onInputChange($event)">
<form (ngSubmit)="onFormSubmit()"></form>

<!-- 双向绑定:需要 FormsModule -->
<input [(ngModel)]="username" placeholder="用户名">
<p>你输入了:{{ username }}</p>

3. 新控制流语法(Angular 17+)

Angular 17 引入了全新的内置控制流语法,用 @if@for@switch 替代旧版的结构型指令 *ngIf*ngFor。新语法性能提升约 90%,且无需导入 CommonModule。

@if 条件渲染

<!-- 新语法(Angular 17+)-->
@if (user.isLoggedIn) {
  <div class="welcome">欢迎,{{ user.name }}!</div>
} @else if (user.isGuest) {
  <div class="guest">访客模式</div>
} @else {
  <button (click)="login()">立即登录</button>
}

<!-- 旧语法(仍有效,但不推荐用于新项目)-->
<div *ngIf="user.isLoggedIn; else loginBlock">欢迎,{{ user.name }}!</div>
<ng-template #loginBlock><button>登录</button></ng-template>

@for 列表渲染

<!-- 新语法:必须提供 track 表达式(性能关键)-->
<ul>
  @for (item of products; track item.id) {
    <li>{{ item.name }} - ¥{{ item.price }}</li>
  } @empty {
    <li>暂无商品</li>
  }
</ul>

<!-- 可访问内置变量 -->
@for (user of users; track user.id; let i = $index, isLast = $last) {
  <div [class.last]="isLast">{{ i + 1 }}. {{ user.name }}</div>
}

<!-- 旧语法 -->
<li *ngFor="let item of products; trackBy: trackById">{{ item.name }}</li>

@switch 多条件渲染

@switch (status) {
  @case ('loading') {
    <app-spinner />
  }
  @case ('success') {
    <app-data-table [data]="data" />
  }
  @case ('error') {
    <app-error-message [msg]="errorMsg" />
  }
  @default {
    <p>未知状态</p>
  }
}

@defer 懒加载(Angular 17+)

@defer 允许声明式地延迟加载某块内容,直到特定条件满足时才渲染,实现组件级别的代码分割。

<!-- 视口可见时才加载 -->
@defer (on viewport) {
  <app-heavy-chart [data]="chartData" />
} @loading {
  <p>图表加载中...</p>
} @placeholder {
  <div class="chart-skeleton"></div>
}

<!-- 空闲时加载 -->
@defer (on idle) {
  <app-footer />
}

<!-- 交互后加载 -->
@defer (on interaction(trigger)) {
  <app-dialog />
}
<button #trigger>打开对话框</button>

4. Standalone Component 详解

Standalone Component 是 Angular 14 引入、17+ 默认的组件模式。它的关键特征是:组件自己声明自己的依赖,不再需要 NgModule 作为容器。

// 一个完整的 Standalone 组件示例
import { Component, signal, computed } from '@angular/core';
import { CurrencyPipe } from '@angular/common';
import { RouterLink } from '@angular/router';
import { ProductCardComponent } from './product-card.component';

@Component({
  selector: 'app-shop',
  standalone: true,
  imports: [
    CurrencyPipe,          // Angular 内置管道
    RouterLink,            // Angular 路由指令
    ProductCardComponent,  // 自定义子组件
  ],
  template: `
    <h1>商品列表({{ total() }} 件)</h1>
    @for (product of products(); track product.id) {
      <app-product-card [product]="product" />
    }
    <p>总价:{{ totalPrice() | currency:'CNY' }}</p>
    <a routerLink="/checkout">去结算</a>
  `
})
export class ShopComponent {
  products = signal([
    { id: 1, name: 'Angular 入门书', price: 89 },
    { id: 2, name: 'TypeScript 手册', price: 69 },
  ]);
  total = computed(() => this.products().length);
  totalPrice = computed(() =>
    this.products().reduce((sum, p) => sum + p.price, 0)
  );
}

5. 输入/输出属性

Angular 18 新增了函数式 API input()output(),与 Signals 更好地集成,逐渐替代 @Input()@Output() 装饰器。

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

@Component({
  selector: 'app-button',
  standalone: true,
  template: `
    <button [class]="variant()" (click)="click.emit()">
      {{ label() }}
    </button>
  `
})
export class ButtonComponent {
  // input() 返回一个 Signal,可直接在模板和代码中用 label()
  label = input<string>('点击');        // 有默认值
  variant = input.required<string>(); // 必填
  click = output<void>();             // 替代 @Output
}

<!-- 父组件使用 -->
<app-button label="提交" variant="primary" (click)="handleClick()" />

6. 样式封装 ViewEncapsulation

Angular 组件的样式默认是局部作用域的——不会影响其他组件,也不会被外部样式覆盖。这是通过 ViewEncapsulation 实现的。

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

@Component({
  selector: 'app-global-styles',
  standalone: true,
  template: `<div class="my-class">内容</div>`,
  styles: [`.my-class { color: red; }`],
  encapsulation: ViewEncapsulation.None  // 全局生效
})
export class GlobalStylesComponent {}

本章小结:Angular 组件通过 @Component 装饰器将逻辑、模板、样式绑定在一起。新的 @if/@for 控制流语法性能更好,Standalone 组件让代码更简洁。下一章深入依赖注入系统。