13_自定義指令實踐
自定義指令實踐:擴展HTML能力
指令是Angular中與組件并列的核心特性,用于擴展HTML元素的行為和外觀。與組件不同,指令沒有模板,而是通過裝飾器和類方法實現(xiàn)對DOM的操作。本章將從基礎到高級,系統(tǒng)講解自定義指令的開發(fā)實踐,包括屬性指令、結構指令的實現(xiàn)技巧,以及指令與組件、服務的協(xié)同工作方式。
1. 指令基礎:類型與核心概念
Angular指令分為三類:組件指令(帶模板的特殊指令)、屬性指令(修改元素屬性或樣式)和結構指令(修改DOM結構)。本章聚焦于自定義屬性指令和結構指令的開發(fā)。
1.1 指令元數(shù)據(jù)與選擇器
自定義指令通過@Directive裝飾器定義,核心元數(shù)據(jù)包括selector(選擇器)和standalone(獨立模式):
import { Directive } from '@angular/core';
// 基本指令結構
@Directive({
selector: '[appHighlight]', // 屬性選擇器:以[]包裹
standalone: true // 獨立指令(無需模塊)
})
export class HighlightDirective {
// 指令邏輯
}
選擇器規(guī)則:
- 屬性選擇器:
[屬性名](如[appHighlight]),匹配帶該屬性的元素 - 元素選擇器:
元素名(如app-my-directive),匹配特定元素 - 類選擇器:
.類名(如.special),匹配帶該類的元素 - 組合選擇器:
input[appNumber](匹配帶appNumber屬性的input元素)
1.2 指令與組件的區(qū)別
| 特性 | 組件 | 指令 |
|---|---|---|
| 模板 | 必須有(template或templateUrl) |
無模板 |
| 用途 | 構建UI視圖,擁有自己的DOM結構 | 擴展現(xiàn)有元素的行為或樣式 |
| 選擇器 | 通常為元素選擇器(如app-component) |
通常為屬性選擇器(如[appDirective]) |
| 生命周期 | 完整的組件生命周期 | 除ngAfterContentInit等內容投影相關鉤子外的大部分生命周期 |
2. 屬性指令:增強元素行為
屬性指令用于修改DOM元素的外觀或行為(如樣式、事件監(jiān)聽、屬性值等),是最常用的自定義指令類型。
2.1 基礎樣式修改指令
實現(xiàn)一個高亮指令,根據(jù)條件修改元素背景色:
// 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();
}
// 更新高亮狀態(tài)
private updateHighlight() {
if (this.appHighlight) {
// 通過nativeElement訪問DOM元素
this.el.nativeElement.style.backgroundColor = this.highlightColor;
} else {
this.el.nativeElement.style.backgroundColor = '';
}
}
// 監(jiān)聽輸入屬性變化,動態(tài)更新樣式
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>
<!-- 動態(tài)控制是否激活 -->
<p [appHighlight]="isActive" [highlightColor]="'pink'">
可切換的高亮(點擊按鈕切換)
</p>
<button (click)="isActive = !isActive">切換高亮</button>
`
})
export class ExampleComponent {
isActive = true;
}
2.2 事件處理指令
實現(xiàn)一個防抖指令,限制高頻事件(如輸入、滾動)的觸發(fā)頻率:
// 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();
});
}
// 監(jiān)聽宿主元素的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 帶依賴注入的指令
指令可以注入服務,實現(xiàn)更復雜的功能(如權限控制、日志記錄):
// 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結構(如條件渲染、循環(huán)渲染),核心是通過TemplateRef和ViewContainerRef操作模板。
3.1 基礎條件渲染指令
實現(xiàn)一個*appIf指令,類似*ngIf但支持自定義加載狀態(tài):
// 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() {
// 清空現(xiàn)有視圖
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">
數(shù)據(jù)加載完成:{{ data }}
</div>
<ng-template #loading>
<p>加載中...</p>
</ng-template>
<button (click)="showContent = !showContent">切換顯示</button>
3.2 循環(huán)渲染指令
實現(xiàn)一個*appFor指令,類似*ngFor但支持索引和空狀態(tài):
// for.directive.ts
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[appFor]',
standalone: true
})
export class ForDirective {
// 存儲當前數(shù)據(jù)
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();
}
// 空狀態(tài)模板
@Input() appForEmpty?: TemplateRef<any>;
// 更新視圖
private updateView() {
this.viewContainer.clear();
if (this.items.length === 0 && this.appForEmpty) {
// 數(shù)據(jù)為空且有empty模板:渲染空狀態(tài)
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>
<!-- 帶空狀態(tài) -->
<div *appFor="let item of products; empty noProducts">
<p>{{ item.name }}</p>
</div>
<ng-template #noProducts>
<p>暫無產品數(shù)據(jù)</p>
</ng-template>
4. 高級指令技巧
4.1 指令與信號(Signals)結合
Angular 16+引入的信號(Signals)可與指令結合,實現(xiàn)響應式狀態(tài)管理:
// tooltip.directive.ts(信號版 tooltip 指令)
import { Directive, Input, ElementRef, HostListener, signal } from '@angular/core';
@Directive({
selector: '[appTooltip]',
standalone: true
})
export class TooltipDirective {
// 信號:控制tooltip顯示狀態(tài)
private isVisible = signal(false);
// 輸入:tooltip文本
@Input() appTooltip!: string;
// 輸入:位置(上/下/左/右)
@Input() tooltipPosition: 'top' | 'bottom' | 'left' | 'right' = 'top';
// tooltip元素
private tooltipElement: HTMLElement;
constructor(private el: ElementRef) {
// 創(chuàng)建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);
// 監(jiān)聽信號變化,更新顯示狀態(tài)
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 指令組合與優(yōu)先級
多個指令可應用于同一元素,通過priority控制執(zhí)行順序(數(shù)值越大優(yōu)先級越高):
// 高優(yōu)先級指令
@Directive({
selector: '[appHighPriority]',
standalone: true,
priority: 100 // 優(yōu)先級高
})
export class HighPriorityDirective {
constructor() {
console.log('高優(yōu)先級指令初始化');
}
}
// 低優(yōu)先級指令
@Directive({
selector: '[appLowPriority]',
standalone: true,
priority: 10 // 優(yōu)先級低
})
export class LowPriorityDirective {
constructor() {
console.log('低優(yōu)先級指令初始化');
}
}
應用多個指令:
<!-- 輸出順序:高優(yōu)先級 → 低優(yōu)先級 -->
<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';
// 鼠標點擊切換激活狀態(tài)
@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 命名規(guī)范
- 指令選擇器前綴:使用項目特定前綴(如
app)避免沖突 - 功能命名:選擇器名稱應明確表示指令功能(如
appHighlight、appDebounce) - 輸入屬性:與指令同名的輸入屬性用于主開關(如
[appPermission]="'edit'")
5.2 性能優(yōu)化
- 避免頻繁DOM操作:通過防抖、節(jié)流減少高頻事件處理
- 清理資源:在
ngOnDestroy中移除事件監(jiān)聽、定時器等 - 使用
DestroyRef:Angular 16+推薦使用DestroyRef管理訂閱生命周期
5.3 適用場景
- 通用UI增強:如高亮、tooltip、拖拽等
- 行為封裝:如防抖、節(jié)流、權限控制等
- 跨組件復用邏輯:不適宜用組件實現(xiàn)的共享行為
5.4 避免過度使用
- 復雜UI邏輯優(yōu)先使用組件
- 單一職責:一個指令只做一件事
- 避免嵌套結構指令:多個結構指令應用于同一元素可能導致不可預期的行為
總結
自定義指令是Angular擴展HTML能力的強大工具,通過屬性指令可以增強元素行為和樣式,通過結構指令可以操縱DOM結構。本章從基礎概念出發(fā),通過實例講解了指令的開發(fā)流程,包括選擇器定義、宿主元素交互、依賴注入集成等核心技術。
高級部分介紹了指令與信號結合、指令組合、宿主綁定等技巧,幫助開發(fā)者構建更靈活、高效的指令。遵循最佳實踐,合理使用指令可以顯著提升代碼復用性和應用質量,尤其在開發(fā)通用組件庫或處理跨組件共享行為時發(fā)揮重要作用。
在實際開發(fā)中,應根據(jù)需求選擇合適的指令類型,平衡靈活性與性能,避免過度設計,讓指令真正成為組件的有力補充。

浙公網安備 33010602011771號