前言
从一段代码引入今天的话题:
你见过下面的代码吗?
js
document.getElementById();
document.getElementsByClassName();
document.createElement();
相信学习过前端基础的伙伴们对这句话并不陌生,或者用过jQuery的小伙伴都知道,这类代码都是在现在主流框架出来前,前端工程师们每天都要打交道的工作--对Dom进行增删改查 ,从而实现用户所看到的交互效果。 那么当我们拥有了强大功能的Angular框架以后,是否还有这样的一个途径让我们直接接触到原生DOM,甚至对它进行操作呢?
答案是肯定的~让我们把目光聚焦到Angular提供的这几个对象ElementRef,ViewContainerRef和 TemplateRef。
ElementRef,ViewContainerRef,TemplateRef的概念
从官网文档来看:
ElementRef : 原生元素的包装器。也就是说它在原生dom外面又包裹了一层,其中nativeElement属性中就是我们上面代码获取的DOM对象。
ViewContainerRef :视图容器,可以将一至多个视图附着其中。它有个属性element,类型为ElementRef,但是该属性里面包含的nativeElment只是一个锚点,<!--ng-container-->用来标记container在HTML存在的位置。
TemplateRef :内嵌模板,可以通过ViewContainerRef的createEmbeddedView方法去实例并且放置到视图容器中,它同样具有elementRef这个对象,类型为ElementRef,也是存的锚点位置<!--container-->。
下面就跟着我从代码的角度去理解这三个对象,和他们之间的关系吧~ 首先,让我们一起新建一个新的Angular项目,执行下面的命令行(我目前的angular版本为14,在命令中默认使用css样式,关闭ssr和standalone模式,目的是向低版本angular兼容,低版本可以执行ng new my-element-explore):
ng new my-element-explore --style=css --standalone=false --ssr=false
清除app.component.html的内容,并且添加以下内容:
html
<h1 id="domElement" #domId>H1: This is a Dom Element</h1>
<ng-container #containerId>
<h2 id="containerElement">H2: This is a content in ng-container</h2>
</ng-container>
<ng-template #templateId>
<h3 id="templateElement">H3: This is a content in ng-template</h3>
</ng-template>
替代app.component.ts内容如下:
js
import { AfterViewInit, Component, ViewChild, ViewContainerRef } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit{
title = 'my-element-explore';
@ViewChild('domId') dom;
@ViewChild('containerId', {read: ViewContainerRef}) viewContainer: ViewContainerRef;
@ViewChild('templateId') template;
ngAfterViewInit(): void {
console.log('dom', this.dom);
console.log('viewContainer', this.viewContainer);
console.log('template', this.template);
}
}
启动项目ng serve,并且前往localhost:4200看下效果和控制台输出:
首先我们简单解释下上面的代码:Angular中有内置标签<ng-template>表示一个模板片段,如果你不告诉angular解析器你想要放在在何处,那它就不会在浏览器中显示出来。(从我们的例子中你也可以看出,页面上只能看到h1和h2两段文字)。<ng-container>则表示一个容器,他并没有实际对应的dom对象,当渲染完成的时候,只有被他包裹的内容会出现在页面中,如果你用developer tool去看的话,html里面只有<h2>标签。针对Angular Html模板中的所有元素(标签),我们都可以用#name去标记它,然后在ts文件中再通过@viewChild('name')去获得这个对象(其中我们用到read这个参数,后面会详细讲解 ),从我们输出的结果中可以看出,页面上的三组对象分别是ElementRef,ViewContainerRef和TemplateRef。
随后我们验证一下dom中的nativeElement属性是不是原生DOM元素,在ngAfterViewInit里面加上下面代码:
js
console.log('nativeElment', this.dom.nativeElement === document.getElementById('domElement'))
会在控制台中看到 true,因而我们可以确认ElementRef中的nativeElement就是原生DOM对象。
如何操作ElementRef
那么获得了DOM, 下面就要探究如何改变dom的形态,甚至对它进行增删呢?
抱歉的通知你,Angular并不推荐你直接获取nativeElement并且对其操作,官方给出的原因是这样会增加XSS攻击的风险。不过他提供了一个新的API供你操作:Renderer2。里面你会发现很多跟document类似的方法,参考官方文档。。
写个例子练下手,在app.component.ts中粘贴如下代码
js
import { AfterViewInit, Component, Renderer2, ViewChild, ViewContainerRef } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit{
title = 'my-element-explore';
@ViewChild('domId') dom;
@ViewChild('containerId', {read: ViewContainerRef}) viewContainer: ViewContainerRef;
@ViewChild('templateId') template;
constructor(private renderer2: Renderer2){}
ngAfterViewInit(): void {
this.renderer2.setStyle(this.dom.nativeElement, 'color', 'red');
}
}
运行结果如下:

