首页
/ Angular 指令(Directives)完全指南:从内置行为封装到宿主指令组合

Angular 指令(Directives)完全指南:从内置行为封装到宿主指令组合

2026-09-06 16:04:28作者:温玫谨Lighthearted

Angular 指令(Directive)是框架中为元素和组件添加行为的核心机制——它可以改变元素的外观、行为方式或其在 DOM 中的组织方式。本文基于 Angular 官方指南中的 指令概览文档,完整覆盖“何时使用指令”、“快速上手示例”与“三类指令”三大核心内容,并结合仓库中 属性指令示例源码结构指令指南指令组合 API 指南 的完整实现,深入剖析 @Directive() 装饰器的 selectorhost 元数据、结构指令微语法(microsyntax)翻译规则以及 hostDirectives 组合语义,帮助你既能写出可复用的自定义指令,也能理解框架底层的模板翻译机制。

指令是什么、何时使用

指令为 Angular 应用中的元素和组件添加行为:它可以改变元素如何呈现如何响应,以及它如何融入 DOM。Angular 自带多个内置指令(如 ngNonBindablengClassngComponentOutlet 以及控制流块 @if/@for/@switch 背后的结构指令),你也可以编写自己的指令。

官方指南给出的判断标准是:指令在封装可复用(reusable)行为并应用到既有元素或组件时最为有效。典型场景包括:

  • 跨多个元素应用相同的外观或行为:例如自动聚焦(autofocus)或工具提示(tooltip);
  • 读写宿主元素的 DOM、属性或 CSS 类:直接操作宿主节点;
  • 为不属于自己的组件添加行为而不修改其源码:例如给第三方组件的主机元素挂载监听逻辑。

一个重要的边界:如果你需要渲染自己的标记(markup)或管理带独立模板的 UI 片段,应该使用组件——组件本质上是“拥有自己模板的专用指令”。指令与组件的分工,决定了你在设计可复用 UI 逻辑时的技术选型。

快速示例:用信号与 host 绑定实现 appHighlight

官方概览文档用一个经典示例说明指令的封装能力:假设希望元素在鼠标悬停时背景变为黄色。与其在每个元素上重复相同的事件处理逻辑,不如把这段行为打包进一个指令,在需要的地方直接应用。

appHighlight 指令在鼠标进入时设置宿主元素的背景色,鼠标离开时清除:

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

@Directive({
  selector: '[appHighlight]',
  host: {
    '(mouseenter)': 'isHovered.set(true)',
    '(mouseleave)': 'isHovered.set(false)',
    '[style.background-color]': 'isHovered() ? "yellow" : null',
  },
})
export class HighlightDirective {
  protected isHovered = signal(false);
}

这里的机制有三层,值得逐个拆解:

  1. selector: '[appHighlight]':方括号表示这是一个属性选择器(attribute selector),指令会匹配任何携带 appHighlight 属性的元素。约定上使用 app 之类的前缀可以命名空间式地避免冲突。
  2. host 元数据:它是宿主元素上声明式绑定的映射表。'(mouseenter)': 'isHovered.set(true)' 这类事件绑定把鼠标事件映射为对 isHovered **信号(signal)**的更新;'[style.background-color]': 'isHovered() ? "yellow" : null' 则把宿主元素的 background-color 样式绑定到信号值——信号变化时框架自动重算样式绑定,鼠标进入时背景变为黄色,离开时置为 null 恢复原状。
  3. 应用方式:把选择器作为属性加到任意元素上即可:
<p appHighlight>Highlight me!</p>

所有携带 appHighlight 属性的元素都获得同样的悬停行为,而逻辑只在一处定义。这个示例体现了现代 Angular 指令的典型形态:声明式 host 绑定 + 信号状态,无需在构造函数中手动 addEventListener

三类指令:组件、属性指令与结构指令

Angular 有三大类指令,概览文档以如下表格概括:

指令类型 说明
组件(Components) 通过自己的模板定义可复用 UI
属性指令(Attribute directives) 改变元素、组件或其他指令的外观或行为
结构指令(Structural directives) 通过增删 DOM 元素来改变 DOM 布局

