Angular ElementRef,ViewContainerRef,TemplateRef 之一网打尽

前言

从一段代码引入今天的话题:

你见过下面的代码吗?

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))。

相关推荐
starrysky8108 天前
你的 Ollama 可能正被别人用:CVE-2024-37032 完整攻击链与 3 条自查命令
angular.js
starrysky81025 天前
明明在赋值前读取,为什么还报 UnboundLocalError?——从 CPython 符号表拆解函数级作用域
angular.js
starrysky81025 天前
只填了个模型地址,5 秒就被接管——Open WebUI 攻击链拆解与「被拒 CVE」的坑
angular.js
大雄很用心1 个月前
package.json 中的 dependencies 是否都会被下载?|Angular 回归笔记 02
angular.js
starrysky8101 个月前
同一个 submit(),两条报错——ThreadPoolExecutor 在解释器退出时的竞态(附 4 组可复现实验)
angular.js
starrysky8101 个月前
30 个 CPU 全在内核态,只回收了 32 个页——per-memcg 内存回收的 mmap_lock 活锁
angular.js
starrysky8102 个月前
一个 NameError 掩盖了真根因:IPython 补全在 Django shell 里二次崩溃——从 jedi 版本错位查到 CPython LOAD_GLOBAL
angular.js
starrysky8102 个月前
SQL 注入 + msgpack 反序列化 = RCE:LangGraph Checkpointer 三洞连爆——Agent 的记忆层就是攻击面
angular.js
starrysky8102 个月前
NVD 9.8、官方 7.5:Redis TLS use-after-free 的评分之争——tlsProcessPendingData 悬指针踩空全解析
angular.js
巴勒个啦2 个月前
2026 年 CSS 选型真相:我用 Tailwind v4 + 原生新特性重构了一个组件库
前端·angular.js