Angular 组件:组件装饰器与模板语法
Angular 作为现代前端框架,组件是其核心概念之一。理解组件装饰器的配置方式以及模板语法的使用技巧,是构建 Angular 应用的基础能力。本文将系统讲解这两部分内容,帮助你快速掌握 Angular 组件的开发要点。
一、认识组件装饰器
Angular 使用 TypeScript 的装饰器来定义组件。@Component 装饰器不仅标识一个类为组件,还负责配置组件的各种行为属性。
1.1 最小化的组件定义
最基本的组件只需要两个属性:模板和样式。
import { Component } from '@angular/core';
@Component({
selector: 'app-simple',
template: '<p>Hello Angular</p>',
styles: ['p { color: blue; }']
})
export class SimpleComponent {}
上述代码定义了一个最简单的组件。selector 指定了组件在 HTML 中的使用方式,template 定义了组件的视图结构,styles 则包含组件私有的样式定义。
1.2 装饰器核心配置项
@Component 装饰器支持多个配置项,以下是最常用的几个:
| 配置项 | 作用说明 | 典型场景 |
|---|---|---|
selector |
组件的 CSS 选择器名称 | 定义如何在 HTML 中使用组件 |
template / templateUrl |
组件的内联模板或外部模板文件路径 | 分离视图代码与组件逻辑 |
styles / styleUrls |
组件的内联样式或外部样式文件路径 | 实现样式隔离 |
standalone |
声明是否为独立组件(Angular 14+) | 现代 Angular 开发的推荐方式 |
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-user-card',
standalone: true,
imports: [CommonModule],
templateUrl: './user-card.component.html',
styleUrls: ['./user-card.component.css']
})
export class UserCardComponent {
name = '张三';
age = 28;
}
使用 standalone: true 声明独立组件后,需要在 imports 数组中显式导入所需模块(如 CommonModule 包含 *ngIf、*ngFor 等常用指令)。
1.3 其他实用配置项
除了基础配置,装饰器还支持以下高级选项:
@Component({
selector: 'app-advanced',
template: '<ng-content></ng-content>',
// 变更检测策略
changeDetection: ChangeDetectionStrategy.OnPush,
// 视图封装模式
encapsulation: ViewEncapsulation.Emulated,
// 生命周期钩子顺序(可选定义)
providers: [UserService],
// 动画定义
animations: [fadeInAnimation]
})
export class AdvancedComponent {}
changeDetection: ChangeDetectionStrategy.OnPush 是性能优化的重要手段。当组件的输入属性不变时,Angular 会跳过变更检测,显著减少不必要的计算开销。
二、模板语法基础
Angular 模板采用 HTML 扩展语法,通过双花括号 {{ }} 实现数据绑定,使用指令处理逻辑控制。
2.1 插值绑定
插值是最基本的数据绑定方式,将组件属性值渲染到视图中。
@Component({
selector: 'app-interpolation',
standalone: true,
template: `
<h1>{{ title }}</h1>
<p>用户名称:{{ user.name }}</p>
<p>计算结果:{{ 2 + 3 }}</p>
<p>方法返回值:{{ getMessage() }}</p>
`
})
export class InterpolationComponent {
title = '插值演示';
user = { name: '李四', age: 25 };
getMessage() {
return '欢迎学习 Angular';
}
}
插值表达式内部可以包含属性访问、方法调用以及简单的数学运算。但需要注意的是,表达式中不应包含赋值语句、链式调用(如 obj?.prop)以及 new 关键字创建对象等复杂操作。
2.2 属性绑定
属性绑定用于将组件属性值设置为 HTML 元素的属性。
@Component({
selector: 'app-property-binding',
standalone: true,
template: `
<img [src]="imageUrl" [alt]="imageDescription">
<button [disabled]="isDisabled">点击我</button>
<div [class.active]="isActive">状态区域</div>
`
})
export class PropertyBindingComponent {
imageUrl = 'assets/demo.jpg';
imageDescription = '示例图片';
isDisabled = false;
isActive = true;
}
方括号 [property] 的语法告诉 Angular 将等号右侧的表达式求值后,赋值给目标属性。这种绑定方式适用于任何 HTML 属性,包括自定义组件的输入属性。
2.3 事件绑定
事件绑定用于响应用户操作或系统事件。
@Component({
selector: 'app-event-binding',
standalone: true,
template: `
<button (click)="onSave($event)">保存</button>
<input (input)="onInput($event)" (blur)="onBlur()">
<div (mouseenter)="onHover()" (mouseleave)="onLeave()">
鼠标悬停区域
</div>
`
})
export class EventBindingComponent {
onSave(event: Event) {
console.log('保存操作', event);
// 执行保存逻辑
}
onInput(event: Event) {
const input = event.target as HTMLInputElement;
console.log('输入内容:', input.value);
}
onBlur() {
console.log('失去焦点');
}
onHover() {
console.log('鼠标进入');
}
onLeave() {
console.log('鼠标离开');
}
圆括号 (event) 的语法用于绑定事件处理器。`$event` 是特殊变量,代表事件对象本身。不同类型的事件会传递不同结构的事件对象,如鼠标事件传递 `MouseEvent`,输入事件传递 `InputEvent` 或 `Event`(取决于浏览器实现)。
---
## 三、结构型指令
结构型指令负责添加、移除或操作 DOM 元素,从而改变页面布局。Angular 提供了三个最常用的结构型指令:`*ngIf`、`*ngFor` 和 `*ngSwitch`。
### 3.1 条件渲染:*ngIf
`*ngIf` 根据条件决定元素是否存在于 DOM 中。
```typescript
@Component({
selector: 'app-condition',
standalone: true,
imports: [CommonModule],
template: `
<div *ngIf="isVisible">内容可见时显示</div>
<div *ngIf="count > 0">计数大于 0 时显示</div>
<!-- else 分支 -->
<div *ngIf="hasData; else noData">
有数据时的显示内容
</div>
<ng-template #noData>
<p>暂无数据</p>
</ng-template>
`
})
export class ConditionComponent {
isVisible = true;
count = 5;
hasData = false;
}
```
`*ngIf` 与 CSS 的 `display: none` 有本质区别。当条件为 `false` 时,元素会从 DOM 中完全移除,而非仅仅隐藏。这意味着相关的变更检测和事件监听也会一并清除,有助于提升应用性能。
### 3.2 列表渲染:*ngFor
`*ngFor` 用于遍历集合并渲染多个元素。
```typescript
@Component({
selector: 'app-list',
standalone: true,
imports: [CommonModule],
template: `
<ul>
<li *ngFor="let item of items">{{ item }}</li>
</ul>
<table>
<tr *ngFor="let user of users; let i = index; trackBy: trackById">
<td>{{ i + 1 }}</td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
</tr>
</table>
`
})
export class ListComponent {
items = ['苹果', '香蕉', '橙子'];
users = [
{ id: 1, name: '王五', email: 'wang@example.com' },
{ id: 2, name: '赵六', email: 'zhao@example.com' }
];
trackById(index: number, user: { id: number }): number {
return user.id;
}
}
```
`trackBy` 函数是性能优化的关键。当列表数据更新时,Angular 会根据 `trackBy` 返回的值来识别哪些元素可以复用,避免完全重建整个列表。对于包含大量数据的列表,这一优化能显著提升渲染性能。
### 3.3 多条件选择:*ngSwitch
当需要根据多个条件之一显示不同内容时,`*ngSwitch` 是合适的选择。
```typescript
@Component({
selector: 'app-switch',
standalone: true,
imports: [CommonModule],
template: `
<div [ngSwitch]="status">
<p *ngSwitchCase="'loading'">加载中...</p>
<p *ngSwitchCase="'success'">操作成功</p>
<p *ngSwitchCase="'error'">发生错误</p>
<p *ngSwitchDefault>未知状态</p>
</div>
`
})
export class SwitchComponent {
status = 'loading'; // 可改为 'success' 或 'error'
}
```
`[ngSwitch]` 绑定条件值,`*ngSwitchCase` 匹配具体条件,`*ngSwitchDefault` 处理默认情况。与 `if-else` 链相比,`ngSwitch` 的结构更清晰,适合处理三到五个条件的场景。
---
## 四、双向绑定与表单
Angular 提供了 `[(ngModel)]` 语法实现双向绑定,常用于表单场景。
### 4.1 双向绑定基础
```typescript
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-two-way',
standalone: true,
imports: [FormsModule],
template: `
<input [(ngModel)]="username" placeholder="请输入用户名">
<p>当前输入:{{ username }}</p>
<label>
同意协议:
<input type="checkbox" [(ngModel)]="agreed">
</label>
<p>协议状态:{{ agreed ? '已同意' : '未同意' }}</p>
`
})
export class TwoWayComponent {
username = '';
agreed = false;
}
```
`[(ngModel)]` 实际上是两个绑定的简写形式:属性绑定 `[ngModel]` 和事件绑定 `(ngModelChange)`。方括号表示数据流向组件,圆括号表示事件流回组件,两者组合实现了双向同步。
### 4.2 表单处理综合示例
```typescript
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
interface User {
name: string;
email: string;
role: string;
active: boolean;
}
@Component({
selector: 'app-form',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<form (ngSubmit)="onSubmit()">
<div>
<label>姓名:</label>
<input
type="text"
name="name"
[(ngModel)]="user.name"
required
minlength="2">
</div>
<div>
<label>邮箱:</label>
<input
type="email"
name="email"
[(ngModel)]="user.email"
required>
</div>
<div>
<label>角色:</label>
<select name="role" [(ngModel)]="user.role">
<option value="admin">管理员</option>
<option value="editor">编辑</option>
<option value="viewer">访客</option>
</select>
</div>
<div>
<label>
<input
type="checkbox"
name="active"
[(ngModel)]="user.active">
启用账户
</label>
</div>
<button type="submit" [disabled]="!isValid()">提交</button>
</form>
<div *ngIf="submitted">
<h3>提交的数据:</h3>
<pre>{{ user | json }}</pre>
</div>
`
})
export class FormComponent {
user: User = {
name: '',
email: '',
role: 'viewer',
active: false
};
submitted = false;
isValid(): boolean {
return this.user.name.length >= 2 &&
this.user.email.includes('@');
}
onSubmit() {
this.submitted = true;
console.log('表单提交', this.user);
}
}
```
使用 `FormsModule` 后,Angular 会自动为表单元素添加变更追踪和验证功能。`| json` 管道用于将对象以 JSON 字符串形式输出显示。
---
## 五、模板中的属性与变量
Angular 模板支持多种方式引用变量和上下文信息。
### 5.1 模板引用变量
使用 `#` 符号可以在模板中创建引用变量,快速访问 DOM 元素或组件实例。
```typescript
@Component({
selector: 'app-template-ref',
standalone: true,
template: `
<input #emailInput type="email" placeholder="输入邮箱">
<button (click)="checkEmail(emailInput.value)">检查</button>
<p>输入长度:{{ emailInput.value.length }}</p>
`
})
export class TemplateRefComponent {
checkEmail(email: string) {
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
console.log('邮箱格式正确:', pattern.test(email));
}
}
模板引用变量在表单验证、与其他框架集成以及需要直接操作 DOM 的场景下非常有用。
### 5.2 输入属性别名
组件可以通过 `@Input()` 装饰器接收父组件传入的数据,并通过装饰器参数指定别名。
```typescript
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-child',
standalone: true,
template: `
<p>原始名称:{{ userName }}</p>
<p>别名传递:{{ customName }}</p>
`
})
export class ChildComponent {
@Input() userName = '';
@Input('data-title') customName = '';
}
@Component({
selector: 'app-parent',
standalone: true,
imports: [ChildComponent],
template: `
<app-child
[userName]="'直接传入'"
[data-title]="'别名传入'">
</app-child>
`
})
export class ParentComponent {}
这种机制在需要统一组件接口或兼容不同调用方式时非常实用。
六、实战:构建完整组件
以下示例综合运用本文讲解的各项技术,构建一个完整的待办事项列表组件。
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
interface Todo {
id: number;
text: string;
completed: boolean;
}
@Component({
selector: 'app-todo-list',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div class="todo-container">
<h2>待办事项列表</h2>
<div class="input-group">
<input
[(ngModel)]="newTodo"
(keyup.enter)="addTodo()"
placeholder="添加新任务">
<button (click)="addTodo()" [disabled]="!newTodo.trim()">
添加
</button>
</div>
<ul class="todo-list">
<li *ngFor="let todo of todos; let i = index; trackBy: trackById">
<label [class.completed]="todo.completed">
<input
type="checkbox"
[(ngModel)]="todo.completed"
(change)="toggleTodo(todo)">
<span>{{ i + 1 }}. {{ todo.text }}</span>
</label>
<button
class="delete-btn"
(click)="removeTodo(todo.id)">
删除
</button>
</li>
</ul>
<div class="stats" *ngIf="todos.length > 0">
<span>总计:{{ todos.length }}</span>
<span>已完成:{{ completedCount }}</span>
<span>未完成:{{ uncompletedCount }}</span>
</div>
<p *ngIf="todos.length === 0" class="empty-tip">
暂无待办事项
</p>
</div>
`,
styles: [`
.todo-container { max-width: 400px; margin: 20px auto; }
.input-group { display: flex; gap: 8px; margin-bottom: 16px; }
.input-group input { flex: 1; padding: 8px; }
.todo-list { list-style: none; padding: 0; }
.todo-list li {
display: flex;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #eee;
}
.todo-list label {
flex: 1;
display: flex;
align-items: center;
cursor: pointer;
}
.todo-list span { margin-left: 8px; }
.completed span {
text-decoration: line-through;
color: #999;
}
.delete-btn {
background: #ff4444;
color: white;
border: none;
padding: 4px 8px;
cursor: pointer;
border-radius: 4px;
}
.stats {
margin-top: 16px;
display: flex;
gap: 16px;
color: #666;
}
.empty-tip { color: #999; text-align: center; }
`]
})
export class TodoListComponent {
todos: Todo[] = [
{ id: 1, text: '学习 Angular 组件', completed: true },
{ id: 2, text: '实践模板语法', completed: false },
{ id: 3, text: '构建待办应用', completed: false }
];
newTodo = '';
nextId = 4;
get completedCount(): number {
return this.todos.filter(t => t.completed).length;
}
get uncompletedCount(): number {
return this.todos.filter(t => !t.completed).length;
}
addTodo() {
if (this.newTodo.trim()) {
this.todos.push({
id: this.nextId++,
text: this.newTodo.trim(),
completed: false
});
this.newTodo = '';
}
}
removeTodo(id: number) {
this.todos = this.todos.filter(t => t.id !== id);
}
toggleTodo(todo: Todo) {
// 状态切换由 ngModel 自动处理
}
trackById(index: number, todo: Todo): number {
return todo.id;
}
}
这个示例涵盖了组件装饰器配置、模板语法插值、事件绑定、双向绑定、结构型指令、计算属性以及样式隔离等核心概念。通过实际练习这些技术,你将能够构建出功能完整的 Angular 组件。
七、常见问题与注意事项
开发过程中,以下几点值得特别注意:
变更检测与性能:使用 OnPush 策略时,确保组件的输入属性通过不可变对象或原始值传递,否则变更检测可能无法正确触发。
样式隔离:默认情况下,组件样式不会泄露到外部,也不会受外部样式影响。如需穿透封装,可使用 ::ng-deep 选择器(谨慎使用)。
空值处理:使用安全导航操作符 ?. 避免在模板中访问 null 或 undefined 属性导致错误,例如 {{ user?.name }}。
模板表达式限制:模板表达式不应包含副作用过大的操作(如修改全局变量、调用非纯函数),这可能导致不可预期的变更检测行为。

暂无评论,快来抢沙发吧!