下面结合仓库中的配套文档与示例源码,对后两类做纵深展开。

属性指令:构建、事件、输入与 NgNonBindable

何时需要属性指令:Angular 模板语法本身已经覆盖了“一次性”行为——类/样式绑定(class/style 绑定)、属性/属性绑定、事件监听都能直接写在模板里。属性指令的价值在于把这类行为打包为可复用单元,应用到任意元素或组件上。

基本构建:自定义属性指令是一个带 @Directive() 装饰器的 JavaScript 类,selector 决定应用该指令的属性名。CLI 的 ng generate directive 命令可以直接脚手架生成指令及其测试文件(见 CLI schematics 指南 中的说明)。指令也可以通过命令式方式修改宿主——仓库示例 highlight.directive.1.ts 展示了注入 ElementRef 并通过其 nativeElement 属性访问宿主元素、将背景设为黄色的写法。需要注意:指令不支持命名空间<p app:Highlight> 这样的写法是非法的。

处理用户事件:通过 host 属性把宿主事件绑定到处理方法。仓库中的完整示例 highlight.directive.2.ts 如下:

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

@Directive({
  selector: '[appHighlight]',
  host: {
    '(mouseenter)': 'onMouseEnter()',
    '(mouseleave)': 'onMouseLeave()',
  },
})
export class HighlightDirective {
  private el = inject(ElementRef);

  onMouseEnter() {
    this.highlight('yellow');
  }

  onMouseLeave() {
    this.highlight('');
  }

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

hostmouseenter/mouseleave 事件映射到 onMouseEnter()/onMouseLeave() 方法,方法再委托给 highlight() 辅助函数直接设置宿主元素背景。与快速示例中“纯信号 + host 样式绑定”的声明式风格相比,这种 ElementRef 直改 DOM 的命令式写法在需要批量、复杂 DOM 操作时更灵活。

接收输入值:与组件一样,指令通过 input() 函数接收输入。把输入与选择器同名是一个关键技巧——这样单个绑定既应用了指令又传入了值。highlight.directive.ts 展示了两个输入的回退链:

defaultColor = input('');
appHighlight = input('');

onMouseEnter() {
  this.highlight(this.appHighlight() || this.defaultColor() || 'red');
}

读取输入时像调用信号一样调用它,并在未设置颜色时回退到默认值。在模板中:

<p appHighlight [appHighlight]="color">...</p>
<p appHighlight [appHighlight]="color" defaultColor="blue">...</p>

因为 appHighlight 输入与选择器同名,[appHighlight] 绑定同时完成“应用指令 + 设置值”两件事;而 defaultColor 接收静态字符串而非动态表达式,所以不需要方括号。

NgNonBindable 停用 Angular 处理:在宿主元素上添加 ngNonBindable 可阻止浏览器端表达式求值——它会停用该元素及子元素中的插值、指令和绑定。例如 {{ 1 + 1 }} 会原样渲染为 {{ 1 + 1 }} 而不是 2。但要留意一条容易踩坑的细节:ngNonBindable 仍然允许应用了它的那个元素本身上的指令继续工作——示例中 appHighlight 依然生效,只是插值表达式不被求值。若应用到父元素,其所有子元素的插值与任意形式的绑定(属性绑定、事件绑定)都会失效。

结构指令:ng-template、微语法与类型收窄

结构指令应用于 <ng-template> 元素,条件地或重复地渲染该模板的内容。日常的条件/循环渲染应优先使用内置控制流块(@if@for@switch);当你需要控制流未覆盖的可复用渲染行为时(例如把内容置于权限检查之后、从外部数据源取数后再提供模板),才编写自定义结构指令(详见 结构指令指南)。

示例场景:SelectDirective。指南以一个取数指令作为贯穿示例:它从给定数据源获取数据,并在数据可用时渲染模板。selectFrom 输入命名数据源,前缀 select 对简写语法很重要;指令用包含所选数据的模板上下文实例化 <ng-template>

<ng-template select let-data [selectFrom]="source">
  <p>The data is: {{ data }}</p>
</ng-template>

<ng-template> 本身默认不渲染任何东西——没有应用结构指令的包裹元素不会被渲染。

结构指令简写(microsyntax):Angular 支持用星号前缀把结构指令直接应用到元素上,如 *select,框架会把星号翻译成包裹该元素及其后代的 <ng-template>

<p *select="let data; from: source">The data is: {{ data }}</p>

这段微语法按如下规则展开(两种形式等价):

<!-- 简写语法: -->
<p class="data-view" *select="let data; from: source">The data is: {{ data }}</p>

<!-- 长形式语法: -->
<ng-template select let-data [selectFrom]="source">
  <p class="data-view">The data is: {{ data }}</p>
</ng-template>

展开遵循两条约定:let data 声明模板变量 data,由于没有赋值表达式,它绑定到模板上下文的 $implicit 属性;from source 是键值对,绑定键 from 通过 PascalCase 转换并前置结构指令选择器 映射到属性 selectFrom——这正是许多结构指令的输入都以选择器为前缀的原因。注意:简写中只有结构指令及其绑定被移到 <ng-template> 上,<p> 上的其他属性(如 class)保持原位。

每个元素只能有一个结构指令(简写场景):因为只展开出一个 <ng-template>,多个指令需要多层嵌套且先后顺序不明。需要叠加多个结构指令时,用 <ng-container> 构建包装层来显式定义嵌套结构。

实现一个结构指令:指令类需注入两个依赖——TemplateRef(访问所应用 <ng-template> 的内容)与 ViewContainerRef(模板可被渲染的 DOM 位置)。指南中的完整实现:

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

export interface DataSource<T> {
  load(): Promise<T>;
}

@Directive({
  selector: '[select]',
})
export class SelectDirective {
  private templateRef = inject(TemplateRef);
  private viewContainerRef = inject(ViewContainerRef);

