MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Angular自定义指令开发指南

2022-09-305.9k 阅读

一、Angular 指令概述

在 Angular 中,指令是一种扩展 HTML 的方式,它允许你为 DOM 元素添加额外的行为或改变其外观。Angular 中有三种类型的指令:组件(Component)、结构型指令(Structural Directive)和属性型指令(Attribute Directive)。组件本质上也是一种指令,它有自己的模板和样式,并且是视图的一部分。结构型指令会改变 DOM 的结构,比如 *ngIf*ngFor。属性型指令则用于改变元素的外观或行为,像 ngModel

二、自定义指令基础

  1. 创建自定义指令 要创建一个自定义指令,首先使用 Angular CLI 生成指令骨架。在项目目录下运行以下命令:
ng generate directive <directive - name>

例如,创建一个名为 highlight 的指令:

ng generate directive highlight

这会在 src/app 目录下生成一个 highlight.directive.ts 文件,内容如下:

import { Directive } from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {
  constructor() { }
}
  1. 理解 @Directive 装饰器 @Directive 装饰器用于定义一个指令。其中,selector 属性指定了指令在 HTML 中如何使用。上述例子中,[appHighlight] 表示该指令作为属性应用在 HTML 元素上,比如 <div appHighlight></div>

三、属性型指令开发

  1. 简单样式修改指令 假设要创建一个指令,当鼠标悬停在元素上时,改变元素的背景颜色。 在 highlight.directive.ts 文件中编写如下代码:
import { Directive, HostListener, ElementRef } from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {
  constructor(private el: ElementRef) { }

  @HostListener('mouseenter') onMouseEnter() {
    this.highlight('yellow');
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(null);
  }

  private highlight(color: string) {
    this.el.nativeElement.style.backgroundColor = color;
  }
}

在上述代码中:

  • 通过构造函数注入 ElementRef,它提供了对宿主 DOM 元素的引用。
  • 使用 @HostListener 装饰器来监听宿主元素的事件。@HostListener('mouseenter') 监听鼠标进入事件,@HostListener('mouseleave') 监听鼠标离开事件。
  • highlight 方法用于设置元素的背景颜色。

在 HTML 中使用该指令:

<div appHighlight>鼠标悬停我,背景变色</div>
  1. 带输入参数的属性型指令 有时需要指令根据传入的参数进行不同的行为。比如创建一个指令,根据传入的颜色参数改变元素的文本颜色。 修改 highlight.directive.ts
import { Directive, Input, ElementRef } from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {
  @Input() highlightColor: string;

  constructor(private el: ElementRef) { }

  ngOnInit() {
    this.highlight(this.highlightColor || 'red');
  }

  private highlight(color: string) {
    this.el.nativeElement.style.color = color;
  }
}

在上述代码中:

  • 使用 @Input() 装饰器定义了一个输入属性 highlightColor
  • ngOnInit 生命周期钩子中,调用 highlight 方法,并使用传入的颜色参数,如果没有传入则使用默认值 red

在 HTML 中使用:

<div appHighlight [highlightColor]="'blue'">我的文本颜色会变成蓝色</div>

四、结构型指令开发

  1. 简单的结构型指令原理 结构型指令主要用于改变 DOM 的结构。以 *ngIf 为例,它根据表达式的值决定是否在 DOM 中添加或移除元素。 要创建一个类似的简单结构型指令,比如 *appUnless,它的功能是当表达式为 false 时显示元素,为 true 时移除元素。 首先创建指令:
ng generate directive unless

unless.directive.ts 文件中编写:

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appUnless]'
})
export class UnlessDirective {
  private hasView = false;

  @Input() set appUnless(condition: boolean) {
    if (!condition &&!this.hasView) {
      this.viewContainer.createEmbeddedView(this.templateRef);
      this.hasView = true;
    } else if (condition && this.hasView) {
      this.viewContainer.clear();
      this.hasView = false;
    }
  }

  constructor(private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef) { }
}

在上述代码中:

  • 通过构造函数注入 TemplateRefViewContainerRefTemplateRef 代表了指令所附加到的模板,ViewContainerRef 用于管理视图的创建和销毁。
  • 使用 @Input() 装饰器定义了 appUnless 属性,它接受一个布尔值。
  • 根据 condition 的值,通过 viewContainercreateEmbeddedView 方法创建视图,或通过 clear 方法移除视图。

