探索使用 ViewContainerRef 的 Angular DOM 操控技术

https://indepth.dev/posts/1052/exploring-angular-dom-manipulation-techniques-using-viewcontainerref

每当我阅读关于在 Angular 中处理 DOM 的文章的时候,我总是看到这些类型中的某些被提到:

  • ElementRef
  • TemplateRef
  • ViewContainerRef
  • ......

不幸的是,尽管它们在 Angular 的文档中被说明了,我还是没有找到对概念模型进行全面说明的文档和示例,以及它们是如何被组合使用的。本文尝试说明 Angular 的概念模型。

如果你正在寻找在 Angular 中使用 Renderer 和 View 来操作 DOM 的深入讨论,请查阅 my talk at NgVikings。或者阅读深入讨论动态 DOM 操控的文章 Working with DOM in Angular: unexpected consequences and optimization techniques

如果原来使用过 angular.js,你就会知道,处理 DOM 是非常简单的事情。Angular 将 DOM 元素 element 传递给 link() 函数,你可以查询组件模板内的任何节点,增加或者删除子节点,修改样式等等。不过,这种方式有一个重要的缺陷 - 它紧密耦合到浏览器平台上。

新的 Angular 运行在多种平台上 - 浏览器,移动平台,或者运行在 Web worker 上。所以需要一个抽象层来从平台特定的 API 中抽象出来框架的接口。在 Angular 中,这些抽象通过这些引用类型表示:

  • ElementRef
  • TemplateRef
  • ViewRef
  • ComponentRef
  • ViewContainerRef

在本文中,我们将深入演练这些抽象类型中每一个,并展示如何使用它们来操控 DOM。

@ViewChild

在开始说明这些 DOM 抽象之前,让我们先理解一下,如何在 Component/Directive 类中访问这些抽象。Angular 提供了被称为 DOM query 的机制。它使用了 @ViewChild 和 @ViewChildren 装饰器。两种的行为类似,只是前一种只返回一个引用,而后一种以 QueryList 对象的形式返回多个引用。在本文的示例中,我将主要使用 ViewChild 装饰器,并忽略这个 @ 符号。

一般来说,这些装饰器与 template reference variables 配套使用,template reference variable 是用来在模板中简单地引用 DOM 元素的方式。你可以想象它类似于 html 元素所提供的 id 特性。使用 template reference variable 来标记一个 DOM 元素,然后在类中使用 ViewChild 装饰器来查询到它。下面是一个基本的示例:

@Component({
selector: 'sample',
template: `
<span #tref>I am span</span>
`
})
export class SampleComponent implements AfterViewInit {
@ViewChild("tref", {read: ElementRef}) tref: ElementRef; ngAfterViewInit(): void {
// outputs `I am span`
console.log(this.tref.nativeElement.textContent);
}
}

@ViewChild 基本的使用语法如下:

@ViewChild([reference from template], {read: [reference type]});

在上面的示例中,你可以看到我指定了 tref 作为在 html 中的模板引用名称,并通过 ElementRef 类型关联到该元素。第 2 个参数 read 并不总是必须的,因为 Angular 可以通过 DOM 元素的类型推断出来引用类型。例如,如果它是一个简单的 html 元素,例如这里的 <span> 元素,Angular 就返回一个 ElementRef。如果它是一个 <template> 元素,就会返回一个 TemplateRef。有些引用,比如 ViewContainerRef 不能被推断出来,就必须在 read 参数中指定。另外,ViewRef 是不能通过 DOM 返回的,它必须手动构造出来。

好了,现在我们知道了如何查询到这些引用,现在可以开始说明它们了。

ElementRef

它是最为基本的抽象了。如果你查看它的类结构,你会发现它仅仅持有它关联到的原生元素。对于访问原生 DOM 元素来说,它非常有用:

// outputs `I am span`
console.log(this.tref.nativeElement.textContent);

不过,这样的用法是不被 Angular 所鼓励使用的。不仅是带来的安全风险,它还将你的应用程序与渲染层绑定在一起,使得难以运行在其它平台上。我相信访问 nativeElement 不仅破坏了抽象,还使用了特定的 DOM API,比如 textContent。不过随我我们会看到,在 Angular 中的概念模型中,很难用到如此低级的操作。

ElementRef 可以通过任何 DOM 元素通过 ViewChild 装饰器获得。

