Angular自定义指令开发指南
一、Angular 指令概述
在 Angular 中,指令是一种扩展 HTML 的方式,它允许你为 DOM 元素添加额外的行为或改变其外观。Angular 中有三种类型的指令:组件(Component)、结构型指令(Structural Directive)和属性型指令(Attribute Directive)。组件本质上也是一种指令,它有自己的模板和样式,并且是视图的一部分。结构型指令会改变 DOM 的结构,比如 *ngIf
和 *ngFor
。属性型指令则用于改变元素的外观或行为,像 ngModel
。
二、自定义指令基础
- 创建自定义指令 要创建一个自定义指令,首先使用 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() { }
}
- 理解
@Directive
装饰器@Directive
装饰器用于定义一个指令。其中,selector
属性指定了指令在 HTML 中如何使用。上述例子中,[appHighlight]
表示该指令作为属性应用在 HTML 元素上,比如<div appHighlight></div>
。
三、属性型指令开发
- 简单样式修改指令
假设要创建一个指令,当鼠标悬停在元素上时,改变元素的背景颜色。
在
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>
- 带输入参数的属性型指令
有时需要指令根据传入的参数进行不同的行为。比如创建一个指令,根据传入的颜色参数改变元素的文本颜色。
修改
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>
四、结构型指令开发
- 简单的结构型指令原理
结构型指令主要用于改变 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) { }
}
在上述代码中:
- 通过构造函数注入
TemplateRef
和ViewContainerRef
。TemplateRef
代表了指令所附加到的模板,ViewContainerRef
用于管理视图的创建和销毁。 - 使用
@Input()
装饰器定义了appUnless
属性,它接受一个布尔值。 - 根据
condition
的值,通过viewContainer
的createEmbeddedView
方法创建视图,或通过clear
方法移除视图。
在 HTML 中使用:
<ng-template appUnless="isTrue">
<div>当 isTrue 为 false 时我会显示</div>
</ng-template>
- 带上下文的结构型指令
有时结构型指令需要向模板传递数据。比如创建一个
*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>
五、指令的生命周期钩子
- 常用生命周期钩子
ngOnInit
:在指令被初始化后调用,通常用于执行一次性的初始化逻辑,比如获取输入参数并进行处理。ngOnChanges
:当指令的输入属性发生变化时调用。可以通过SimpleChanges
对象获取变化的信息。ngDoCheck
:在 Angular 检测变更时调用,可用于实现自定义的变更检测逻辑。ngOnDestroy
:在指令被销毁前调用,常用于清理资源,比如取消订阅事件。
- 示例:使用
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
属性变化时,在控制台打印变化前后的值。
六、指令的依赖注入
- 依赖注入基础
在指令中,可以通过构造函数进行依赖注入。Angular 会自动创建并提供依赖的实例。例如,前面提到的
ElementRef
、TemplateRef
和ViewContainerRef
都是通过依赖注入获得的。 - 自定义依赖注入
假设创建一个服务
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
,并在指令逻辑中使用该服务获取高亮颜色。
七、指令的测试
- 测试属性型指令
以
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
中初始化测试环境。 - 编写两个测试用例,分别测试鼠标进入和离开时的样式变化。
- 测试结构型指令
以
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
时元素显示。
八、指令最佳实践
- 指令命名规范
- 指令选择器命名应该遵循 Angular 约定,通常使用中划线命名法,并且前缀使用应用名,如
[app - directive - name]
。 - 指令类名应该以
Directive
结尾,如HighlightDirective
。
- 指令功能单一性 每个指令应该专注于一个特定的功能。比如,不要在一个指令中既处理样式变化,又处理 DOM 结构变化,这样会使指令难以维护和测试。
- 合理使用输入和输出属性 如果指令需要从外部接收数据,使用输入属性。如果指令需要向外部传递事件,使用输出属性。确保输入和输出属性的命名清晰,易于理解。
- 性能优化
- 在结构型指令中,尽量减少不必要的视图创建和销毁。例如,可以缓存视图而不是每次都重新创建。
- 在属性型指令中,避免频繁操作 DOM,尤其是在高频率触发的事件中。可以使用
requestAnimationFrame
等方法优化性能。
通过以上内容,你应该对 Angular 自定义指令的开发有了深入的了解。无论是属性型指令还是结构型指令,都为我们在 Angular 应用中扩展 HTML 行为提供了强大的能力。合理运用指令的生命周期钩子、依赖注入和测试,能使我们开发出健壮、可维护的指令。