  selectFrom = input.required<DataSource<unknown>>();

  async ngOnInit() {
    const data = await this.selectFrom().load();
    this.viewContainerRef.createEmbeddedView(this.templateRef, {
      // 通过 $implicit 键把数据放入上下文对象。
      $implicit: data,
    });
  }
}

selectFrom 使用 input.required(),因为没有数据源该指令毫无用处。createEmbeddedView() 的第二个参数是模板的上下文对象——模板可用 let 声明绑定其中值;把数据赋给 $implicit 使其成为 let-data(简写中的 let data)的默认接收值。指南同时注明:该示例在初始化时渲染一次,绑定数据源变化后不会重新渲染。

语法参考(grammar):编写自定义结构指令时应遵循的语法规则:

_: prefix = "( :let | :expression ) (';' | ',')? ( :let | :as | :keyExp )_";

as = :export "as" :local ";"?
keyExp = :key ":"? :expression ("as" :local)? ";"?
let = "let" :local "=" :export ";"?
关键字 含义
prefix HTML 属性键
key HTML 属性键
local 模板中使用的局部变量名
export 指令以给定名称导出的值
expression 标准 Angular 表达式

简写翻译规则如下表:

简写 翻译结果
prefix 与裸 expression [prefix]="expression"
keyExp [prefixKey]="expression"key 前加上 prefix
let local let-local="export"

几个具体翻译示例,可验证上述规则:

简写 Angular 的解析
*myDir="let item of [1,2,3]" <ng-template myDir let-item [myDirOf]="[1, 2, 3]">
*myDir="let item of [1,2,3] as items; trackBy: myTrack; index as i" <ng-template myDir let-item [myDirOf]="[1,2,3]" let-items="myDirOf" [myDirTrackBy]="myTrack" let-i="index">
*ngComponentOutlet="componentClass" <ng-template [ngComponentOutlet]="componentClass">
*ngComponentOutlet="componentClass; inputs: myInputs" <ng-template [ngComponentOutlet]="componentClass" [ngComponentOutletInputs]="myInputs">
*myDir="exp as value" <ng-template [myDir]="exp" let-value="myDir">

改进模板类型检查:为自定义指令添加模板守卫(template guards),让模板类型检查器在编译期发现模板错误。两种守卫:

