WinddSnow

Angular-Component-and-Template-Syntax

字数统计: 6.3k阅读时长: 27 min
2026/08/01

第116课:Angular 组件与模板语法——数据绑定、*ngIf*ngFor、管道、样式封装

1
Angular 组件由三部分组成:一个 TypeScript 类(逻辑)、一个 HTML 模板(视图)和一个可选的 CSS 样式文件。模板是 Angular 组件中最具特色的部分——它不仅是静态 HTML,还通过一套**声明式绑定语法**将组件类的数据与 DOM 元素连接起来。Angular 提供了四种数据绑定方式:**插值**(`{{ }}`)、**属性绑定**(`[ ]`)、**事件绑定**(`( )`)和**双向绑定**(`[( )]`)。同时,Angular 内置了一套**结构型指令**(`*ngIf`、`*ngFor`、`*ngSwitch`)用于控制 DOM 结构的增删和重复渲染,以及**管道**(Pipe)用于在模板中格式化数据。本节课将从零开始,逐一拆解这些模板语法的原理和使用方式,并通过一个完整的任务管理组件将所学知识串联。

1. 组件的基本结构:装饰器、选择器、模板与样式

在 Angular 中,一个组件是由 @Component 装饰器标注的 TypeScript 类。@Component 接收一个元数据对象,用于描述组件的模板、样式、选择器(CSS 选择器形式的标签名)以及它所属的模块。

1
2
3
4
5
6
7
8
9
10
11
// task-list.component.ts
import { Component } from '@angular/core';

@Component({
selector: 'app-task-list', // 在 HTML 中作为 <app-task-list> 使用
templateUrl: './task-list.component.html', // 外部模板文件
styleUrls: ['./task-list.component.scss'], // 外部样式文件
})
export class TaskListComponent {
tasks = ['学习 Angular', '完成项目', '代码审查'];
}

**templateUrltemplate**:推荐使用 templateUrl 引用外部 HTML 文件。对于极简组件,也可以使用 template 属性直接写内联 HTML 字符串。

styleUrlsstyles:同理,styleUrls 引用外部 CSS/SCSS 文件,styles 写内联样式数组。Angular 默认使用样式封装ViewEncapsulation.Emulated),为每个组件的 CSS 生成独特的属性选择器,确保组件样式不会泄漏到外部,也不会被外部样式意外覆盖。

1.1 组件类的生命周期

组件类中可以定义属性和方法,这些属性和方法就是模板的数据源和事件处理器。Angular 在创建组件实例后,会按照既定的生命周期调用钩子方法(如 ngOnInitngOnChangesngOnDestroy 等),这些将在后续课程详述。本节课聚焦于模板如何访问组件类中的成员——任何 public 的属性和方法都可以在模板中直接使用。


2. 数据绑定:将组件类与模板连接

Angular 模板中的数据绑定是一种声明式机制:你不需要手动调用 DOM API 来更新元素内容或属性,只需在模板中用特殊语法绑定组件类的成员,Angular 会在变更检测时自动同步 DOM。

2.1 插值(Interpolation)

1
最基础的绑定方式,用于将组件属性的值渲染为元素的文本内容。`{{ }}` 内部可以写任何合法的 TypeScript 表达式(但不能有副作用——如赋值、`new`、`++`、`--` 等)。
1
2
3
4
5
6
7
export class AppComponent {
title = '我的应用';
version = 1;
getVersionInfo() {
return `v${this.version}`;
}
}
1
2
3
<h1>{{ title }}</h1>
<p>版本:{{ getVersionInfo() }}</p>
<p>明年版本号:{{ version + 1 }}</p>
1
**安全机制**:Angular 自动对插值结果进行 HTML 转义,防止 XSS 攻击。如果组件中有 `htmlSnippet = '<script>alert("XSS")</script>'`,使用 `{{ htmlSnippet }}` 会将其显示为纯文本,而非执行脚本。若确实需要渲染原始 HTML(谨慎使用),需使用 `[innerHTML]` 属性绑定并配合 Angular 的 `DomSanitizer` 服务。

2.2 属性绑定(Property Binding):[property]="expression"

用于将组件数据单向地流向 DOM 元素的属性(Property,而非 HTML Attribute)。属性绑定使用方括号 [ ] 语法。