由此可见,我们已经成功改变了第一行元素的样式~在Angular中对DOM元素的操作就算完成啦,更多详情请仔细阅读Renderer2文档哦!
ViewContainerRef 和 TemplateRef 配合使用
让我们聚焦到上面提到的ng-template,该元素内部是一个h3标签,但是我们在页面上并没有看见它,源自于ng-template它在未指定显示在哪个容器前是不会出现在浏览器页面中。那么如果在ts文件中给他指定容器,那就需要用到我们的ViewContainerRef中的createEmbeddedView()方法啦。 试着在ngAfterViewInit中添加下面的代码:
js
this.viewContainer.createEmbeddedView(this.template);
也就是说咱们把ng-template里的内容放置到ViewContainerRef对应的ng-container里面,页面上就会出现下面的h3内容啦:

可以看到createEmbeddedView方法是将TemplateRef对象插入视图的最末端,并不影响其他元素的位置,如果你想移除掉视图里的内嵌内容(我理解为通过createEmbeddedView插入的内容),可以先调用ViewContainerRef的clean()方法清空他们。
ViewContainerRef 和 ElementRef
从Component层面,一个Component里面会各有一个ViewContainerRef和ElementRef对象,我们可以直接在constructor()构造器中直接实例化他们,即可获得相应的对象:
ts
import { Component, ElementRef, OnInit, ViewContainerRef } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit{
constructor(private vcComponentRef: ViewContainerRef, private eleComponentRef: ElementRef){}
ngOnInit(): void {
console.log(this.vcComponentRef);
console.log(this.eleComponentRef);
}
}
从控制台看输出内容:

由此可见他们都是指向<app-root>标签的,那试问以下代码会返回什么值呢?
js
console.log(this.vcComponentRef.element === this.eleComponentRef); // false -- 看来ViewContainerRef.element和elementRef并不能完全划等号
console.log(this.vcComponentRef.element.nativeElement === this.eleComponentRef.nativeElement); // true -- 但是他们内部存储的原生DOM确实是同一个
同时由于自身component就有视图对象,因此我们也可以把ng-template中的内容嵌入到Component对应的视图中,在保持上述html不变的前提下,执行以下代码:
js
import { AfterViewInit, Component, ElementRef, ViewChild, ViewContainerRef } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit{
title = 'my-element-explore';
@ViewChild('domId') dom: any; // 确认该对象前,不指定类型
@ViewChild('containerId', { read: ViewContainerRef })
viewContainer!: any; // 默认是elementRef对象,因此必须指定read
@ViewChild('templateId') template: any; // 确认该对象前,不指定类型
constructor(private vcComponentRef: ViewContainerRef, private tempComponent: ElementRef){}
ngAfterViewInit(): void {
this.viewContainer.createEmbeddedView(this.template);
this.vcComponentRef.createEmbeddedView(this.template);
}
}
渲染结果:

由此可见ng-template内容被插入了两次 ,一次是插入我们html中定义的ng-container,另一个则是插入了component层面的视图中。
后话
其实针对ng-template,ng-container,ng-content里面还有很多知识,不过由于今天的主题是各种Ref的关系,因此就在这里小小挖个坑,日后再填啦。
下面一篇文章会先记录下在使用@viewChild的时候一些领悟,敬请期待一下~
这是本章内容的示例代码,按需取用~(VickySH9112020/my-element-explore (github.com))。