在 HTML 中使用:

<ng-template appUnless="isTrue">
  <div>当 isTrue 为 false 时我会显示</div>
</ng-template>
  1. 带上下文的结构型指令 有时结构型指令需要向模板传递数据。比如创建一个 *appFor 指令,类似 *ngFor,可以遍历数组并向模板传递当前项和索引。 创建指令:
ng generate directive for

for.directive.ts 文件中编写:

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appFor]'
})
export class ForDirective {
  constructor(private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef) { }

  @Input() set appFor(iterable: any[]) {
    this.viewContainer.clear();
    iterable.forEach((item, index) => {
      const context = {
        $implicit: item,
        index: index
      };
      this.viewContainer.createEmbeddedView(this.templateRef, context);
    });
  }
}

在上述代码中:

  • @Input() 装饰器定义的 appFor 属性接受一个数组。
  • 遍历数组,为每个项创建视图,并通过 createEmbeddedView 的第二个参数传递上下文对象,其中 $implicit 代表当前项,index 代表当前索引。

在 HTML 中使用:

<ng-template appFor="items">
  <div>索引: {{ index }}, 项: {{ $implicit }}</div>
</ng-template>

五、指令的生命周期钩子

  1. 常用生命周期钩子
  • ngOnInit:在指令被初始化后调用,通常用于执行一次性的初始化逻辑,比如获取输入参数并进行处理。
  • ngOnChanges:当指令的输入属性发生变化时调用。可以通过 SimpleChanges 对象获取变化的信息。
  • ngDoCheck:在 Angular 检测变更时调用,可用于实现自定义的变更检测逻辑。
  • ngOnDestroy:在指令被销毁前调用,常用于清理资源,比如取消订阅事件。
  1. 示例:使用 ngOnChanges 假设创建一个指令,当输入的数字发生变化时,在控制台打印变化信息。
import { Directive, Input, OnChanges, SimpleChanges } from '@angular/core';

@Directive({
  selector: '[appNumberChange]'
})
export class NumberChangeDirective implements OnChanges {
  @Input() number: number;

  ngOnChanges(changes: SimpleChanges) {
    if (changes['number']) {
      console.log('数字从', changes['number'].previousValue, '变为', changes['number'].currentValue);
    }
  }
}

在 HTML 中使用:

<input type="number" [(ngModel)]="inputNumber">
<div appNumberChange [number]="inputNumber"></div>

在上述代码中,ngOnChanges 方法检测到 number 属性变化时,在控制台打印变化前后的值。

六、指令的依赖注入

  1. 依赖注入基础 在指令中,可以通过构造函数进行依赖注入。Angular 会自动创建并提供依赖的实例。例如,前面提到的 ElementRefTemplateRefViewContainerRef 都是通过依赖注入获得的。
  2. 自定义依赖注入 假设创建一个服务 HighlightService,用于管理高亮颜色,然后在指令中注入该服务。 首先创建服务:
ng generate service highlight

highlight.service.ts 文件中编写:

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class HighlightService {
  private highlightColor = 'yellow';

  getColor() {
    return this.highlightColor;
  }
}

highlight.directive.ts 中注入该服务:

import { Directive, ElementRef, HostListener } from '@angular/core';
import { HighlightService } from './highlight.service';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {
  constructor(private el: ElementRef, private highlightService: HighlightService) { }

  @HostListener('mouseenter') onMouseEnter() {
    this.highlight(this.highlightService.getColor());
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(null);
  }

  private highlight(color: string) {
    this.el.nativeElement.style.backgroundColor = color;
  }
}

在上述代码中,通过构造函数注入 HighlightService,并在指令逻辑中使用该服务获取高亮颜色。

七、指令的测试

  1. 测试属性型指令HighlightDirective 为例,测试其鼠标悬停和离开时的样式变化。 在 highlight.directive.spec.ts 文件中编写测试代码:
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { HighlightDirective } from './highlight.directive';
import { Component } from '@angular/core';

@Component({
  template: '<div appHighlight></div>'
})
class TestComponent { }

