13_自定义指令实践
自定义指令实践:扩展HTML能力
指令是Angular中与组件并列的核心特性,用于扩展HTML元素的行为和外观。与组件不同,指令没有模板,而是通过装饰器和类方法实现对DOM的操作。本章将从基础到高级,系统讲解自定义指令的开发实践,包括属性指令、结构指令的实现技巧,以及指令与组件、服务的协同工作方式。
1. 指令基础:类型与核心概念
Angular指令分为三类:组件指令(带模板的特殊指令)、属性指令(修改元素属性或样式)和结构指令(修改DOM结构)。本章聚焦于自定义属性指令和结构指令的开发。
1.1 指令元数据与选择器
自定义指令通过@Directive装饰器定义,核心元数据包括selector(选择器)和standalone(独立模式):
import { Directive } from '@angular/core';
// 基本指令结构
@Directive({
selector: '[appHighlight]', // 属性选择器:以[]包裹
standalone: true // 独立指令(无需模块)
})
export class HighlightDirective {
// 指令逻辑
}
选择器规则:
- 属性选择器:
[属性名](如[appHighlight]),匹配带该属性的元素 - 元素选择器:
元素名(如app-my-directive),匹配特定元素 - 类选择器:
.类名(如.special),匹配带该类的元素 - 组合选择器:
input[appNumber](匹配带appNumber属性的input元素)
1.2 指令与组件的区别
| 特性 | 组件 | 指令 |
|---|---|---|
| 模板 | 必须有(template或templateUrl) |
无模板 |
| 用途 | 构建UI视图,拥有自己的DOM结构 | 扩展现有元素的行为或样式 |
| 选择器 | 通常为元素选择器(如app-component) |
通常为属性选择器(如[appDirective]) |
| 生命周期 | 完整的组件生命周期 | 除ngAfterContentInit等内容投影相关钩子外的大部分生命周期 |
2. 属性指令:增强元素行为
属性指令用于修改DOM元素的外观或行为(如样式、事件监听、属性值等),是最常用的自定义指令类型。
2.1 基础样式修改指令
实现一个高亮指令,根据条件修改元素背景色:
// highlight.directive.ts
import { Directive, ElementRef, Input, OnInit } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective implements OnInit {
// 输入属性:高亮颜色(默认黄色)
@Input() highlightColor = 'yellow';
// 输入属性:是否激活(默认true)
@Input() appHighlight = true; // 与选择器同名,支持<元素 [appHighlight]="false">语法
// ElementRef:获取宿主元素的DOM引用
constructor(private el: ElementRef) {}
ngOnInit() {
// 初始化时设置样式
this.updateHighlight();
}
// 更新高亮状态
private updateHighlight() {
if (this.appHighlight) {
// 通过nativeElement访问DOM元素
this.el.nativeElement.style.backgroundColor = this.highlightColor;
} else {
this.el.nativeElement.style.backgroundColor = '';
}
}
// 监听输入属性变化,动态更新样式
ngOnChanges() {
this.updateHighlight();
}
}
使用指令:
// 使用指令的组件
import { Component } from '@angular/core';
import { HighlightDirective } from './highlight.directive';
@Component({
selector: 'app-example',
standalone: true,
imports: [HighlightDirective], // 导入指令
template: `
<!-- 基础用法 -->
<p appHighlight>默认高亮(黄色)</p>
<!-- 自定义颜色 -->
<p appHighlight [highlightColor]="'lightblue'">浅蓝色高亮</p>
<!-- 动态控制是否激活 -->
<p [appHighlight]="isActive" [highlightColor]="'pink'">
可切换的高亮(点击按钮切换)
</p>
<button (click)="isActive = !isActive">切换高亮</button>
`
})
export class ExampleComponent {
isActive = true;
}
2.2 事件处理指令
实现一个防抖指令,限制高频事件(如输入、滚动)的触发频率:
// debounce.directive.ts
import { Directive, HostListener, Input, Output, EventEmitter } from '@angular/core';
import { Subject, debounceTime, takeUntil, skip } from 'rxjs';
import { DestroyRef, inject } from '@angular/core';
@Directive({
selector: '[appDebounce]',
standalone: true
})
export class DebounceDirective {
// 防抖时间(默认300ms)
@Input() debounceTime = 300;
// 输出防抖后的事件
@Output() debounced = new EventEmitter<any>();
// 用于防抖的Subject
private eventSubject = new Subject<any>();
// 自动销毁订阅(Angular 16+)
private destroyRef = inject(DestroyRef);
constructor() {
// 防抖处理
const subscription = this.eventSubject
.pipe(
debounceTime(this.debounceTime),
skip(1) // 跳过初始值
)
.subscribe(value => {
this.debounced.emit(value);
});
// 组件销毁时自动取消订阅
this.destroyRef.onDestroy(() => {
subscription.unsubscribe();
});
}
// 监听宿主元素的input事件
@HostListener('input', ['$event.target.value'])
onInput(value: string) {
this.eventSubject.next(value);
}
// 支持其他事件(如scroll)
@HostListener('scroll', ['$event'])
onScroll(event: Event) {
this.eventSubject.next(event);
}
}
使用防抖指令:
<!-- 输入框防抖 -->
<input
type="text"
appDebounce
[debounceTime]="500"
(debounced)="handleSearch($event)"
placeholder="搜索(500ms防抖)"
>
<!-- 滚动防抖 -->
<div
appDebounce
[debounceTime]="100"
(debounced)="handleScroll($event)"
style="height: 200px; overflow: auto; border: 1px solid #ccc;"
>
<!-- 长内容 -->
<p *ngFor="let i of [1,2,3,4,5,6,7,8,9,10]">滚动内容 {{i}}</p>
</div>
2.3 带依赖注入的指令
指令可以注入服务,实现更复杂的功能(如权限控制、日志记录):
// permission.directive.ts(权限控制指令)
import { Directive, Input, OnInit, TemplateRef, ViewContainerRef } from '@angular/core';
import { AuthService } from './auth.service';
@Directive({
selector: '[appPermission]',
standalone: true
})
export class PermissionDirective {
// 需要的权限
@Input() appPermission!: string;
constructor(
private authService: AuthService, // 注入权限服务
private templateRef: TemplateRef<any>, // 指令所在的模板
private viewContainer: ViewContainerRef // 视图容器
) {}
ngOnInit() {
// 检查是否有权限
if (this.authService.hasPermission(this.appPermission)) {
// 有权限:渲染模板
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
// 无权限:清空视图
this.viewContainer.clear();
}
}
}
权限服务:
// auth.service.ts
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class AuthService {
// 模拟权限检查
hasPermission(permission: string): boolean {
const userPermissions = ['read', 'edit']; // 当前用户拥有的权限
return userPermissions.includes(permission);
}
}
使用权限指令:
<!-- 有权限才显示的内容 -->
<button *appPermission="'edit'">编辑(需要edit权限)</button>
<!-- 无权限则不显示 -->
<button *appPermission="'delete'">删除(需要delete权限)</button>
3. 结构指令:操纵DOM结构
结构指令通过*语法糖修改DOM结构(如条件渲染、循环渲染),核心是通过TemplateRef和ViewContainerRef操作模板。
3.1 基础条件渲染指令
实现一个*appIf指令,类似*ngIf但支持自定义加载状态:
// if.directive.ts
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[appIf]',
standalone: true
})
export class IfDirective {
// 存储当前条件
private currentCondition: boolean = false;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef
) {}
// 输入属性:支持<元素 *appIf="condition; else elseTemplate">语法
@Input() set appIf(condition: boolean) {
this.currentCondition = condition;
this.updateView();
}
// 输入属性:else模板
@Input() appIfElse?: TemplateRef<any>;
// 更新视图
private updateView() {
// 清空现有视图
this.viewContainer.clear();
if (this.currentCondition) {
// 条件为true:渲染主模板
this.viewContainer.createEmbeddedView(this.templateRef);
} else if (this.appIfElse) {
// 条件为false且有else模板:渲染else模板
this.viewContainer.createEmbeddedView(this.appIfElse);
}
}
}
使用*appIf指令:
<!-- 基础用法 -->
<p *appIf="showContent">条件为true时显示</p>
<!-- 带else模板 -->
<div *appIf="hasData; else loading">
数据加载完成:{{ data }}
</div>
<ng-template #loading>
<p>加载中...</p>
</ng-template>
<button (click)="showContent = !showContent">切换显示</button>
3.2 循环渲染指令
实现一个*appFor指令,类似*ngFor但支持索引和空状态:
// for.directive.ts
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[appFor]',
standalone: true
})
export class ForDirective {
// 存储当前数据
private items: any[] = [];
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef
) {}
// 解析输入:*appFor="let item of items; let i = index; empty emptyTemplate"
@Input() set appForOf(items: any[]) {
this.items = items || [];
this.updateView();
}
// 空状态模板
@Input() appForEmpty?: TemplateRef<any>;
// 更新视图
private updateView() {
this.viewContainer.clear();
if (this.items.length === 0 && this.appForEmpty) {
// 数据为空且有empty模板:渲染空状态
this.viewContainer.createEmbeddedView(this.appForEmpty);
} else {
// 渲染列表项
this.items.forEach((item, index) => {
this.viewContainer.createEmbeddedView(this.templateRef, {
// 暴露变量给模板:$implicit(默认变量)、index(索引)
$implicit: item,
index: index
});
});
}
}
}
使用*appFor指令:
<!-- 基础用法 -->
<ul>
<li *appFor="let user of users; let i = index">
{{ i + 1 }}. {{ user.name }}
</li>
</ul>
<!-- 带空状态 -->
<div *appFor="let item of products; empty noProducts">
<p>{{ item.name }}</p>
</div>
<ng-template #noProducts>
<p>暂无产品数据</p>
</ng-template>
4. 高级指令技巧
4.1 指令与信号(Signals)结合
Angular 16+引入的信号(Signals)可与指令结合,实现响应式状态管理:
// tooltip.directive.ts(信号版 tooltip 指令)
import { Directive, Input, ElementRef, HostListener, signal } from '@angular/core';
@Directive({
selector: '[appTooltip]',
standalone: true
})
export class TooltipDirective {
// 信号:控制tooltip显示状态
private isVisible = signal(false);
// 输入:tooltip文本
@Input() appTooltip!: string;
// 输入:位置(上/下/左/右)
@Input() tooltipPosition: 'top' | 'bottom' | 'left' | 'right' = 'top';
// tooltip元素
private tooltipElement: HTMLElement;
constructor(private el: ElementRef) {
// 创建tooltip DOM元素
this.tooltipElement = document.createElement('div');
this.tooltipElement.className = 'app-tooltip';
this.tooltipElement.style.position = 'absolute';
this.tooltipElement.style.display = 'none';
document.body.appendChild(this.tooltipElement);
// 监听信号变化,更新显示状态
this.isVisible.subscribe(visible => {
this.tooltipElement.style.display = visible ? 'block' : 'none';
if (visible) {
this.positionTooltip();
}
});
}
// 定位tooltip
private positionTooltip() {
const hostRect = this.el.nativeElement.getBoundingClientRect();
const tooltipRect = this.tooltipElement.getBoundingClientRect();
switch (this.tooltipPosition) {
case 'top':
this.tooltipElement.style.top = `${hostRect.top - tooltipRect.height - 5}px`;
this.tooltipElement.style.left = `${hostRect.left + hostRect.width / 2 - tooltipRect.width / 2}px`;
break;
// 其他位置逻辑...
}
}
// 鼠标进入:显示tooltip
@HostListener('mouseenter') onMouseEnter() {
this.tooltipElement.textContent = this.appTooltip;
this.isVisible.set(true);
}
// 鼠标离开:隐藏tooltip
@HostListener('mouseleave') onMouseLeave() {
this.isVisible.set(false);
}
// 清理:移除tooltip元素
ngOnDestroy() {
document.body.removeChild(this.tooltipElement);
}
}
4.2 指令组合与优先级
多个指令可应用于同一元素,通过priority控制执行顺序(数值越大优先级越高):
// 高优先级指令
@Directive({
selector: '[appHighPriority]',
standalone: true,
priority: 100 // 优先级高
})
export class HighPriorityDirective {
constructor() {
console.log('高优先级指令初始化');
}
}
// 低优先级指令
@Directive({
selector: '[appLowPriority]',
standalone: true,
priority: 10 // 优先级低
})
export class LowPriorityDirective {
constructor() {
console.log('低优先级指令初始化');
}
}
应用多个指令:
<!-- 输出顺序:高优先级 → 低优先级 -->
<div appHighPriority appLowPriority></div>
4.3 宿主绑定与类/样式操作
通过@HostBinding直接绑定宿主元素的属性、类或样式:
// active.directive.ts
import { Directive, HostBinding, HostListener } from '@angular/core';
@Directive({
selector: '[appActive]',
standalone: true
})
export class ActiveDirective {
// 绑定宿主元素的class.active
@HostBinding('class.active') isActive = false;
// 绑定宿主元素的style.cursor
@HostBinding('style.cursor') cursor = 'pointer';
// 鼠标点击切换激活状态
@HostListener('click') onClick() {
this.isActive = !this.isActive;
}
// 鼠标悬停效果
@HostListener('mouseenter') onMouseEnter() {
this.cursor = 'pointer';
}
@HostListener('mouseleave') onMouseLeave() {
this.cursor = 'default';
}
}
使用效果:
<!-- 点击切换active类,鼠标悬停改变光标 -->
<div appActive>点击激活</div>
5. 指令最佳实践
5.1 命名规范
- 指令选择器前缀:使用项目特定前缀(如
app)避免冲突 - 功能命名:选择器名称应明确表示指令功能(如
appHighlight、appDebounce) - 输入属性:与指令同名的输入属性用于主开关(如
[appPermission]="'edit'")
5.2 性能优化
- 避免频繁DOM操作:通过防抖、节流减少高频事件处理
- 清理资源:在
ngOnDestroy中移除事件监听、定时器等 - 使用
DestroyRef:Angular 16+推荐使用DestroyRef管理订阅生命周期
5.3 适用场景
- 通用UI增强:如高亮、tooltip、拖拽等
- 行为封装:如防抖、节流、权限控制等
- 跨组件复用逻辑:不适宜用组件实现的共享行为
5.4 避免过度使用
- 复杂UI逻辑优先使用组件
- 单一职责:一个指令只做一件事
- 避免嵌套结构指令:多个结构指令应用于同一元素可能导致不可预期的行为
总结
自定义指令是Angular扩展HTML能力的强大工具,通过属性指令可以增强元素行为和样式,通过结构指令可以操纵DOM结构。本章从基础概念出发,通过实例讲解了指令的开发流程,包括选择器定义、宿主元素交互、依赖注入集成等核心技术。
高级部分介绍了指令与信号结合、指令组合、宿主绑定等技巧,帮助开发者构建更灵活、高效的指令。遵循最佳实践,合理使用指令可以显著提升代码复用性和应用质量,尤其在开发通用组件库或处理跨组件共享行为时发挥重要作用。
在实际开发中,应根据需求选择合适的指令类型,平衡灵活性与性能,避免过度设计,让指令真正成为组件的有力补充。

浙公网安备 33010602011771号