1
2
3
4
5
6
7
8
9
10
11
<!-- 将 imgUrl 属性绑定到 <img> 的 src 属性 -->
<img [src]="imgUrl" [alt]="imgAlt" />

<!-- 绑定样式 -->
<div [style.background-color]="bgColor">彩色方块</div>

<!-- 绑定 CSS 类名 -->
<div [class.active]="isActive">可切换激活状态</div>

<!-- 绑定 HTML 属性(Attribute)——少数情况使用 -->
<button [attr.aria-label]="buttonLabel">点击</button>

Property vs Attribute:DOM 元素的 Property 是 JavaScript 对象的属性,反映元素的当前状态(如 input.value 是用户实时输入的值)。HTML Attribute 是写在标签上的静态字符串,仅用于初始化 Property。Angular 的属性绑定绑定的是 DOM Property,而非 HTML Attribute。例如 [disabled]="isDisabled" 绑定的是 HTMLButtonElement.disabled Property,当 isDisabledtrue 时按钮不可用;而 attr.disabled 绑定的是 HTML 标签上的 disabled 属性字符串。

2.3 事件绑定(Event Binding):(event)="handler()"

用于从 DOM 元素单向地向组件传递用户操作。事件绑定使用圆括号 ( ) 语法。

1
2
3
4
5
6
7
8
9
10
11
12
export class ButtonComponent {
count = 0;

handleClick(): void {
this.count++;
console.log('按钮点击次数:', this.count);
}

handleKeyUp(event: KeyboardEvent): void {
console.log('按键:', event.key);
}
}
1
2
<button (click)="handleClick()">点击我 ({{ count }})</button>
<input (keyup)="handleKeyUp($event)" placeholder="输入内容" />