因为所有的 Component 都是寄宿在一个自定义的 DOM 元素之中,而所有的指令都需要通过 DOM 元素来应用,所以,Component 和 Directive 可以通过依赖注入而得到一个其关联寄宿元素的 ElementRef 的实例。

@Component({
selector: 'sample',
...
export class SampleComponent{
constructor(private hostElement: ElementRef) {
//outputs <sample>...</sample>
console.log(this.hostElement.nativeElement.outerHTML);
}

所以,Component 是通过 DI 来访问其计算的元素,而 ViewChild 装饰器更多用于获得 Component 内部模板中的 DOM 元素的引用。不过,对指令则不是,它们是没有模板的,指令直接工作在它们应用的元素之上。

TemplateRef

大多数的 Web 开发者都应该熟悉 template。它是一组 DOM 元素,可以跨整个应用程序在视图中重用。在 HTML5 标准引入 <template> 之前,很多模式是通过使用 script 在浏览器中实现的,这需要使用 type 一些变体。

<script id="tpl" type="text/template">
<span>I am span in template</span>
</script>

这种方式存在多种缺陷,比如语义和需要手动创建 DOM 模型。使用 <template>,浏览器可以解析其中的 HTML 并创建 DOM 树,而不需要渲染它。以后它可以通过 content 属性访问。

<script>
let tpl = document.querySelector('#tpl');
let container = document.querySelector('.insert-after-me');
insertAfter(container, tpl.content);
</script>
<div class="insert-after-me"></div>
<ng-template id="tpl">
<span>I am span in template</span>
</ng-template>

Angular 拥抱这种方式,并通过 TemplateRef 类来使用 <template>。下面是如何使用的示例。

@Component({
selector: 'sample',
template: `
<ng-template #tpl>
<span>I am span in template</span>
</ng-template>
`
})
export class SampleComponent implements AfterViewInit {
@ViewChild("tpl") tpl: TemplateRef<any>; ngAfterViewInit() {
let elementRef = this.tpl.elementRef;
// outputs `template bindings={}`
console.log(elementRef.nativeElement.textContent);
}
}

Angular 框架会从 DOM 中删除 <template> 元素,然后在它的位置插入一个注释。这里是渲染的结果:

<sample>
<!--template bindings={}-->
</sample>

对于 TemplateRef 类型,它只是一个简单的类。通过属性 elementRef 持有其宿主元素的引用,还有一个方法:createEmbeddedView()。不过,该方法非常有用,因为它支持我们创建 View 并返回一个对该 View 的引用 ViewRef。

ViewRef

在 Angular 中,ViewRef 表示 Angular 视图 (View) 的抽象表示。在 Angular 的世界中,View 是应用程序的基本构建块。它是在一起被创建或者销毁的最小元素组单位。Angular 哲学鼓励开发者将 UI 界面看作 View 的聚合。而不要看作标准的 HTML 元素树。

Angular 支持两种 View:

  • Embedded View,指 Template
  • Host View,指 Component

创建 embedded view

Template 用来简单地持有一个 View 的蓝图。View 可以通过 createEmbeddedView() 方法,通过 template 实例化出来。

指通过 <template>元素来创建出来模板,然后通过 createEmbeddedView() 方法实例化出来。

ngAfterViewInit() {
let view = this.tpl.createEmbeddedView(null);
}

创建 host view

Host View 通过组件动态实例化。组件可以使用 ComponentFactoryResolver() 方法动态创建出来。

constructor(private injector: Injector,
private r: ComponentFactoryResolver) {
let factory = this.r.resolveComponentFactory(ColorComponent);
let componentRef = factory.create(injector);
let view = componentRef.hostView;
}

在 Angular 中,每个 Component 都绑定到一个 Injector 的实例上,所以,当创建这个 Component 的时候,我们将当前组件的 Injector 传递进去。另外,不要忘了,动态实例化的 Component 需要加入到 Module 或者托管的 Component 的 EntryComponents 中。

所以,我们已经看到了可以创建的 embeded 和 host 两种 View。一旦 View 被创建出来,它就可以使用 ViewContainer 插入到 DOM 中。下一节我们就介绍它的功能。

ViewContainerRef

ViewContainerRef 表示可以容纳一个或者多个 View 的容器。

首先需要提醒的是,任何 DOM 元素都可以作为 View 的容器。有趣的是,Angular 不是将 View 插入到元素中,而是绑定到元素的 ViewContainer 中。这类似于 router-outlet 如何插入 Component。

通常,比较好的将一个位置标记为 ViewContainer 的方式,是创建一个 <ng-container> 元素。它会被渲染为一条 comment,所以不会带来多于的 HTML 元素到 DOM 中。下面是一个示例,演示了在 Component 的模板中创建 ViewContainer。

@Component({
selector: 'sample',
template: `
<span>I am first span</span>
<ng-container #vc></ng-container>
<span>I am last span</span>
`
})
export class SampleComponent implements AfterViewInit {
@ViewChild("vc", {read: ViewContainerRef}) vc: ViewContainerRef; ngAfterViewInit(): void {
// outputs `template bindings={}`
console.log(this.vc.element.nativeElement.textContent);
}
}

与其它的 DOM 抽象类似,ViewContainer 也通过 element 属性绑定以特定的 DOM 元素。在上面的示例中,就是 ng-container 元素,它被渲染为一个 comment,所以输出就成为 template bindings={}

操控 Views

ViewContainer 提供了一系列便捷的 API 来操作 View。

class ViewContainerRef {
...
clear() : void
insert(viewRef: ViewRef, index?: number) : ViewRef
get(index: number) : ViewRef
indexOf(viewRef: ViewRef) : number
detach(index?: number) : ViewRef
move(viewRef: ViewRef, currentIndex: number) : ViewRef
}

前面我们已经看到过,如何手工创建两种类型的 View,分别是通过 <template> 和 Component。一旦创建了 View,我们就可以使用 insert() 方法将它们插入到容器中。下面是使用 <template> 创建嵌入的 View,并插入到使用 ng-container 元素指定的特定位置。

@Component({
selector: 'sample',
template: `
<span>I am first span</span>
<ng-container #vc></ng-container>
<span>I am last span</span>
<ng-template #tpl>
<span>I am span in template</span>
</ng-template>
`
})
export class SampleComponent implements AfterViewInit {
@ViewChild("vc", {read: ViewContainerRef}) vc: ViewContainerRef;
@ViewChild("tpl") tpl: TemplateRef<any>; ngAfterViewInit() {
let view = this.tpl.createEmbeddedView(null);
this.vc.insert(view);
}
}

从 DOM 中删除 View,也就是从 ViewContainer 中删除 View,可以使用 detach() 方法。所有其它方法都是自解释的,可以通过下标获得相关的 View,将 View 移动到其它位置,或者删除 Container 中所有的 View。

创建 View

ViewContainer 还提供了一个自动创建 View 的 API

class ViewContainerRef {
element: ElementRef
length: number createComponent(componentFactory...): ComponentRef<C>
createEmbeddedView(templateRef...): EmbeddedViewRef<C>
...
}

它们是上面手工创建方式的简单封装。通过 Template 或者 Component 创建 View,并插入到特定的位置。

ngTemplateOutlet 和 ngComponentOutlet

尽管理解底层是如何工作的很重要,通常期望的使用方式总是简单。有两个指令实现快捷操作

  • ngTemplateOutlet
  • ngComponentOutlet

非常好理解它们的作用。

ngTemplateOutlet

它将一个 DOM 元素标记为 ViewContainer,创建 <template> 的 View 实例,并将这个 Embeded View 插入到其中,而不需要在 Component 类中显式用代码完成。这意味着,上面的示例可以重写为如下形式。

@Component({
selector: 'sample',
template: `
<span>I am first span</span>
<ng-container [ngTemplateOutlet]="tpl"></ng-container>
<span>I am last span</span>
<ng-template #tpl>
<span>I am span in template</span>
</ng-template>
`
})
export class SampleComponent {}

如你所见,我们不再需要任何实例化 View 的代码,特别方便。

ngComponentOutlet

与 ngTemplateOutlet 指令类似,该指令创建 Host View ( Component 的示例),而不是 Embeded View,你可以仿照下面的示例使用。

<ng-container *ngComponentOutlet="ColorComponent"></ng-container>

总结

对 Angular 中对通过 View 来操作 DOM,有一个清晰的概念模型非常重要。

探索使用 ViewContainerRef 的 Angular DOM 操控技术的更多相关文章

  1. XML解析之DOM解析技术案例

    Java代码: package com.xushouwei.xml; import java.io.File; import javax.xml.parsers.DocumentBuilder; im ...

  2. Angular i18n的技术分享、踩过的坑

    1.安装 npm @ngx-translate/core --save npm @ngx-translate/http-loader 2.配置(文本背景部分为该模块新增的)~app.module.ts ...

  3. 流量操控技术---rinetd

    应用场景 实验机器:monomall防火墙,windows xp ,kali , windows 2003 场景假设,公司对你的办公电脑做了限制只允许53端口出去不能访问互联网. 突破思路:见上图 下 ...

  4. angular Dom属性绑定

  5. 深入浅出DOM基础——《DOM探索之基础详解篇》学习笔记

    来源于:https://github.com/jawil/blog/issues/9 之前通过深入学习DOM的相关知识,看了慕课网DOM探索之基础详解篇这个视频(在最近看第三遍的时候,准备记录一点东西 ...

  6. 第61节:Java中的DOM和Javascript技术

    Java中的DOM和Javascript技术 DOM是一门技术,是文档对象模型.所需的文档只有标记型文档,如我们所学的html文档(文档中的所有标签都封装成为对象了) DOM: 为Document O ...

  7. DOM技术

    DOM概述 DOM:Document Object Model(文档对象模型)(DOM核心就是 文档变对象,标签也变对象,属性也变对象,反正就是把标记文档拆散) 用来将标记型对象封装成对象,并将标记型 ...

  8. angular监听dom渲染完成,判断ng-repeat循环完成

    一.前言 最近做了一个图片懒加载的小插件,功能需要dom渲染完成后,好获取那些需要懒加载的dom元素.那么问题来了,如果只是感知静态的dom用ready,onload都可以,但项目用的angular, ...

  9. [Angular] Step-By-Step Implementation of a Structural Directive - Learn ViewContainerRef

    For example we have two buttons: When we click nether one of those tow button, the modal should show ...

  10. 深入探索AngularJS(持续更新)

    数据双向绑定并不是Angular最出彩的地方.大部分对AngularJs的介绍都偏重于使用,使用的学习只是学了AngularJs的API,而那只能AngularJs的很小一部分.随着使用越来越深,系统 ...

随机推荐

  1. Redis 发布订阅模式

    概述 Redis 的发布/订阅是一种消息通信模式:发送者(Pub)向频道(Channel)发送消息,订阅者(Sub)接收频道上的消息.Redis 客户端可以订阅任意数量的频道,发送者也可以向任意频道发 ...

  2. 了解final关键字在Java并发编程领域的作用吗?

    在Java并发编程领域,final关键字扮演着一个至关重要的角色.虽然很多同学熟悉final用于修饰变量.方法和类的基本用法,但其在并发环境中的应用和原理却常常被忽视.final关键字不仅仅是一个简单 ...

  3. NICE与静态优先级的关系

    在Linux系统中,nice值和静态优先级用于控制进程调度的优先级,但它们的范围和含义有所不同.让我们详细解释一下两者的区别和联系. 1. Nice值 范围:nice值的范围是从 -20 到 19. ...

  4. iOS通知使用小结

    最近在项目开发中遇到了一个问题,首页底部菜单和底部子菜单的互动.需求是这样的,编辑状态下点击红色删除按钮,首页底部菜单移除该项,子菜单中对应项选中状态设置为未选中,典型的一对多方式.刚开始的方案是想通 ...

  5. 75.cancat是否会改变原数组

    cancat 用来链接 2 个数组,不会改变原数组 :

  6. 65.说下vue3的使用感想(说些vue3对比vue3的方便之处)

    vue3 使用了组合式API,setup 替换了选项式api ,不需要在多个api里面写代码了,而且使用了setup的语法糖,可以更加方便写代码 : vue3使用proxy替代了Object.defi ...

  7. Python之py9-py9博客情况获取

    #!/usr/bin/env python # -*- coding:utf-8 -*- import os import re import datetime import requests url ...

  8. KubeSphere 社区双周报 | KubeKey 新增网络插件 Hybridnet | 2023.08.18-08.31

    KubeSphere 社区双周报主要整理展示新增的贡献者名单和证书.新增的讲师证书以及两周内提交过 commit 的贡献者,并对近期重要的 PR 进行解析,同时还包含了线上/线下活动和布道推广等一系列 ...

  9. 基于 Python + Vue3!一个轻量级的域名和 SSL 证书监测平台!

    大家好,我是 Java陈序员. 在企业开发中,由于业务众多,涉及到很多业务域名证书,证书过期由于遗忘常常未能及时续期,导致线上访问异常,给企业带来损失! 今天,给大家介绍一个轻量级的域名和 SSL 证 ...

  10. ajax下载二进制文件(导出Excel)

    var url = 'http://127.0.0.1'; var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); // 也可以使用PO ...