## 2.5指令 (Directives) ## 2.5 指令 (Directives) 在 Angular 的核心概念中,指令 (Directives) 是至关重要的组成部分,它们赋予了 HTML 强大的动态性和可扩展性。指令允许您扩展 HTML 的功能,操作 DOM (文档对象模型),并构建可复用的 UI 组件。理解指令是深入掌握 Angular 框架的关键一步。 ### 2.5.1 指令的概念与作用 指令本质上是赋予 DOM 元素行为的类。它们指示 Angular 的编译器在 DOM 渲染过程中如何转换或增强 DOM 元素。可以将指令视为 HTML 的“指令”,告诉浏览器如何处理特定的 HTML 结构或元素。 指令的主要作用包括: * **DOM 操作**: 指令可以直接访问和修改 DOM 元素,动态地更改元素的属性、样式、内容甚至结构。 * **行为添加**: 指令可以为 DOM 元素添加交互行为,例如响应用户事件、处理数据绑定、实现复杂的 UI 逻辑。 * **组件构建**: 组件 (Components) 本身也是一种特殊的指令,用于构建可复用的 UI 模块。 * **代码复用**: 通过创建自定义指令,可以将通用的 DOM 操作和行为逻辑封装起来,在多个组件和模板中复用,提高开发效率和代码可维护性。 总而言之,指令是 Angular 框架实现声明式 UI 和组件化开发的核心机制之一。它们使得开发者能够以更简洁、高效的方式构建动态 Web 应用。 ### 2.5.2 指令的类型 Angular 中主要有三种类型的指令: 1. **组件指令 (Component Directives)**: 组件是指令中最常见的类型,实际上,组件可以看作是带有模板的指令。它们拥有自己的模板 (HTML)、样式 (CSS) 和逻辑 (TypeScript 代码),用于创建可复用的 UI 元素。我们通常所说的“组件”就是指组件指令。 2. **结构型指令 (Structural Directives)**: 结构型指令负责**改变 DOM 的结构**。它们可以添加、移除或替换 DOM 元素。结构型指令通常以星号 `*` 开头,例如 `*ngIf`、`*ngFor`、`*ngSwitch` 等。 3. **属性型指令 (Attribute Directives)**: 属性型指令负责**改变元素的外观或行为**。它们以属性的形式添加到元素上,例如 `ngModel`、`ngStyle`、`ngClass` 以及自定义的属性指令。 可以用 Mermaid 图表来更清晰地展示指令的类型和关系: ```mermaid graph TD A[指令 (Directives)] --> B{类型}; B --> C[组件指令 (Component Directives)]; B --> D[结构型指令 (Structural Directives)]; B --> E[属性型指令 (Attribute Directives)]; C --> F[创建可复用 UI 组件]; D --> G[DOM 结构操作 (添加/移除/替换)]; E --> H[元素外观/行为修改]; F --> I[例如: 各种组件]; G --> J[例如: *ngIf, *ngFor, *ngSwitch]; H --> K[例如: ngStyle, ngClass, 各种自定义属性指令]; ``` ### 2.5.3 结构型指令详解与实践 结构型指令是 Angular 中非常强大且常用的指令类型,它们通过操作 DOM 结构来实现动态视图的渲染。 #### 2.5.3.1 `*ngIf` 指令:条件渲染 `*ngIf` 指令根据条件表达式的结果,决定是否将元素及其子元素添加到 DOM 中。如果条件为真 (truthy),则元素会被添加到 DOM;如果条件为假 (falsy),则元素会被从 DOM 中移除。 **代码实践:** 假设我们有一个布尔类型的变量 `isLoggedIn`,我们希望根据用户是否登录来显示不同的内容。 **组件代码 (component.ts):** ```typescript import { Component } from '@angular/core'; @Component({ selector: 'app-ng-if-example', templateUrl: './ng-if-example.component.html', styleUrls: ['./ng-if-example.component.css'] }) export class NgIfExampleComponent { isLoggedIn = false; // 初始状态为未登录 toggleLoginStatus() { this.isLoggedIn = !this.isLoggedIn; } } ``` **模板代码 (ng-if-example.component.html):** ```html 切换登录状态
``` **运行效果:** 初始状态下,页面显示 "请先登录。"。点击 "切换登录状态" 按钮后,`isLoggedIn` 变为 `true`,页面显示 "欢迎回来,用户!"。再次点击按钮,状态切换回未登录,显示 "请先登录。"。 **`*ngIf` 的工作原理 (简化版):** `*ngIf` 指令实际上是一个语法糖。Angular 在编译时会将 `*ngIf` 转换为 `` 元素。例如: ```html
``` 会被编译成类似下面的结构: ```html
``` 当 `isLoggedIn` 为真时,`ngIf` 指令会实例化 `` 中的模板内容,并将其添加到 DOM 中。当 `isLoggedIn` 为假时,模板内容会被销毁并从 DOM 中移除。 可以用 Mermaid 图表来表示 `*ngIf` 的工作流程: ```mermaid graph TD A[模板解析器遇到 *ngIf] --> B[评估条件表达式 (isLoggedIn)]; B -- 条件为真 (true) --> C[实例化 内容]; B -- 条件为假 (false) --> D[销毁 内容]; C --> E[将实例化内容添加到 DOM]; D --> F[从 DOM 中移除内容]; E --> G[视图更新]; F --> G; ``` **注意事项:** * `*ngIf` 是**移除** DOM 元素,而不是隐藏。这意味着当条件为假时,元素及其子元素不会存在于 DOM 中,相关的事件监听器和数据绑定也会被销毁。这与使用 CSS 的 `display: none` 隐藏元素不同,后者只是视觉上隐藏元素,但元素仍然存在于 DOM 中。 * 频繁使用 `*ngIf` 进行 DOM 结构的动态切换可能会带来一定的性能开销,尤其是在元素及其子元素结构复杂的情况下。需要根据实际场景权衡使用。 #### 2.5.3.2 `*ngFor` 指令:列表渲染 `*ngFor` 指令用于循环遍历集合 (例如数组),并为集合中的每个元素渲染模板。它常用于展示列表数据。 **代码实践:** 假设我们有一个包含商品信息的数组 `products`,我们希望在页面上展示商品列表。 **组件代码 (component.ts):** ```typescript import { Component } from '@angular/core'; @Component({ selector: 'app-ng-for-example', templateUrl: './ng-for-example.component.html', styleUrls: ['./ng-for-example.component.css'] }) export class NgForExampleComponent { products = [ { id: 1, name: '商品 A', price: 10 }, { id: 2, name: '商品 B', price: 20 }, { id: 3, name: '商品 C', price: 30 } ]; } ``` **模板代码 (ng-for-example.component.html):** ```html
- {{ i + 1 }}. {{ product.name }} - 价格: {{ product.price }}
``` **运行效果:** 页面会渲染出一个商品列表,每个商品信息显示在 `
` 元素中,并带有序号和价格。`even` 和 `odd` 类会根据索引的奇偶性动态添加到 `
` 元素上。 **`*ngFor` 的语法详解:** * `let product of products`: `product` 是循环变量,代表数组 `products` 中的当前元素。 `of` 关键字表示遍历的对象。 * `let i = index`: `index` 是 Angular 提供的特殊变量,表示当前元素的索引 (从 0 开始)。 `i` 是自定义的索引变量名。 * `let isEven = even`: `even` 是 Angular 提供的特殊变量,表示当前索引是否为偶数 (布尔值)。 `isEven` 是自定义的偶数判断变量名。 * `let isOdd = odd`: `odd` 是 Angular 提供的特殊变量,表示当前索引是否为奇数 (布尔值)。 `isOdd` 是自定义的奇数判断变量名。 * `trackBy: trackByProductId`: `trackBy` 是一个性能优化的选项,用于指定一个跟踪函数 `trackByProductId`。当数组数据发生变化时,Angular 会根据 `trackBy` 函数返回的值来判断元素是否发生变化,从而避免不必要的 DOM 更新。 **`trackBy` 优化:** `trackBy` 函数接收两个参数:`index` (索引) 和 `product` (当前元素),并返回一个唯一标识符。通常使用元素的唯一 ID 作为标识符。 **组件代码 (component.ts) - 添加 `trackByProductId` 函数:** ```typescript import { Component } from '@angular/core'; @Component({ selector: 'app-ng-for-example', templateUrl: './ng-for-example.component.html', styleUrls: ['./ng-for-example.component.css'] }) export class NgForExampleComponent { products = [ { id: 1, name: '商品 A', price: 10 }, { id: 2, name: '商品 B', price: 20 }, { id: 3, name: '商品 C', price: 30 } ]; trackByProductId(index: number, product: any): number { return product.id; // 使用 product.id 作为唯一标识符 } } ``` **`*ngFor` 的工作原理 (简化版):** `*ngFor` 指令会遍历数组,为数组中的每个元素创建一个模板实例。当数组数据发生变化时,Angular 会根据元素的标识符 (通过 `trackBy` 或默认的引用比较) 来判断哪些元素发生了变化,并只更新发生变化的 DOM 元素,从而提高性能。 可以用 Mermaid 图表来表示 `*ngFor` 的工作流程: ```mermaid graph TD A[模板解析器遇到 *ngFor] --> B[遍历数组 (products)]; B --> C{每个元素}; C --> D[创建 实例]; D --> E[绑定元素数据到模板]; E --> F[将模板实例添加到 DOM]; F --> C; C -- 遍历完成 --> G[视图更新]; ``` **注意事项:** * `*ngFor` 适用于渲染列表数据,尤其是在数据量较大时,`trackBy` 优化可以显著提升性能。 * 避免在 `*ngFor` 循环体内进行复杂的计算或 DOM 操作,这会影响渲染性能。 * 当需要对列表进行过滤、排序等操作时,应在组件的 TypeScript 代码中处理数据,而不是在模板中直接操作,以保持模板的简洁和可维护性。 #### 2.5.3.3 `*ngSwitch` 指令:条件分支渲染 `*ngSwitch` 指令用于根据不同的条件值,渲染不同的模板片段,类似于 JavaScript 中的 `switch` 语句。 **代码实践:** 假设我们有一个变量 `status`,表示不同的状态值,我们希望根据不同的状态值显示不同的消息。 **组件代码 (component.ts):** ```typescript import { Component } from '@angular/core'; @Component({ selector: 'app-ng-switch-example', templateUrl: './ng-switch-example.component.html', styleUrls: ['./ng-switch-example.component.css'] }) export class NgSwitchExampleComponent { status = 'loading'; // 初始状态为 loading setStatus(newStatus: string) { this.status = newStatus; } } ``` **模板代码 (ng-switch-example.component.html):** ```html 设置为 Loading 设置为 Success 设置为 Error
``` **运行效果:** 初始状态下,页面显示 "加载中..."。点击不同的按钮,`status` 值会改变,页面会显示相应的消息。当 `status` 值不匹配任何 `*ngSwitchCase` 时,会显示 `*ngSwitchDefault` 中的内容。 **`*ngSwitch` 的语法详解:** * `[ngSwitch]="status"`: `[ngSwitch]` 是宿主指令,它接收一个表达式 `status`,用于判断条件值。 * `*ngSwitchCase="'loading'"`: `*ngSwitchCase` 是子指令,用于匹配特定的条件值。当 `status` 的值与 `'loading'` 相等时,该 `*ngSwitchCase` 对应的模板会被渲染。 * `*ngSwitchDefault`: `*ngSwitchDefault` 是默认分支,当 `status` 的值不匹配任何 `*ngSwitchCase` 时,该指令对应的模板会被渲染。 **`*ngSwitch` 的工作原理 (简化版):** `*ngSwitch` 指令会根据 `[ngSwitch]` 表达式的值,匹配相应的 `*ngSwitchCase` 或 `*ngSwitchDefault`,并只渲染匹配到的模板片段。 可以用 Mermaid 图表来表示 `*ngSwitch` 的工作流程: ```mermaid graph TD A[模板解析器遇到 *ngSwitch] --> B[获取 [ngSwitch] 表达式的值 (status)]; B --> C{匹配 *ngSwitchCase?}; C -- 是 --> D[渲染匹配的 *ngSwitchCase 模板]; C -- 否 --> E{存在 *ngSwitchDefault?}; E -- 是 --> F[渲染 *ngSwitchDefault 模板]; E -- 否 --> G[不渲染任何模板]; D --> H[视图更新]; F --> H; G --> H; ``` **注意事项:** * `*ngSwitch` 适用于多条件分支渲染的场景,可以使模板结构更清晰,逻辑更易于理解。 * `*ngSwitch` 只能用于**值相等**的判断,不能用于范围判断或其他复杂条件判断。 * 避免在 `*ngSwitchCase` 或 `*ngSwitchDefault` 中编写过于复杂的模板逻辑,保持模板的简洁性。 ### 2.5.4 属性型指令详解与实践 属性型指令用于改变元素的外观或行为,它们通过修改元素的属性、样式或监听事件来实现。 #### 2.5.4.1 内置属性型指令:`NgStyle` 和 `NgClass` Angular 提供了两个常用的内置属性型指令:`NgStyle` 和 `NgClass`,用于动态地绑定元素的样式和 CSS 类。 **`NgStyle` 指令:动态样式绑定** `NgStyle` 指令允许您动态地设置元素的内联样式。它接收一个 JavaScript 对象,对象的键是 CSS 属性名,值是属性值。 **代码实践:** 假设我们有一个变量 `textColor`,我们希望动态地改变文本颜色。 **组件代码 (component.ts):** ```typescript import { Component } from '@angular/core'; @Component({ selector: 'app-ng-style-example', templateUrl: './ng-style-example.component.html', styleUrls: ['./ng-style-example.component.css'] }) export class NgStyleExampleComponent { textColor = 'blue'; // 初始颜色为蓝色 changeTextColor(color: string) { this.textColor = color; } } ``` **模板代码 (ng-style-example.component.html):** ```html 红色 绿色 蓝色
这段文字的颜色会动态改变。
``` **运行效果:** 初始状态下,文本颜色为蓝色,且加粗。点击不同的颜色按钮,文本颜色会动态切换。 **`NgClass` 指令:动态 CSS 类绑定** `NgClass` 指令允许您动态地添加或移除元素的 CSS 类。它可以接收多种类型的输入,例如字符串、数组或对象。 **代码实践:** 假设我们希望根据一个布尔变量 `isActive` 来动态地添加或移除 `active` CSS 类。 **组件代码 (component.ts):** ```typescript import { Component } from '@angular/core'; @Component({ selector: 'app-ng-class-example', templateUrl: './ng-class-example.component.html', styleUrls: ['./ng-class-example.component.css'] }) export class NgClassExampleComponent { isActive = false; // 初始状态为非激活 toggleActive() { this.isActive = !this.isActive; } } ``` **模板代码 (ng-class-example.component.html):** ```html 切换激活状态
这个 div 的 active 类会动态切换。
``` **CSS 代码 (ng-class-example.component.css):** ```css .active { background-color: yellow; padding: 10px; } ``` **运行效果:** 初始状态下,div 没有 `active` 类,背景颜色为默认色。点击 "切换激活状态" 按钮后,`isActive` 变为 `true`,div 会添加 `active` 类,背景颜色变为黄色,并有内边距。再次点击按钮,状态切换回非激活,`active` 类被移除,背景颜色恢复默认。 #### 2.5.4.2 自定义属性型指令:创建和使用 除了内置的属性型指令,我们还可以创建自定义的属性型指令,以封装特定的 DOM 操作或行为逻辑。 **创建自定义属性型指令的步骤:** 1. **使用 Angular CLI 生成指令:** ```bash ng generate directive highlight ``` 这会生成一个名为 `highlight.directive.ts` 的指令文件和一个测试文件。 2. **修改指令类:** 打开 `highlight.directive.ts` 文件,修改指令类,实现所需的功能。 ```typescript import { Directive, ElementRef, HostListener, Input } from '@angular/core'; @Directive({ selector: '[appHighlight]' // 使用属性选择器 [appHighlight] }) export class HighlightDirective { @Input('appHighlight') highlightColor: string; // 输入属性,用于接收高亮颜色 constructor(private el: ElementRef) { // ElementRef 用于访问指令宿主元素 } @HostListener('mouseenter') onMouseEnter() { this.highlight(this.highlightColor || 'yellow'); // 鼠标移入时高亮,默认黄色 } @HostListener('mouseleave') onMouseLeave() { this.highlight(null); // 鼠标移出时移除高亮 } private highlight(color: string) { this.el.nativeElement.style.backgroundColor = color; // 直接操作 DOM 元素样式 } } ``` **代码解释:** * `@Directive({ selector: '[appHighlight]' })`: `@Directive` 装饰器标记这是一个指令类,`selector: '[appHighlight]'` 定义了指令的选择器,使用属性选择器 `[appHighlight]`,表示该指令可以像属性一样添加到 HTML 元素上,例如 `
`。 * `export class HighlightDirective`: 定义指令类 `HighlightDirective`。 * `constructor(private el: ElementRef)`: 构造函数,注入 `ElementRef` 服务。`ElementRef` 封装了指令宿主元素的原生 DOM 元素,通过 `el.nativeElement` 可以访问到原生 DOM 元素。**注意:直接操作原生 DOM 元素应谨慎,在 Angular 中推荐使用 `Renderer2` 服务进行 DOM 操作,以提高跨平台兼容性和安全性。** 这里为了简化示例,直接使用了 `ElementRef`。 * `@Input('appHighlight') highlightColor: string;`: `@Input` 装饰器定义了一个输入属性 `highlightColor`,并使用别名 `'appHighlight'`。这意味着在模板中使用 `appHighlight` 属性时,可以将数据绑定到指令的 `highlightColor` 属性上。 * `@HostListener('mouseenter') onMouseEnter() { ... }`: `@HostListener` 装饰器用于监听宿主元素的 DOM 事件。`'mouseenter'` 表示监听 `mouseenter` 事件,当鼠标移入宿主元素时,`onMouseEnter` 方法会被调用。 * `@HostListener('mouseleave') onMouseLeave() { ... }`: 监听 `mouseleave` 事件,当鼠标移出宿主元素时,`onMouseLeave` 方法会被调用。 * `private highlight(color: string) { ... }`: `highlight` 方法用于设置宿主元素的背景颜色。`this.el.nativeElement.style.backgroundColor = color;` 直接修改原生 DOM 元素的 `style.backgroundColor` 属性。 3. **在模块中声明指令:** 打开指令所属的模块文件 (例如 `app.module.ts`),在 `declarations` 数组中声明 `HighlightDirective`。 ```typescript import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; import { HighlightDirective } from './highlight.directive'; // 导入指令 @NgModule({ declarations: [ AppComponent, HighlightDirective // 声明指令 ], imports: [ BrowserModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } ``` 4. **在模板中使用指令:** 在组件的模板中,将 `appHighlight` 属性添加到 HTML 元素上,并可以可选地绑定 `highlightColor` 输入属性。 ```html
鼠标移入这段文字会高亮 (默认黄色)。
鼠标移入这段文字会高亮浅蓝色。
``` **运行效果:** 当鼠标移入第一个 `
` 元素时,背景颜色会变为黄色。当鼠标移入第二个 `
` 元素时,背景颜色会变为浅蓝色。鼠标移出时,背景颜色会恢复默认。 可以用 Mermaid 图表来表示自定义属性型指令的工作流程: ```mermaid graph TD A[模板解析器遇到 [appHighlight]] --> B[创建 HighlightDirective 实例]; B --> C[指令绑定到宿主元素]; C --> D[监听宿主元素事件 (mouseenter, mouseleave)]; D -- mouseenter 触发 --> E[执行 onMouseEnter()]; D -- mouseleave 触发 --> F[执行 onMouseLeave()]; E --> G[修改宿主元素样式 (高亮)]; F --> H[修改宿主元素样式 (移除高亮)]; G --> I[视图更新]; H --> I; ``` **更推荐的 DOM 操作方式:使用 `Renderer2`** 为了提高跨平台兼容性和安全性,Angular 推荐使用 `Renderer2` 服务进行 DOM 操作,而不是直接使用 `ElementRef.nativeElement`。 **修改 `HighlightDirective` 使用 `Renderer2`:** ```typescript import { Directive, ElementRef, HostListener, Input, Renderer2 } from '@angular/core'; @Directive({ selector: '[appHighlight]' }) export class HighlightDirective { @Input('appHighlight') highlightColor: string; constructor(private el: ElementRef, private renderer: Renderer2) { // 注入 Renderer2 服务 } @HostListener('mouseenter') onMouseEnter() { this.highlight(this.highlightColor || 'yellow'); } @HostListener('mouseleave') onMouseLeave() { this.highlight(null); } private highlight(color: string) { if (color) { this.renderer.setStyle(this.el.nativeElement, 'backgroundColor', color); // 使用 Renderer2 设置样式 } else { this.renderer.removeStyle(this.el.nativeElement, 'backgroundColor'); // 使用 Renderer2 移除样式 } } } ``` **代码解释:** * `constructor(private el: ElementRef, private renderer: Renderer2)`: 构造函数中注入 `Renderer2` 服务。 * `this.renderer.setStyle(this.el.nativeElement, 'backgroundColor', color);`: 使用 `renderer.setStyle()` 方法设置元素的样式。 * `this.renderer.removeStyle(this.el.nativeElement, 'backgroundColor');`: 使用 `renderer.removeStyle()` 方法移除元素的样式。 使用 `Renderer2` 进行 DOM 操作可以避免直接操作原生 DOM 元素可能带来的潜在问题,并提高代码的可维护性和可测试性。 ### 2.5.5 指令的最佳实践和注意事项 * **职责单一原则**: 指令应专注于完成特定的 DOM 操作或行为添加,避免指令过于庞大和复杂。 * **代码复用性**: 尽量将通用的 DOM 操作和行为逻辑封装成可复用的指令,提高代码复用率。 * **性能优化**: 避免在指令中进行复杂的计算或 DOM 操作,尤其是在结构型指令中,要注意性能影响。可以使用 `trackBy` 优化 `*ngFor` 指令。 * **避免直接操作 DOM**: 尽量使用 `Renderer2` 服务进行 DOM 操作,而不是直接使用 `ElementRef.nativeElement`,以提高跨平台兼容性和安全性。 * **清晰的命名**: 指令的命名应具有描述性,能够清晰地表达指令的功能。 * **适当的注释**: 为指令添加必要的注释,解释指令的作用、用法和注意事项,提高代码可读性和可维护性。 ### 2.5.6 总结 指令是 Angular 框架的核心概念之一,它们为 HTML 提供了强大的动态性和可扩展性。通过结构型指令和属性型指令,我们可以灵活地操作 DOM 结构、改变元素外观和行为,构建动态、交互式的 Web 应用。理解和掌握指令是成为 Angular 开发专家的关键一步。