$event 对象$event 是 Angular 模板中的特殊变量,代表原生 DOM 事件对象。在事件处理函数中可以通过 $event.target.value 获取输入框的值。不过更好的方式是使用双向绑定(见下文)或模板引用变量(#inputRef)配合 (input) 事件。

2.4 双向绑定(Two-way Binding):[(ngModel)]

将属性绑定和事件绑定组合为一个语法糖。[(ngModel)] 是 Angular 内置的双向绑定指令,需要导入 FormsModule 才能使用。

1
2
3
4
5
6
7
// app.module.ts
import { FormsModule } from '@angular/forms';

@NgModule({
imports: [FormsModule, /* ... */],
})
export class AppModule {}
1
2
<input [(ngModel)]="username" placeholder="用户名" />
<p>输入的用户名:{{ username }}</p>

[(ngModel)]="username" 等价于 [ngModel]="username" (ngModelChange)="username = $event"。它既将 username 的值绑定到输入框的 value 属性,又监听输入框的 input 事件,将新值写回 username。**ngModelFormsModule 提供的指令,不是 Angular 核心的一部分**,因此使用前必须导入。

2.5 四种绑定的记忆法则

绑定方向 语法 记忆口诀
组件 → 模板(文本) {{ expression }} 双花括号
组件 → 模板(属性) [property]="value" 方括号
模板 → 组件(事件) (event)="handler()" 圆括号
双向 [(ngModel)]="value" 香蕉箱(方括号套圆括号)

3. 结构型指令:*ngIf*ngFor*ngSwitch

结构型指令通过添加或移除 DOM 元素来改变页面结构。它们以星号(*)为前缀,这是 Angular 的语法糖——实际上每个 * 指令都被展开为一个 <ng-template> 包裹的结构。

3.1 *ngIf:条件渲染

当表达式为 truthy 时,渲染宿主元素及其子节点;为 falsy 时,将其从 DOM 中完全移除(而非隐藏)。

1
2
3
4
5
6
<div *ngIf="isLoggedIn; else loggedOut">
欢迎回来,{{ username }}!
</div>
<ng-template #loggedOut>
<div>请先登录。</div>
</ng-template>

[hidden] 的区别[hidden] 仅设置 CSS display: none,元素仍在 DOM 中,占用内存并可能被屏幕阅读器读取。*ngIf 完全销毁或创建元素,性能更优(对于不常切换的区块),但频繁切换时创建/销毁开销更大。选择原则:内容敏感或需要销毁资源时用 *ngIf;纯视觉切换且需要保留状态时用 [hidden] 或 CSS

as 语法:在 *ngIf 中将条件结果赋值给局部变量,供后续使用。

1
2
3
<div *ngIf="user$ | async as user">
{{ user.name }}
</div>

3.2 *ngFor:循环渲染

为数组中的每个元素重复渲染宿主元素。语法为 *ngFor="let item of items; index as i; trackBy: trackByFn"

1
2
3
4
5
<ul>
<li *ngFor="let task of tasks; index as i; trackBy: trackById">
{{ i + 1 }}. {{ task.title }} - {{ task.priority }}
</li>
</ul>

trackBy 的作用:默认情况下,当 tasks 数组的引用发生变化时,Angular 会销毁所有旧的 DOM 元素并重建新的。如果数组中只是修改了某个元素的属性(而非整体替换),大量的 DOM 操作是不必要的。trackBy 函数接收 indexitem,返回一个唯一标识(如 item.id),Angular 根据该标识来跟踪元素,仅更新发生变化的部分,大幅提升列表渲染性能。

1
2
3
trackById(index: number, item: Task): number {
return item.id;
}

局部变量*ngFor 提供了多个可用的局部变量:

变量 含义
index 当前元素的索引(从 0 开始)
count 数组的总长度
first 是否为第一个元素(boolean)
last 是否为最后一个元素(boolean)
even 索引是否为偶数(boolean)
odd 索引是否为奇数(boolean)
1
2
3
4
<li *ngFor="let item of items; index as i; first as isFirst"
[class.first-item]="isFirst">
{{ item }}
</li>

3.3 *ngSwitch:多分支条件渲染

相当于 JavaScript 的 switch 语句,用于根据一个表达式的值从多个可能的模板中选择一个渲染。

1
2
3
4
5
<div [ngSwitch]="userRole">
<app-admin-panel *ngSwitchCase="'admin'"></app-admin-panel>
<app-editor-panel *ngSwitchCase="'editor'"></app-editor-panel>
<app-viewer-panel *ngSwitchDefault></app-viewer-panel>
</div>

[ngSwitch] 是属性绑定,*ngSwitchCase*ngSwitchDefault 是结构型指令。与 *ngIf 类似,不匹配的分支会被完全从 DOM 中移除。


4. 管道(Pipe):数据格式化

管道是 Angular 模板中用于转换和格式化数据的工具。你可以在插值和属性绑定中使用管道,通过 | 符号连接。

4.1 内置管道

管道 用途 示例
uppercase 将字符串转为全大写 {{ 'hello' | uppercase }}HELLO
lowercase 将字符串转为全小写 {{ 'HELLO' | lowercase }}hello
date 格式化日期对象 {{ today | date:'yyyy-MM-dd' }}
currency 格式化货币值 {{ price | currency:'CNY' }}¥100.00
number / decimal 格式化数字 {{ pi | number:'1.2-3' }}3.14
percent 格式化为百分比 {{ 0.85 | percent }}85%
json 将对象序列化为 JSON 字符串(调试用) {{ user | json }}
async 订阅 Promise 或 Observable,自动取值 {{ data$ | async }}
slice 截取数组或字符串的部分 {{ items | slice:0:5 }}

4.2 管道链与管道参数

管道可以串联使用(前一个管道的输出作为后一个管道的输入),部分管道可以接收参数(用 : 分隔)。

1
2
<p>生日:{{ birthday | date:'yyyy年MM月dd日' | uppercase }}</p>
<p>价格:{{ price | currency:'CNY':'symbol-narrow' }}</p>

4.3 自定义管道

使用 @Pipe 装饰器并实现 PipeTransform 接口。管道类必须提供一个 transform 方法。

1
2
3
4
5
6
7
8
9
10
11
// truncate.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'truncate' })
export class TruncatePipe implements PipeTransform {
transform(value: string, limit: number = 20, ellipsis: string = '...'): string {
if (!value) return '';
if (value.length <= limit) return value;
return value.substring(0, limit) + ellipsis;
}
}

在模块的 declarations 中注册后即可在模板中使用:

1
<p>{{ longText | truncate:50 }}</p>

纯管道与不纯管道:默认情况下,管道是的——仅当输入值的引用发生变化时,Angular 才会重新执行 transform。如果输入是一个对象,修改对象内部的属性不会触发管道重新计算(因为引用未变)。如果需要检测对象内部的变化,可以将管道设为 pure: false(不纯管道),但会带来性能开销,因为每次变更检测都会执行。大多数场景下,应使用纯管道,并在数据变化时替换对象引用(不可变更新)。


5. 样式封装(View Encapsulation)

Angular 默认使用 ViewEncapsulation.Emulated 模式。它会为每个组件的 HTML 元素生成一个唯一的属性(如 _ngcontent-xxx),并将组件的 CSS 选择器自动附加该属性选择器,从而将样式的作用域限制在组件内部。

1
2
3
4
5
/* 组件写的 CSS */
p { color: red; }

/* Angular 编译后等价于 */
p[_ngcontent-xxx] { color: red; }

三种封装模式

模式 行为
Emulated(默认) 样式被限定在组件内部,通过属性选择器模拟 Shadow DOM。
ShadowDom 使用浏览器原生 Shadow DOM 封装样式,完全隔离。
None 样式全局生效,不添加任何隔离机制。适用于需要全局样式穿透的场景。

@Component 中显式指定封装模式:

1
2
3
4
5
6
7
8
import { Component, ViewEncapsulation } from '@angular/core';

@Component({
selector: 'app-no-encapsulation',
template: `<p>全局样式会生效</p>`,
encapsulation: ViewEncapsulation.None,
})
export class NoEncapsulationComponent {}

::ng-deep(已弃用):在 Emulated 模式下,如果需要穿透组件样式作用于子组件,可以使用 ::ng-deep 伪类选择器(但已官方弃用,推荐使用全局样式文件或 ShadowDom 模式替代)。


6. 模板引用变量(Template Reference Variable)

使用 #variableName 在模板中声明一个局部变量,指向一个 DOM 元素、组件实例或指令。可以在当前模板的任何地方使用该变量。

1
2
3
4
5
6
7
<!-- 指向 input 元素 -->
<input #taskInput type="text" placeholder="输入任务" />
<button (click)="addTask(taskInput.value); taskInput.value = ''">添加</button>

<!-- 指向子组件实例 -->
<app-task-list #taskListComp></app-task-list>
<button (click)="taskListComp.refresh()">刷新列表</button>

模板引用变量的作用域是当前模板,在组件类中不能直接访问(除非通过 @ViewChild 装饰器)。它常用于表单输入、获取子组件引用和与 *ngFor 配合。


7. 综合实战:任务管理组件

以下示例将数据绑定、结构型指令、管道和模板引用变量整合为一个功能完整的任务管理组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// task.model.ts
export interface Task {
id: number;
title: string;
completed: boolean;
priority: 'high' | 'medium' | 'low';
createdAt: Date;
}

// task-manager.component.ts
import { Component } from '@angular/core';
import { Task } from './task.model';

@Component({
selector: 'app-task-manager',
templateUrl: './task-manager.component.html',
styleUrls: ['./task-manager.component.scss'],
})
export class TaskManagerComponent {
tasks: Task[] = [
{ id: 1, title: '学习 Angular 模板语法', completed: false, priority: 'high', createdAt: new Date('2026-08-01') },
{ id: 2, title: '完成项目文档', completed: true, priority: 'medium', createdAt: new Date('2026-07-28') },
{ id: 3, title: '代码审查', completed: false, priority: 'low', createdAt: new Date('2026-07-25') },
];

newTaskTitle = '';
filter: 'all' | 'active' | 'completed' = 'all';

get filteredTasks(): Task[] {
if (this.filter === 'active') {
return this.tasks.filter(t => !t.completed);
} else if (this.filter === 'completed') {
return this.tasks.filter(t => t.completed);
}
return this.tasks;
}

addTask(title: string): void {
if (!title.trim()) return;
this.tasks.push({
id: Date.now(),
title: title.trim(),
completed: false,
priority: 'medium',
createdAt: new Date(),
});
this.newTaskTitle = '';
}

toggleTask(task: Task): void {
task.completed = !task.completed;
}

deleteTask(id: number): void {
this.tasks = this.tasks.filter(t => t.id !== id);
}

setFilter(f: 'all' | 'active' | 'completed'): void {
this.filter = f;
}

trackById(index: number, task: Task): number {
return task.id;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<!-- task-manager.component.html -->
<div class="task-manager">
<h2>任务管理器</h2>

<!-- 添加任务表单 -->
<div class="task-form">
<input
#taskInput
[(ngModel)]="newTaskTitle"
(keyup.enter)="addTask(taskInput.value); taskInput.value = ''"
placeholder="输入新任务..."
/>
<button (click)="addTask(taskInput.value); taskInput.value = ''">添加</button>
</div>

<!-- 筛选按钮 -->
<div class="filters">
<button
*ngFor="let f of ['all', 'active', 'completed']"
[class.active]="filter === f"
(click)="setFilter(f)">
{{ f === 'all' ? '全部' : f === 'active' ? '未完成' : '已完成' }}
</button>
</div>

<!-- 统计信息 -->
<p class="stats">
共 {{ tasks.length }} 项,
已完成 {{ tasks.filter(t => t.completed).length }} 项,
未完成 {{ tasks.filter(t => !t.completed).length }} 项
</p>

<!-- 任务列表 -->
<ul class="task-list" *ngIf="filteredTasks.length > 0; else empty">
<li
*ngFor="let task of filteredTasks; trackBy: trackById"
[class.completed]="task.completed"
[class.priority-high]="task.priority === 'high'"
[class.priority-medium]="task.priority === 'medium'"
[class.priority-low]="task.priority === 'low'"
>
<input
type="checkbox"
[checked]="task.completed"
(change)="toggleTask(task)"
/>
<span class="task-title">{{ task.title }}</span>
<span class="task-date">{{ task.createdAt | date:'MM/dd' }}</span>
<button (click)="deleteTask(task.id)">×</button>
</li>
</ul>
<ng-template #empty>
<p class="empty-message">暂无任务</p>
</ng-template>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/* task-manager.component.scss */
.task-manager {
max-width: 500px;
margin: 0 auto;
padding: 24px;
}
.task-form {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.task-form input {
flex: 1;
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 4px;
}
.filters {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.filters button {
padding: 4px 12px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
cursor: pointer;
}
.filters button.active {
background: #4f46e5;
color: white;
border-color: #4f46e5;
}
.stats {
font-size: 0.875rem;
color: #666;
margin-bottom: 12px;
}
.task-list {
list-style: none;
padding: 0;
}
.task-list li {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border: 1px solid #eee;
border-radius: 6px;
margin-bottom: 6px;
border-left: 4px solid #ccc;
}
.task-list li.priority-high { border-left-color: #ef4444; }
.task-list li.priority-medium { border-left-color: #f59e0b; }
.task-list li.priority-low { border-left-color: #10b981; }
.task-list li.completed .task-title {
text-decoration: line-through;
color: #999;
}
.task-date {
margin-left: auto;
font-size: 0.75rem;
color: #999;
}

技术要点

  • [(ngModel)] 实现输入框的双向绑定,配合模板引用变量 #taskInput 在回车时获取值。
  • *ngFor 渲染任务列表,trackBy 优化渲染性能。
  • *ngIf 条件显示“暂无任务”的空状态。
  • [class.xxx] 属性绑定动态设置 CSS 类名,实现优先级颜色和完成状态样式。
  • date 管道格式化任务创建日期。
  • [ngSwitch] 虽未在本例使用,但同等场景下可用 [ngSwitch] 替代 *ngIf 的多条件分支。
  • 组件类中的 get filteredTasks() 是一个 TypeScript getter,在模板中被视为属性使用,每次变更检测时重新计算。

课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) Angular 中,[hidden]="condition"*ngIf="condition" 的核心区别是什么?
    A. *ngIf 仅隐藏元素,[hidden] 从 DOM 中移除元素。
    B. [hidden] 仅隐藏元素,*ngIf 从 DOM 中完全移除或创建元素。
    C. 两者行为完全相同。
    D. [hidden] 是 Angular 指令,*ngIf 是 HTML 原生属性。

  2. (单选)*ngFor 中使用 trackBy 的主要目的是?
    A. 提高数组遍历的速度。
    B. 避免不必要的变化检测。
    C. 通过唯一标识跟踪元素,减少不必要的 DOM 重建,提升渲染性能。
    D. 动态修改数组的长度。

  3. (填空) Angular 模板中,使用 | 符号对数据进行格式化转换的工具称为 ______

  4. (多选) 以下哪些是 Angular 模板中合法的数据绑定语法?
    A. {{ title }}
    B. [src]="imageUrl"
    C. (click)="handleClick()"
    D. [[ngModel]]="name"

二、AI 编程任务:编写面向 AI 的提示词

场景:你需要实现一个 Angular 组件 ProductListComponent,用于展示商品列表并支持筛选和排序。要求如下:

  • 使用 *ngFor 渲染商品列表,每个商品包含名称、价格、分类和库存状态。
  • 使用 *ngIf 条件显示“暂无商品”的空状态。
  • 通过属性绑定 [class.out-of-stock] 标记库存为 0 的商品。
  • 提供一个下拉框,使用 [(ngModel)] 双向绑定分类筛选条件,点击筛选按钮过滤商品。
  • 使用自定义管道 sortByPrice 对商品列表按价格排序(升序/降序由组件属性控制)。
  • 使用 date 管道格式化商品的发布日期。

任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的完整 Angular 组件代码(含 TypeScript 类、HTML 模板、SCSS 样式和自定义管道)。提示词中需明确指定组件选择器、数据模型、指令和管道的使用方式。

三、Agent 模式下的提示词示例

你是一个资深前端开发 Agent。请创建一个 Angular 商品列表组件和对应的自定义管道。需要创建以下文件:

  1. src/app/product/product.model.ts:导出 Product 接口,包含 id: numbername: stringprice: numbercategory: 'electronics' | 'clothing' | 'food'stock: numbercreatedAt: Date
  2. src/app/product/product-list/product-list.component.ts
    • 选择器 app-product-list
    • 属性:products: Product[](静态数据,至少 5 条不同分类和库存的商品)、selectedCategory: string = 'all'sortOrder: 'asc' | 'desc' = 'asc'
    • 计算属性 filteredProducts:先按 selectedCategory 过滤('all' 则不过滤),再按 price 排序(使用 sortOrder)。
    • 方法 setSort(order: 'asc' | 'desc') 切换排序。
    • 模板:使用 [(ngModel)] 绑定下拉框到 selectedCategory*ngFor 遍历 filteredProductstrackBy 使用 product.id[class.out-of-stock]="product.stock === 0"*ngIf 判断 filteredProducts.length === 0 显示空状态;date 管道格式化 createdAt;点击排序按钮调用 setSort
  3. src/app/product/pipes/sort-by-price.pipe.ts
    • 自定义管道 sortByPrice(纯管道)。
    • transform(products: Product[], order: 'asc' | 'desc' = 'asc'): Product[]:返回排序后的新数组(不修改原数组)。
  4. 在模板中使用管道:*ngFor="let p of filteredProducts | sortByPrice:sortOrder"(如果已在 getter 中排序则无需管道,为演示目的可任选一种方式;Agent 应选择在 getter 中排序,管道仅作为演示备用或单独导出)。
    • Agent 可决定将管道用于单独示例,或采用 getter 方式。为了展示管道用法,可在模板中对另一个数组使用。
  5. 添加 JSDoc 注释和适当的 CSS 样式(.out-of-stock { opacity: 0.5; } 等)。完成后列出所有文件内容。

四、面试真题与参考答案

题目(腾讯前端面试题):

请解释 Angular 中 *ngIf[hidden] 的区别,以及各自适合的使用场景。另外,为什么在 *ngFor 中需要使用 trackBy 函数?如果不使用 trackBy,当列表数据发生变化时 Angular 会如何处理 DOM?请结合变更检测机制说明。

参考答案

*ngIf 是结构型指令,当条件为 false 时,它会将宿主元素及其子节点完全从 DOM 中移除,同时销毁组件实例(如果是组件),释放相关资源。当条件变回 true 时,Angular 会重新创建 DOM 元素和组件实例。[hidden] 是属性绑定,它仅设置元素的 hidden 属性(等价于 CSS display: none),元素仍在 DOM 中,组件实例未被销毁,事件监听和订阅也依然存在。

*ngIf 适合内容不常切换但资源占用较大的场景(如模态框、大型数据面板),切换时彻底释放资源。[hidden] 适合频繁切换且需要保留状态的场景(如标签页中已填写内容的表单——使用 *ngIf 切换会丢失用户的输入),或切换开销极小(仅视觉隐藏)。

*ngFor 中的 trackBy 用于优化列表渲染性能。默认情况下,当 *ngFor 的表达式数组引用发生变化时(如 this.items = newItems),Angular 会销毁所有旧的 DOM 元素并从头创建新元素。如果数组中大部分数据并未变化(如仅修改了某一项的属性),这种全量重建是完全不必要的。trackBy 函数返回一个唯一标识(如 item.id),Angular 根据该标识来跟踪每个元素的身份:当新数组中存在与旧数组相同标识的项时,Angular 会复用对应的 DOM 元素,仅更新其中变化的部分,而非销毁重建。

不使用时,对于大数据列表的频繁更新,DOM 操作的开销和变更检测的负担会显著增加。trackBy 是 Angular 性能优化的关键手段之一。


课后练习答案

一、概念自测答案

  1. B

    • 解析:[hidden] 仅隐藏元素(CSS display: none),元素仍在 DOM 中;*ngIf 在条件为 false 时从 DOM 中完全移除元素。
  2. C

    • 解析:trackBy 通过唯一标识跟踪元素,避免不必要的 DOM 销毁与重建,提升列表渲染性能。它不直接提高遍历速度,也不修改数组长度。
  3. 管道(或 Pipe

    • 解析:管道使用 | 符号对数据进行格式化,如 {{ birthday | date }}
  4. A、B、C

    • 解析:D 错误,双向绑定的语法是 [(ngModel)] 而非 [[ngModel]]。A(插值)、B(属性绑定)、C(事件绑定)均为合法语法。

二、AI 编程任务参考答案(提示词示例)

示例提示词
“请用 Angular 实现一个商品列表组件和自定义排序管道。要求:

  • Product 接口:id, name, price, category, stock, createdAt。
  • 组件:使用 *ngFor 渲染列表,*ngIf 处理空状态,[(ngModel)] 绑定筛选下拉框,[class.out-of-stock] 标记库存为 0 的商品,date 管道格式化日期。
  • 自定义管道 sortByPrice:纯管道,按价格排序,参数为 ‘asc’ 或 ‘desc’。
  • 输出组件 TS、HTML、SCSS 和管道文件的完整代码。”
CATALOG
  1. 1. 第116课:Angular 组件与模板语法——数据绑定、*ngIf、*ngFor、管道、样式封装
    1. 1.1. 1. 组件的基本结构:装饰器、选择器、模板与样式
      1. 1.1.1. 1.1 组件类的生命周期
    2. 1.2. 2. 数据绑定:将组件类与模板连接
      1. 1.2.1. 2.1 插值(Interpolation)
      2. 1.2.2. 2.2 属性绑定(Property Binding):[property]="expression"
      3. 1.2.3. 2.3 事件绑定(Event Binding):(event)="handler()"
      4. 1.2.4. 2.4 双向绑定(Two-way Binding):[(ngModel)]
      5. 1.2.5. 2.5 四种绑定的记忆法则
    3. 1.3. 3. 结构型指令:*ngIf、*ngFor、*ngSwitch
      1. 1.3.1. 3.1 *ngIf:条件渲染
      2. 1.3.2. 3.2 *ngFor:循环渲染
      3. 1.3.3. 3.3 *ngSwitch:多分支条件渲染
    4. 1.4. 4. 管道(Pipe):数据格式化
      1. 1.4.1. 4.1 内置管道
      2. 1.4.2. 4.2 管道链与管道参数
      3. 1.4.3. 4.3 自定义管道
    5. 1.5. 5. 样式封装(View Encapsulation)
    6. 1.6. 6. 模板引用变量(Template Reference Variable)
    7. 1.7. 7. 综合实战:任务管理组件
    8. 1.8. 课后练习
      1. 1.8.1. 一、概念自测(选择题 / 填空题)
      2. 1.8.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.8.3. 三、Agent 模式下的提示词示例
      4. 1.8.4. 四、面试真题与参考答案
    9. 1.9. 课后练习答案
      1. 1.9.1. 一、概念自测答案
      2. 1.9.2. 二、AI 编程任务参考答案(提示词示例)