认识angular中对dom的操作 ViewChild ElementRef等

467 阅读5分钟

了解一个问题要从:是什么,为什么,怎么做。逐步深入去考虑

基础知识

什么是dom

参见MDN

为什么要操作dom(应用场景)

搜索相关文章,只有 “ 为什么减少操作dom ”,之类的文章,但是没有说,我们在开发中为什么需要操作dom

这里操作dom说的是,在使用框架的前提下,为什么需要操作dom(不用框架,用原生或JQ,就是直接和dom打交道的)

但是,有了框架构建的声明式编程范式的前提下,我们为什么有时候还需要操作dom,我个人理解的是:

怎么做?

正文开始

浅析angular中对dom的操作

先上总结

个人认为,看着总结,脑子里有个整体观念,再去具体看问题,会清晰一些

  • ViewChild 获取ng的DOM的抽象类(区分虚拟dom)
  • ElementRef
  • 模板:TemplentRef创建嵌入视图
  • 组件:ComponentRef创建宿主视图

看似有很多新知识需要消化啊,但实际上 Angular 通过视图操作 DOM 的思路模型是很清晰和连贯的。你可以使用 ViewChild 查询模板引用变量来获得 Angular DOM 抽象类。DOM 元素的最简单封装是 ElementRef;而对于模板,你可以使用 TemplateRef 来创建嵌入视图;而对于组件,可以使用 ComponentRef 来创建宿主视图,同时又可以使用 ComponentFactoryResolver 创建 ComponentRef。这两个创建的视图(即嵌入视图和宿主视图)又会被 ViewContainerRef 管理。最后,Angular 又提供了两个快捷指令自动化这个过程:ngTemplateOutlet 指令使用模板创建嵌入视图;ngComponentOutlet 使用动态组件创建宿主视图。

@ViewChild

Angular 提供了一种叫做 DOM Query 的技术

@ViewChild: 返回单个引用,在视图的 DOM 中查找能匹配上该选择器的第一个元素或指令。

@ViewChildren :返回由 QueryList 对象包装好的多个引用,在视图的 DOM 中查找能匹配上该选择器的所有元素或指令。

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

使用 ViewChild装饰的 DOM 元素会返回 ElementRef

@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);
    }
}

他就是相当于拿到了这个dom,这个东西,好像是包含着原生dom对象

DOM Query 的技术查找出来的对象分为三类:

  • ElementRef:如果它挂载的是类似 span 的简单 html 元素;
  • TemplateRef: 如果它挂载的是 template 元素;
  • ViewContainerRef: 无法推断,一般要程序员要在read中指明,任何DOM元素都可以被用作为视图容器。

ElementRef

这是最基本的抽象类,如果你查看它的类结构,就发现它只包含所挂载的元素对象

TemplateRef

ng-template 是 Angular 提供的类似于 template 原生 html 标签

@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);
    }
}

ViewRef

该抽象表示一个 Angular 视图(View)

  • 嵌入视图(Embedded View),由 Template 提供
  • 宿主视图(Host View),由 Component 提供

创建嵌入视图

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

创建宿主视图

宿主视图是在组件动态实例化时创建的,一个动态组件(dynamic component)可以通过 ComponentFactoryResolver 创建:

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

一旦视图被创建,它就可以使用 ViewContainer 插入 DOM 树中。

ViewContainerRef

视图容器就是挂载一个或多个视图的容器。

任何 DOM 元素都可以作为视图容器

比较好的方式是把 ViewContainer 绑定在 ng-container 元素上,因为 ng-container 元素会被渲染为注释,从而不会在 DOM 中引入多余的 html 元素。

@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);
    }
}

操作视图

创建视图

ngTemplateOutlet 和 ngComponentOutlet

根据一个提前备好的 TemplateRef 插入一个内嵌视图。

@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 {}

ngComponentOutlet

这个指令与 ngTemplateOutlet 很相似,区别是 ngComponentOutlet 创建的是由组件实例化生成的宿主视图,不是嵌入视图。

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

避免使用 ElementRef

允许直接访问 DOM 会导致你的应用在 XSS 攻击前面更加脆弱。

应用

动态组件 为什么?

我们先明确下动态组件的使用场景,在代码运行时要动态加载组件,换成普通人话,代码需要根据具体情况(比如用户的操作,向后台请求结果)确定在某些地方加载某些组件,这些组件不是静态的(不是固定的)。

再比如

动态弹出框,弹出的组件是不确定的、不断更新的,这里那里弹出个购买框,那那那又需要弹出样式选择框,静态组件结构模板是不能满足群众日渐增长的需求。

怎么做?

Q 动态组件放在哪

A 放在锚点上,锚点就是DOM,这个时候就需要用到上文的 操作DOM 的相关知识了

只有ViewContainerRef才是可以将一个或多个视图附着到组件中的容器,也就是只有它才可以加载组件。

它是一个可以将新的组件作为其兄弟(节点)的DOM元素(容器),是兄弟,不是父子

获取ViewContainerRef有两种方式:

  • DOM Query查询@ViewChild获取
<ng-container #addComp></ng-container>

@ViewChild('addComp', {read: ViewContainerRef}) adComp:ViewContainerRef;
  • 官网里的例子,用依赖注入
import { Directive, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[ad-host]',
})
export class AdDirective {
  constructor(public viewContainerRef: ViewContainerRef) { }
}

锚点设置ng-template上,通过指令注入获取ViewContainerRef

template: `
            <div class="ad-banner-example">
              <h3>Advertisements</h3>
              <ng-template ad-host></ng-template>
            </div>
          `

太麻烦了,不懂啊,去看官网的弹窗组件例子吧,头晕

参考文章:juejin.cn/post/684490…

参考文章:blog.csdn.net/from_the_st…