describe('HighlightDirective', () => {
  let component: TestComponent;
  let fixture: ComponentFixture<TestComponent>;
  let directiveElement: HTMLElement;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [HighlightDirective, TestComponent]
    });
    fixture = TestBed.createComponent(TestComponent);
    component = fixture.componentInstance;
    directiveElement = fixture.nativeElement.querySelector('[appHighlight]');
    fixture.detectChanges();
  });

  it('should highlight on mouseenter', () => {
    const mouseEnterEvent = new MouseEvent('mouseenter');
    Object.defineProperty(mouseEnterEvent, 'target', { value: directiveElement });
    directiveElement.dispatchEvent(mouseEnterEvent);
    expect(directiveElement.style.backgroundColor).toBe('yellow');
  });

  it('should un - highlight on mouseleave', () => {
    const mouseEnterEvent = new MouseEvent('mouseenter');
    Object.defineProperty(mouseEnterEvent, 'target', { value: directiveElement });
    directiveElement.dispatchEvent(mouseEnterEvent);

    const mouseLeaveEvent = new MouseEvent('mouseleave');
    Object.defineProperty(mouseLeaveEvent, 'target', { value: directiveElement });
    directiveElement.dispatchEvent(mouseLeaveEvent);
    expect(directiveElement.style.backgroundColor).toBe('');
  });
});

在上述测试中:

  • 创建了一个测试组件 TestComponent 来包裹要测试的指令。
  • 使用 TestBed 配置测试模块,声明指令和测试组件。
  • beforeEach 中初始化测试环境。
  • 编写两个测试用例,分别测试鼠标进入和离开时的样式变化。
  1. 测试结构型指令UnlessDirective 为例,测试其根据条件显示和隐藏元素的功能。 在 unless.directive.spec.ts 文件中编写:
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { UnlessDirective } from './unless.directive';
import { Component } from '@angular/core';

@Component({
  template: `
    <ng - template appUnless="condition">
      <div>测试元素</div>
    </ng - template>
  `
})
class TestComponent {
  condition = true;
}

describe('UnlessDirective', () => {
  let component: TestComponent;
  let fixture: ComponentFixture<TestComponent>;
  let testElement: HTMLElement;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [UnlessDirective, TestComponent]
    });
    fixture = TestBed.createComponent(TestComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
    testElement = fixture.nativeElement.querySelector('div');
  });

  it('should not display element when condition is true', () => {
    expect(testElement).toBeNull();
  });

  it('should display element when condition is false', () => {
    component.condition = false;
    fixture.detectChanges();
    testElement = fixture.nativeElement.querySelector('div');
    expect(testElement).not.toBeNull();
  });
});

在上述测试中:

  • 创建测试组件 TestComponent 来测试 UnlessDirective
  • beforeEach 中初始化测试环境。
  • 编写两个测试用例,一个测试条件为 true 时元素不显示,另一个测试条件为 false 时元素显示。

八、指令最佳实践

  1. 指令命名规范
  • 指令选择器命名应该遵循 Angular 约定,通常使用中划线命名法,并且前缀使用应用名,如 [app - directive - name]
  • 指令类名应该以 Directive 结尾,如 HighlightDirective
  1. 指令功能单一性 每个指令应该专注于一个特定的功能。比如,不要在一个指令中既处理样式变化,又处理 DOM 结构变化,这样会使指令难以维护和测试。
  2. 合理使用输入和输出属性 如果指令需要从外部接收数据,使用输入属性。如果指令需要向外部传递事件,使用输出属性。确保输入和输出属性的命名清晰,易于理解。
  3. 性能优化
  • 在结构型指令中,尽量减少不必要的视图创建和销毁。例如,可以缓存视图而不是每次都重新创建。
  • 在属性型指令中,避免频繁操作 DOM,尤其是在高频率触发的事件中。可以使用 requestAnimationFrame 等方法优化性能。

通过以上内容,你应该对 Angular 自定义指令的开发有了深入的了解。无论是属性型指令还是结构型指令,都为我们在 Angular 应用中扩展 HTML 行为提供了强大的能力。合理运用指令的生命周期钩子、依赖注入和测试,能使我们开发出健壮、可维护的指令。