Angular 指令(Directives)完全指南:从内置行为封装到宿主指令组合
Angular 指令(Directive)是框架中为元素和组件添加行为的核心机制——它可以改变元素的外观、行为方式或其在 DOM 中的组织方式。本文基于 Angular 官方指南中的 指令概览文档,完整覆盖“何时使用指令”、“快速上手示例”与“三类指令”三大核心内容,并结合仓库中 属性指令示例源码、结构指令指南 与 指令组合 API 指南 的完整实现,深入剖析 @Directive() 装饰器的 selector、host 元数据、结构指令微语法(microsyntax)翻译规则以及 hostDirectives 组合语义,帮助你既能写出可复用的自定义指令,也能理解框架底层的模板翻译机制。
指令是什么、何时使用
指令为 Angular 应用中的元素和组件添加行为:它可以改变元素如何呈现、如何响应,以及它如何融入 DOM。Angular 自带多个内置指令(如 ngNonBindable、ngClass、ngComponentOutlet 以及控制流块 @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);
}
这里的机制有三层,值得逐个拆解:
selector: '[appHighlight]':方括号表示这是一个属性选择器(attribute selector),指令会匹配任何携带appHighlight属性的元素。约定上使用app之类的前缀可以命名空间式地避免冲突。host元数据:它是宿主元素上声明式绑定的映射表。'(mouseenter)': 'isHovered.set(true)'这类事件绑定把鼠标事件映射为对isHovered**信号(signal)**的更新;'[style.background-color]': 'isHovered() ? "yellow" : null'则把宿主元素的background-color样式绑定到信号值——信号变化时框架自动重算样式绑定,鼠标进入时背景变为黄色,离开时置为null恢复原状。- 应用方式:把选择器作为属性加到任意元素上即可:
<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;
}
}
host 把 mouseenter/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 组合 Menu 与 Tooltip,SpecializedMenuWithTooltip 再组合 MenuWithTooltip——最终模板中使用时会创建全部三个指令的实例,各指令的 host 绑定都作用于最终宿主元素。
执行顺序上:宿主指令经历与普通组件/指令相同的生命周期,但其构造函数、生命周期钩子和绑定总是先于它所应用的组件或指令执行——组合多个行为时,这个“宿主指令先行”的语义会影响初始化逻辑的依赖假设。
核心要点与仓库索引
- 选型判断:封装可复用行为 → 指令;渲染自有模板 → 组件;改 DOM 结构 → 结构指令。
- 属性指令:
selector方括号属性选择 +host声明式绑定(事件 + 样式,可配信号)或ElementRef命令式操作;输入与选择器同名可“一绑定两用”;ngNonBindable停用子树绑定但保留元素自身指令。 - 结构指令:
TemplateRef+ViewContainerRef双注入;微语法按“键加前缀转 PascalCase”规则翻译为长形式;let无赋值默认绑定$implicit;每元素限一个结构指令,嵌套用<ng-container>;用ngTemplateGuard_*/ngTemplateContextGuard获得编译期类型收窄。 - 组合 API:
hostDirectives编译期静态应用、忽略 selector、需显式声明才能暴露输入/输出,且宿主指令生命周期先于宿主组件执行。
仓库中可继续深入的入口:指令概览、属性指令指南、结构指令指南、指令组合 API 指南,以及可运行的示例源码 highlight.directive.1.ts、highlight.directive.2.ts、highlight.directive.3.ts、highlight.directive.ts 与 app.component.html。指令与组件的运行时行为由核心包 packages/core 中的 DI、渲染器与变更检测体系支撑,可进一步查阅其中的 linker 与 render3 实现了解指令实例化与 host 绑定的底层链路。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00