前言
从一段代码引入今天的话题:
你见过下面的代码吗?
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))。