CHARLIE SAYS

查理如是说
DATE 2026-08-24
THEME
SERIES / ANGULAR / P-176 · Angular 高级教程

Angular 22+ 教程 11:属性型指令 Attribute Directives

指令(Directive)是 Angular 中容易被低估的能力:组件本质上就是”带模板的指令”,而狭义的指令则用于在不新建组件的情况下复用 DOM 行为。本文聚焦属性型指令(Attribute Directives)——监听宿主元素、改写它的外观与行为、通过 input() 接收配置。所有示例基于 Angular v22,信号 API 优先,旧的装饰器写法作为对照讲解。

指令的三种形态

类型作用典型代表
组件带模板的指令,构成视图@Component
属性型指令改变单个元素的外观或行为NgClass、自定义 appHighlight
结构型指令增删/复制 DOM 子树*ngIf(历史)、新控制流 @if

属性型指令以”属性”的形式挂在元素上,不改变 DOM 结构,只改变元素本身——这是它与结构型指令最本质的区别(结构型指令见下一篇)。

第一个属性型指令:appHighlight

需求:鼠标悬停时给元素加背景色,颜色可配置。

import { Directive, computed, input, signal } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  host: {
    '(mouseenter)': 'setHover(true)',
    '(mouseleave)': 'setHover(false)',
    '[style.background]': 'bg()',
  },
})
export class HighlightDirective {
  color = input<string>('yellow');
  hovered = signal(false);
  bg = computed(() => (this.hovered() ? this.color() : 'transparent'));

  setHover(hovered: boolean) {
    this.hovered.set(hovered);
  }
}

使用:

<p appHighlight>默认黄色高亮</p>
<p appHighlight color="gold">自定义颜色</p>

要点:

  • selector 用方括号 [appHighlight] 表示”作为属性匹配”;命名上建议 app 前缀,避免与原生属性、第三方指令冲突。
  • v22 中指令默认 standalone,直接在使用它的组件 imports 中声明即可。
  • host 对象里静态属性直接写键值对(如 role: 'button'),绑定沿用模板的 []() 语法。
  • host 绑定表达式里可以直接调用信号(如 bg()),信号变化会自动同步宿主——这正是 Zoneless 时代的推荐姿势,无需任何手动刷新。

host 对象 vs @HostBinding / @HostListener

同样的功能,历史写法使用装饰器:

import { Directive, HostBinding, HostListener } from '@angular/core';

@Directive({ selector: '[appHighlight]' })
export class HighlightDirective {
  @HostBinding('style.background') bg = 'transparent';

  @HostListener('mouseenter') onEnter() {
    this.bg = 'gold';
  }

  @HostListener('mouseleave') onLeave() {
    this.bg = 'transparent';
  }
}

两种写法编译结果等价,但官方风格指南推荐 host 对象:

维度host 对象(推荐)@HostBinding / @HostListener
声明位置集中在装饰器元数据分散在各个类成员上
可读性宿主契约一目了然需通读类定义才能拼出全貌
继承行为父类元数据与子类逻辑解耦,行为可预测装饰器成员随继承传播,容易踩坑
类型检查参与 strictTemplates 对 host 绑定的检查同样受检查
现状v22 新代码首选完全可用,存量代码大量存在

注意 host 对象中的事件绑定引用的是”方法名字符串”,方法必须存在于指令实例上。这反过来促使我们把宿主逻辑收敛为少量方法,而非散落的匿名处理。

用 input() 接收参数

指令与组件一样可以使用函数式 input,并且天然是信号:

@Directive({
  selector: '[appBadge]',
  host: {
    '[textContent]': 'label()',
  },
})
export class BadgeDirective {
  count = input.required<number>();
  max = input(99);

  label = computed(() =>
    this.count() > this.max() ? `${this.max()}+` : `${this.count()}`
  );
}
<span appBadge [count]="messages().length" [max]="99"></span>

如果需要在参数变化时执行副作用(调用 DOM API、打点、发请求),用 effect()

