掌握 Angular 结构指令——基础知识
由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!
Angular 中的结构指令是该框架最强大的工具之一。我们在学习 Angular 的早期就会接触到它们。即使是开发最基本的 Angular 应用程序,它们也是必不可少的。
无论是渲染待办事项列表,还是在待办事项完成后切换图标,对于 Angular 开发者来说,*ngFor 和 *ngIf 指令在早期阶段就已耳熟能详。从那时起,它们便成为我们值得信赖的伙伴,在日常开发任务中我们经常依赖它们。然而,这些指令的内部运作机制对于新手和经验丰富的开发者来说往往都是个谜。
在本系列文章中,我们将深入探讨结构化指令的内部运作机制,全面解读星号背后的含义。本文将着重分析结构化指令在 DOM 中渲染所需的条件。
*easyToSpot - 结构指令微语法的简要介绍
你可能已经注意到了两个内置的结构指令:NgIf 和 NgFor。它们很容易识别,因为我遵循了 Angular 文档中直接规定的约定:结构指令通常以星号 * 开头。更有趣的是,文档还指出,Angular 使用此约定将指令应用的元素(也称为宿主元素)包裹在一个冒号 ( ng-template:)中。
<div *ngIf="hero">{{hero.name}}</div>
变成
<ng-template [ngIf]="hero">
<div>{{hero.name}}</div>
</ng-template>
从这份手写版本来看,我们可以发现定义结构指令的两种方式:
- 根据它们的作用(正如官方文档所述):结构指令是通过添加和删除 DOM 元素来改变 DOM 布局的指令。
- 它们本质上是:应用于
ng-templates 的指令,并带有可选的微语法,使我们的 HTML 更易于阅读。
指令的强大功能——借助依赖注入将内容渲染到 DOM。
我们可以充分利用 Angular 的依赖注入 (DI) 功能,因为我们处理的是一个指令。鉴于我们知道指令宿主的依赖关系,我们可以直接将其注入到我们的指令中来访问它。
以下示例对此进行了说明:
@Component({
// in our app
selector: 'app',
// ourDirective is applied to the host component
template: `<host ourDirective ></host>`,
})
export class AppComponent {}
@Component({
selector: 'host',
// the host simply renders the currentName
template: `{{ currentName }}`,
})
export class HostComponent {
// by default the currentName is setByTheHost
currentName = 'setByTheHost';
}
@Directive({
selector: '[ourDirective]',
})
export class OurDirective implements OnInit {
// ourDirective uses DI to get access to the HostComponent
public hostComponent = inject(HostComponent);
public ngOnInit(): void {
// after 3 seconds OurDirective sets the hostComponent's currentName as changedByDirective
setTimeout(() => {
this.hostComponent.currentName = 'changedByDirective';
}, 3000);
}
}
如果你想查看代码的实际运行效果,可以查看这个Stackblitz 示例。
注入模板
前面我们已经知道,结构化指令总是应用于ng-template元素。因此,我们可以注入 Angular 的 TemplateRef,它为我们提供将模板渲染到 DOM 所需的信息。让我们看一下下面的代码,了解 TemplateRef 的内部工作原理:
@Component({
selector: 'my-app',
template: `<ng-template [ourDirective]>I am in the template</ng-template>`,
})
export class AppComponent {}
@Directive({
selector: '[ourDirective]',
})
export class OurDirective implements OnInit {
private template = inject(TemplateRef);
public ngOnInit(): void {
console.log(
(this.template as any)._declarationTContainer.tViews.template + ''
);
}
}
这将把 TemplateRef 的指令(告诉 Angular 如何生成我们的 DOM 元素)记录到控制台:
function AppComponent_ng_template_0_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵtext(0, "I am in the template");
} }
现在我们已经掌握了如何渲染模板的信息,接下来需要找到一个地方来渲染它。Angular 的依赖注入系统再次为我们提供了所需的资源,即ViewContainerRef。
注入视图容器
每个 Angular 组件或指令都可以访问一个名为 ViewContainerRef 的东西。官方文档将其定义为一个容器,一个或多个视图可以附加到组件上。
我们可以将其理解为围绕锚元素的虚拟容器。锚元素指示了 DOM 中可以动态创建新元素的位置。该容器可以动态实例化新元素,并将这些新元素渲染为锚元素的同级元素。
我们的锚点元素可以是自定义元素、元素节点,甚至是注释元素。让我们来看下面的例子:
@Component({
selector: 'my-app',
template: `
<our-component></our-component>
<div ourDirective>On div</div>
<ng-template ourDirective>On ng-template</ng-template>
`,
})
export class AppComponent {}
@Directive({
selector: '[ourDirective]',
})
export class OurDirective {
private vcr = inject(ViewContainerRef);
public ngOnInit(): void {
console.log(this.vcr.element.nativeElement);
}
}
@Component({
selector: 'our-component',
template: `<div>Our Component</div>`,
})
export class OurComponent {
private vcr = inject(ViewContainerRef);
public ngOnInit(): void {
console.log(this.vcr.element.nativeElement);
}
}
这在 Stackblitz 和 Chrome 控制台中会产生以下结果: 我们可以看到,ViewContainerRef 的原生元素包括我们的自定义元素、一个常规 HTML元素,以及在 `<div>` 的情况下,Angular 会将其插入到它管理的任何(潜在)视图的 HTML 中的注释。每次我们都会获得 DOM 锚点,ViewContainerRef 可以使用该锚点创建新的(同级)元素。
divng-template<!--container-->
再次建议您查看Stackblitz 中的运行代码。
将两者结合起来
最后,我们拥有了符合结构指令官方定义所需的一切:
通过添加和删除 DOM 元素来改变 DOM 布局。
让我们创建一个自定义结构指令,将我们的模板渲染到 DOM 中两次!太棒了!
@Directive({
selector: '[twoTimes]',
})
export class TwoTimesDirective implements OnInit {
// get the template ref from the ng-template host
private template = inject(TemplateRef);
// get the viewcontainerref from the host: <!--comment-->
private vcr = inject(ViewContainerRef);
// on initialization of our directive, render our template to the DOM twice
public ngOnInit(): void {
this.vcr.createEmbeddedView(this.template);
this.vcr.createEmbeddedView(this.template);
}
}
我们将 TemplateRef 和 ViewContainerRef 都注入到指令中。在ngOnInit生命周期钩子中,我们基于从指令宿主获取的模板创建两个同级元素。
@Component({
selector: 'my-app',
template: `
<p *twoTimes>Two times from asterisk</p>
<ng-template twoTimes><p>Two times from ng-template</p></ng-template>
`,
})
export class AppComponent {}
为了说明我们的微语法已被正确转译,我们在 AppComponent 中使用了两种替代方案。结果是 DOM 中总共渲染了四个元素。每个组件都根据<!--comment-->各自的 ViewContainerRef 创建了两个同级元素:
<my-app ng-version="15.0.2">
<p>Two times from asterisk</p>
<p>Two times from asterisk</p>
<!--container-->
<p>Two times from ng-template</p>
<p>Two times from ng-template</p>
<!--container-->
</my-app>
刚刚开始
在本文中,我们迈出了真正理解结构化指令工作原理的第一步。然而,我们仅仅触及了皮毛,甚至还没来得及深入了解其背后的含义。为了真正发挥结构化组件的强大功能,我们需要了解如何使用上下文对象将数据传递给模板,如何确保对该上下文进行严格的模板类型检查,以及结构化指令的语法是如何解析的。
所以,让我们为自己感到骄傲,快速休息一下(非睡眠深度休息),让信息沉淀下来,然后兴奋地迎接我们通往结构指令掌握之旅的下一阶段。
文章来源:https://dev.to/this-is-angular/mastering-angular-structural-directives-the-basics-jhk