  • ngTemplateGuard_(input):控制某个输入表达式应如何被收窄。支持两种形式——类型断言函数收窄,以及用字面量类型 'binding' 声明“按真值收窄”:
@Directive(...)
class ActorIsUser {
  actor = input<User | Robot>();

  static ngTemplateGuard_actor(dir: ActorIsUser, expr: User | Robot): expr is User {
    return true; // 运行时不使用,仅为避免 TS 报错
  }
}

@Directive(...)
class CustomIf {
  condition = input.required<boolean>();

  static ngTemplateGuard_condition: 'binding';
}
  • ngTemplateContextGuard:确定指令模板上下文对象的类型,对泛型指令尤其有用——用指令自身的类型推导上下文类型:
export interface SelectTemplateContext<T> {
  $implicit: T;
}

@Directive(...)
export class SelectDirective<T> {
  selectFrom = input.required<DataSource<T>>();

  static ngTemplateContextGuard<T>(dir: SelectDirective<T>, ctx: any): ctx is SelectTemplateContext<T> {
    return true;
  }
}

指令组合 API:hostDirectives

指令组合 API 允许你在组件 TypeScript 类内部把指令应用到组件的宿主元素。在组件装饰器中添加 hostDirectives 属性:

@Component({
  selector: 'admin-menu',
  templateUrl: './admin-menu.html',
  hostDirectives: [MenuBehavior],
})
export class AdminMenu {}

框架渲染组件时,会为每个宿主指令创建实例,其 host 绑定作用于组件宿主元素。三条约束必须记住:

  • 宿主指令在编译期静态应用,不能运行时动态添加;
  • hostDirectives 中使用的指令不得声明 standalone: false
  • Angular 忽略 hostDirectives 中指令的 selector

默认情况下宿主指令的输入/输出不暴露为组件公共 API。展开条目可以显式包含,还支持别名自定义组件 API:

hostDirectives: [
  {
    directive: MenuBehavior,
    inputs: ['menuId: id'],
    outputs: ['menuClosed: closed'],
  },
]
<admin-menu id="top-menu" (closed)="logMenuClosed()"></admin-menu>

hostDirectives 同样可以加在指令上,实现行为的传递性聚合MenuWithTooltip 组合 MenuTooltipSpecializedMenuWithTooltip 再组合 MenuWithTooltip——最终模板中使用时会创建全部三个指令的实例,各指令的 host 绑定都作用于最终宿主元素。

执行顺序上:宿主指令经历与普通组件/指令相同的生命周期,但其构造函数、生命周期钩子和绑定总是先于它所应用的组件或指令执行——组合多个行为时,这个“宿主指令先行”的语义会影响初始化逻辑的依赖假设。

核心要点与仓库索引

  1. 选型判断:封装可复用行为 → 指令;渲染自有模板 → 组件;改 DOM 结构 → 结构指令。
  2. 属性指令selector 方括号属性选择 + host 声明式绑定(事件 + 样式,可配信号)或 ElementRef 命令式操作;输入与选择器同名可“一绑定两用”;ngNonBindable 停用子树绑定但保留元素自身指令。
  3. 结构指令TemplateRef + ViewContainerRef 双注入;微语法按“键加前缀转 PascalCase”规则翻译为长形式;let 无赋值默认绑定 $implicit;每元素限一个结构指令,嵌套用 <ng-container>;用 ngTemplateGuard_* / ngTemplateContextGuard 获得编译期类型收窄。
  4. 组合 APIhostDirectives 编译期静态应用、忽略 selector、需显式声明才能暴露输入/输出,且宿主指令生命周期先于宿主组件执行。

仓库中可继续深入的入口:指令概览属性指令指南结构指令指南指令组合 API 指南,以及可运行的示例源码 highlight.directive.1.tshighlight.directive.2.tshighlight.directive.3.tshighlight.directive.tsapp.component.html。指令与组件的运行时行为由核心包 packages/core 中的 DI、渲染器与变更检测体系支撑,可进一步查阅其中的 linker 与 render3 实现了解指令实例化与 host 绑定的底层链路。

登录后查看全文
热门项目推荐
相关项目推荐