constructor() {
  effect(() => {
    const count = this.count();
    // 副作用:埋点、同步到 DOM 等
  });
}

对照历史写法:@Input() + ngOnChanges 仍是可用 API(旧的结构型指令教程大量使用),但在 v22 中,“声明式响应输入”应优先选择 computed / effect,它们与 Zoneless 变更检测无缝配合。

ElementRef 与 Renderer2

host 绑定覆盖不了所有场景。当需要直接操作 DOM 时,注入 ElementRef

import { Directive, ElementRef, afterNextRender, inject } from '@angular/core';

@Directive({ selector: '[appAutofocus]' })
export class AutofocusDirective {
  private el = inject<ElementRef<HTMLElement>>(ElementRef);

  constructor() {
    afterNextRender(() => this.el.nativeElement.focus());
  }
}

两个工程要点:

  • afterNextRender 保证回调只在浏览器执行,SSR 安全;直接在构造器里操作 nativeElement 可能污染服务端渲染输出。
  • 需要批量修改属性/样式时,inject(Renderer2) 提供平台无关的 setAttribute / setStyle,比直接操作 nativeElement 在 SSR 等环境下更稳妥。不过若能用 host 绑定 + 信号表达,就优先用绑定,DOM 操作永远是最后手段。

exportAs:让模板引用指令实例

指令可以通过 exportAs 暴露自己,模板变量即可读取指令状态或调用其方法:

@Directive({
  selector: '[appColorPicker]',
  exportAs: 'appColorPicker',
  host: {
    '(input)': 'onInput($event)',
  },
})
export class ColorPickerDirective {
  value = signal('#6750a4');

  onInput(event: Event) {
    this.value.set((event.target as HTMLInputElement).value);
  }

  reset() {
    this.value.set('#6750a4');
  }
}
<input appColorPicker #picker="appColorPicker" />
<button type="button" (click)="picker.reset()">重置</button>

内置属性型指令速览

名称来源作用示例
NgClass@angular/common批量增删 class<div [ngClass]="cls()">
NgStyle@angular/common批量设置样式<div [ngStyle]="{ fontSize: '14px' }">
class 绑定内置语法单个 class 开关<div [class.active]="isActive()">
style 绑定内置语法单条样式<div [style.width.px]="width()">
属性绑定内置语法任意 attribute<td [attr.colspan]="span()">

日常开发优先使用 class / style / attr 绑定语法;NgClass / NgStyle 适合对象或数组驱动的批量场景。

实战:图片加载失败回退

import { Directive, ElementRef, inject, input } from '@angular/core';

@Directive({
  selector: 'img[appFallback]',
  host: {
    '(error)': 'onError()',
  },
})
export class ImageFallbackDirective {
  fallback = input.required<string>();
  private el = inject<ElementRef<HTMLImageElement>>(ElementRef);

  onError() {
    const node = this.el.nativeElement;
    if (node.src !== this.fallback()) {
      node.src = this.fallback();
    }
  }
}
<img
  [src]="user().avatar"
  appFallback
  fallback="/assets/avatar-placeholder.png"
  alt="avatar"
/>

这个例子体现了属性型指令的核心价值:把”横切”的 DOM 行为从业务模板中抽走,任何 img 标签一行声明即可复用,且实现与业务完全解耦。

小结

  • 属性型指令 = 选择器 + host 契约 + 可选的 input(),用于横向复用 DOM 行为。
  • host 对象是 v22 推荐写法,@HostBinding / @HostListener 仍可用,重点掌握两者互转。
  • 信号(input / signal / computed)让指令天然融入 Zoneless 变更检测,不需要 markForCheck
  • 直接操作 DOM 时注意 SSR 安全:afterNextRenderRenderer2
  • exportAs 让指令可以被模板消费,是指令对外暴露受控 API 的正规方式。

系列导航

← 算法 011:归并排序(Merge Sort) 目录 设计模式 011:组合(Composite) →
← 返回文章列表