日常写模板,@if / @for 已经够用;但在它们之下还有一层更基础的机制——ng-template 与嵌入式视图(embedded view)。理解这层机制,才能看懂结构型指令的微语法、CDK Portal、动态表格这类”模板作为数据传递”的高级玩法。本文从”什么都不渲染的元素”讲起,直到用它搭出一个动态渲染器。
先看一个”什么都不渲染”的元素
<ng-template>
<p>你好</p>
</ng-template>
页面空空如也。ng-template 的内容不会自己渲染——它只是一份”模板定义”,被 Angular 编译成 TemplateRef 对象挂在一旁,等某处显式要求渲染时,才会实例化成真实的视图。这正是它与普通元素最本质的区别:
| 普通元素 | ng-template | |
|---|---|---|
| 内容何时渲染 | 立即 | 从不(除非被显式渲染) |
| 编译产物 | 立即的视图指令 | TemplateRef 定义 |
| 典型用途 | 页面本体 | 延迟渲染、复用渲染、模板传参 |
模板引用:#tpl 与 ngTemplateOutlet
最直接的使用方式:模板引用变量拿到 TemplateRef,交给 ngTemplateOutlet 渲染。
<ng-template #greeting>
<p>欢迎回来!</p>
</ng-template>
<!-- 结构型写法 -->
<ng-container *ngTemplateOutlet="greeting" />
<!-- 等价的属性写法(可与 @if 等控制流共存于同一元素) -->
<ng-container [ngTemplateOutlet]="greeting" />
两种写法的选择:结构型 * 写法会”吃掉”元素上的其他结构型指令,需要与其他控制流并列时用 [ngTemplateOutlet] 属性写法更清晰。ngTemplateOutlet 从 @angular/common 导入。
上下文:let- 与 context
模板可以接收参数化的数据。let- 声明上下文变量,context 传值:
<ng-template #itemTpl let-item let-i="index">
<li>{{ i + 1 }}. {{ item.title }}</li>
</ng-template>
@for (row of rows(); track row.id; let i = $index) {
<ng-container
[ngTemplateOutlet]="itemTpl"
[ngTemplateOutletContext]="{ $implicit: row, index: i }"
/>
}
规则只有两条:
let-item不带名字,取上下文的$implicit(“默认值”槽位)。let-i="index"带名字,取上下文对象的同名属性。
这就是同一份模板渲染不同数据的全部机制——传不同的 context,得到不同的视图。
在组件类中拿到模板:TemplateRef
模板引用变量也能被类代码查询到。v22 的信号式查询写法:
import { Component, TemplateRef, viewChild } from '@angular/core';
@Component({
selector: 'app-renderer',
template: `
<ng-template #rowTpl let-row>
<span>{{ row.name }}</span>
</ng-template>
`,
})
export class RendererComponent {
rowTpl = viewChild.required<TemplateRef<RowContext>>('rowTpl');
}
另一种常见入口是”挂在 ng-template 上的指令”,通过注入拿 TemplateRef:
import { Directive, TemplateRef, inject } from '@angular/core';
@Directive({ selector: 'ng-template[appCell]' })
export class CellDirective {
readonly tpl = inject<TemplateRef<unknown>>(TemplateRef);
}
选择器 ng-template[appCell] 意为”ng-template 且带 appCell 属性”,指令实例化时 DI 容器自动把宿主模板注入进来。viewChildren(CellDirective) 配合这种写法,可以把页面上的多个模板收集成一个字典——后面实战会用到。
渲染它:ViewContainerRef.createEmbeddedView
ViewContainerRef 是”视图容器”,负责在其所在位置插入/移除视图:
import { Component, TemplateRef, ViewContainerRef, inject, viewChild } from '@angular/core';
@Component({
selector: 'app-host',
template: `
<ng-template #toastTpl let-msg>
<div class="toast">{{ msg }}</div>
</ng-template>
`,
})
export class HostComponent {
private toastTpl = viewChild.required<TemplateRef<{ $implicit: string }>>('toastTpl');
private vcr = inject(ViewContainerRef);
showToast(msg: string) {
this.vcr.createEmbeddedView(this.toastTpl(), { $implicit: msg });
}
clear() {
this.vcr.clear();
}
}
createEmbeddedView(templateRef, context) 把模板实例化并追加到容器,返回的 EmbeddedViewRef 还支持 destroy() 单独销毁。手动管理视图的机会不多,但 toast、tooltip、动态行这类”调用式产生视图”的场景,它是最趁手的底层工具。
新控制流的底座
ng-template 不止是历史 API,它是整套结构体系的编译目标:
flowchart TD
A["模板代码"] --> B["块控制流<br/>@if / @for / @switch"]
A --> C["结构型指令微语法<br/>*ngIf / *ngTemplateOutlet"]
B --> D["嵌入式视图<br/>TemplateRef + ViewContainerRef"]
C --> D
D --> E["DOM"]
- 旧的
*微语法会被脱糖(desugar)成<ng-template>包裹(第 12 篇详解)。 - 新的
@if/@for不再经过指令,但编译产物仍然是”创建/销毁嵌入式视图”这套底层操作,只是由编译器直接生成调用,省掉了指令运行时开销。
所以理解 ng-template 的收益是双重的:读得懂旧代码与第三方库(大量使用 * 语法与 TemplateRef),也更能理解新控制流的性能来源。
何时直接使用 ng-template
把判断标准列成清单:
- 同一份数据需要多套渲染模板(表格的文本列/徽章列/进度列)——模板即策略。
- 库与抽象层开发(CDK Portal、可复用容器组件),把”渲染权”交给调用方。
- 调用式渲染(toast、确认框),需要 ViewContainerRef 动态插入。
- 单纯的”延迟到某条件再渲染”:用
@if或@defer,不要用 ng-template 绕路。
容易混淆的近亲也顺便划清:ng-content 是内容投影(把父组件写的子内容投进来,第 16 篇相关);动态组件(createComponent)渲染的是组件类而非模板(第 15 篇)。
实战:动态单元格渲染器
综合示例——表格列配置里放 TemplateRef,渲染时按列选择模板:
import { Component, Directive, TemplateRef, viewChildren } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
@Directive({ selector: 'ng-template[appCell]' })
export class CellDirective {
constructor(readonly tpl: TemplateRef<unknown>) {}
}
@Component({
selector: 'app-data-table',
imports: [NgTemplateOutlet],
template: `
<!-- 列模板:由使用方在内容区声明 -->
<ng-template appCell let-row>{{ row.name }}</ng-template>
<ng-template appCell let-row>
<span class="badge">{{ row.status }}</span>
</ng-template>
@for (row of rows(); track row.id) {
<div class="row">
@for (cell of cells(); track cell) {
<ng-container [ngTemplateOutlet]="cell.tpl" [ngTemplateOutletContext]="{ $implicit: row }" />
}
</div>
}
`,
})
export class DataTableComponent {
rows = signal([
{ id: 1, name: '订单 A', status: 'PAID' },
{ id: 2, name: '订单 B', status: 'PENDING' },
]);
cells = viewChildren(CellDirective);
}
viewChildren(CellDirective) 把所有挂在 ng-template 上的指令实例收集为信号数组,每个指令持有自己的 TemplateRef——模板的顺序即列的顺序。换列、换样式只需增删 ng-template 声明,渲染逻辑零改动。这正是 Material 表格与各类企业组件库的通用手法。
小结
ng-template的内容默认不渲染,编译为 TemplateRef 等待显式实例化。#tpl+ngTemplateOutlet(context 配合let-)覆盖大多数复用渲染场景。- 类代码用
viewChild/viewChildren查询模板,或用指令注入TemplateRef。 ViewContainerRef.createEmbeddedView是调用式产生视图的底层入口。*微语法脱糖为 ng-template,新控制流直接编译为嵌入式视图操作——它是新旧两代语法的共同底座。- 模板即数据:列配置、Portal、Toast 都是这一机制的直接应用。
系列导航
- 上一篇:@let 模板局部变量
- 下一篇:属性型指令 Attribute Directives
- 延伸阅读:结构型指令与微语法 · 动态组件 · 新的控制流 @if / @for / @switch