阅读 831

[NGX]使用ViewContainerRef来操作Angular中的DOM

https://blog.angularindepth.com/exploring-angular-dom-abstractions-80b3ebcfc02

原文链接,墙裂推荐阅读原文,事实上这篇文章网上已有的翻译都有或多或少的错误,我这篇肯定也不例外。

Angular文档中关于使用Angular DOM的操作,总是会提到一个或几个类: ElementRef, TemplateRef, ViewContainerRef等。本文旨在描述这种模型。

在Angular中DOM被抽象出ElementRef, TemplateRef, ViewRef, ComponentRefViewContainerRef

@ViewChild | @ViewChildren

在深入这些DOM抽象之前,我们先了解一下如何在组件/指令类中访问这些DOM抽象。Angular提供了一个DOM查询机制。这个机制由@ViewChild@ViewChildren两个装饰器完成。他俩的使用方法是一毛一样的,只是返回结果有所不同,显而易见,后折返回一个列表,前者只返回一个引用。

通常情况下,这些装饰器都和模板引用标识(template reference variable)同时出现,模板引用标识(template reference variable)是一个在模板文件里给dom元素命名的东西,你也可以把它理解成类似于dom元素的id的存在。给一个元素增加一个引用标识,你就可以通过这两个装饰器来获取它们。

@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]});第二个参数并不是必须的,假如他是一个简单的dom元素,类似span这样的,angular会推断为ElementRef。如果是一个template元素,则会推断为TemplateRef。当然,也有一些类型比如ViewContainerRef是不能被推断出来的,需要手动声明在read的值中,Others, like ViewRef cannot be returned from the DOM and have to be constructed manually.

ok,现在我们知道怎么执行dom查询了,我们来开始深入这些dom抽象吧~

ElementRef

ElementRef可以说是坠基本的dom抽象了。

class ElementRef<T> {
    constructor(nativeElement: T)
    nativeElement: T
}
复制代码

这个类中只包含了与之关联的原生元素,通过它你可以很轻松的查找到dom元素。

console.log(this.tref.nativeElement.textContent);

但是Angular并不推荐这种直接dom元素进行操作的方法,不光是安全原因,更是因为这样做会使得一套代码多平台运行的原则受到了打破,它使得应用和渲染层紧密耦合。我嚼的这并不是因为使用nativeElement导致的,而是因为使用了textContent这样的DOM API导致的。事实上Angular所实现的DOM操作模型几乎没有用到这么底层的dom访问。

对任何dom元素使用@ViewChild装饰器都能返回ElementRef。因为所有的组件其实都是被host在普通的DOM元素上,所有的指令都是作用于DOM元素,所以借助于Angular的依赖注入机制,所有的组件/指令类可以获取它们的**宿主元素(host element)**的ElementRef。方法如下:

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

所以既然一个组件可以通过DI机制轻松的获取它的宿主元素,@ViewChild装饰器一般就用来获取模板中的一个子元素了。对指令而言则是完全相反的,它们没有视图、没有子元素,所以它们通常与他们所依附的元素一起工作。

TemplateRef

模板的概念对于广大web开发者而言可以说是见的多了。它是在应用中能被多次复用的一个DOM元素的集合。在HTML5将template便签列入标准之前,多数模板都是通过script标签包裹的方式实现的。这种实现方式不是本文的重点,所以我们按住不表。

我们来讲讲template标签,作为HTML5新增加的标签,浏览器会解析这个标签并生成对应的DOM元素,但是并不会直接渲染到页面上。通过template元素的content属性我们就可以获取到这个DOM元素。

<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在渲染过程中移除了template标签。并在其位置插入了一条注释,

<sample>
    <!--template bindings={}-->
</sample>
复制代码

下面是Angular文档中对TemplateRef类的描述 TemplateRef类的数据结构如下:

class TemplateRef<C> {
    get elementRef: ElementRef
    createEmbeddedView(context: C): EmbeddedViewRef<C>
}
复制代码

The location in the View where the Embedded View logically belongs to.

他有一个属性elementRef: ElementRef,代表了这个内嵌视图在view中所属的位置,也就是ng-template标签上的模板引用标识获取的ElementRef。

The data-binding and injection contexts of Embedded Views created from this TemplateRef inherit from the contexts of this location.

Typically new Embedded Views are attached to the View Container of this location, but in advanced use-cases, the View can be attached to a different container while keeping the data-binding and injection context from the original location.

他有一个方法createEmbeddedView()可以创建一个内嵌视图并将其驻留在一个视图容器上,同时还能返回一个对视图的引用:ViewRef。

ViewRef

ViewRef正是对Angular中最基本的UI构成-View的抽象。他是一系列元素的最小集合,被同时创建出来又被同时销毁。Angular高度鼓励开发者们将UI视为一系列View的组成,而不是一个个html标签组成的树。

Angular有且仅有两种视图类型:内嵌视图(Embedded Views)宿主视图(Host Views)。通常情况下,内嵌视图往往跟Template相关联,而宿主视图与组件相关联。

创建一个内嵌视图

TemplateRef中的方法createEmbeddedView()方法可以直接创建出一个内嵌视图,该方法返回的类型正是ViewRef;

ngAfterViewInit() {
    let view = this.tpl.createEmbeddedView(null);
}
复制代码

创建一个宿主视图

当一个组件被动态生成时,宿主视图也就随之被创建出来了。通过ComponentFactoryResolver类你可以轻松的动态创建出一个组件。

constructor(
    private _injector: Injector,
    private _r: ComponentFactoryResolver,
) {
    let factory = this._r.resolveComponentFactory(aComponent);
    let componentRef = factory.create(this._injector);
    let view = componentRef.hostView;
}
复制代码

这里薛薇的解释一下以上代码,在Angular中每个组件都和一个**注入器(Injector)**的实例绑定的,因此当我们动态创建一个组件的时候,我们会把当前的注入器实例传递进create()方法中。当然,除了上述代码,如果你想要获取一个组件的组件工厂,你需要在当前模块的entryComponents中声明这个组件。

下面是Angular官网中对VIewRef类的一点描述:

class ViewRef extends ChangeDetectorRef {
    get destroyed: boolean
    destroy(): void
    onDestroy(callback: Function): any

    // 自ChangeDetectorRef继承来的属性方法
    markForCheck(): void
    detach(): void
    detectChanges(): void
    checkNoChanges(): void
    reattach(): void
}
复制代码

好的,现在我们知道了如何创建内嵌视图和宿主视图。当这些视图创建完毕之后我们就可以通过ViewContainer将他们插入到DOM中。

ViewContainerRef

首先要声明的是,任何DOM元素都可以被用作为视图容器。英催思挺的是Angular不会在元素内部插入视图,而是把这些视图添加到绑定到ViewContainer的元素后面。这跟router-outlet插入组件的方法灰常相似。

通常情况下,一个坠佳的创建ViewContainer的地方是<ng-container></ng-container>标签。最终它会被渲染为一行注释而不会在dom中引入冗余的元素。

@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 one line of comment
        console.log(this.vc.element.nativeElement.textContent);
    }
}
复制代码

Angular文档中对于ViewContainerRef类的描述如下:

class ViewContainerRef {
    get element: ElementRef
    get injector: Injector
    get parentInjector: Injector
    get length: number
    clear(): void
    get(index: number): ViewRef | null
    createEmbeddedView<C>(templateRef: TemplateRef<C>, context?: C, index?: number): EmbeddedViewRef<C>
    createComponent<C>(componentFactory: ComponentFactory<C>, index?: number, injector?: Injector, projectableNodes?: any[][], ngModule?: NgModuleRef<any>): ComponentRef<C>
    insert(viewRef: ViewRef, index?: number): ViewRef
    move(viewRef: ViewRef, currentIndex: number): ViewRef
    indexOf(viewRef: ViewRef): number
    remove(index?: number): void
    detach(index?: number): ViewRef | null
}
复制代码

操作视图

前面我们已经知道了两种视图类型是如何从template和component创建出来的,一旦我们有了view,就可以通过viewContainer的insert()方法将其插入到dom中。

import {
  AfterViewInit,
  Component,
  TemplateRef,
  ViewChild,
  ViewContainerRef
} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <span>firsr para</span>
    <ng-container #vc></ng-container>
    <span>second para</span>
    <ng-template #tpl>
      <span>the para in template</span>
    </ng-template>
  `
})
export class AppComponent implements AfterViewInit {
  @ViewChild('vc', {read: ViewContainerRef}) viewContainer: ViewContainerRef;
  @ViewChild('tpl') tpl: TemplateRef<any>;

  ngAfterViewInit() {
    const tp_view = this.tpl.createEmbeddedView(null);
    this.viewContainer.insert(tp_view);
    set
  }
}
// 输出
// <span>firsr para</span>
// <!---->
// <span>the para in template</span>
// <span>second para</span>
// <!---->
复制代码

要移除这个被插入的元素,只要调用viewContainer的detach()方法。所有其他方法都是自解释性的,可用于获取索引视图的引用,将视图移到另一个位置,或者从容器中删除所有视图。

创建视图

createEmbeddedView<C>(templateRef: TemplateRef<C>, context?: C, index?: number): EmbeddedViewRef<C>
createComponent<C>(componentFactory: ComponentFactory<C>, index?: number, injector?: Injector, 
复制代码

这两个方法相当于对我们上面的代码进行了一层封装,他会从template或component中创建一个view出来并插入到dom中相应的位置。

总结

现在看起来似乎有很多概念需要去理解吸收。但是实际上这些概念的调条理都十分清晰,并且这些概念组成了一个十分清晰的视图操作DOM的模型。

通过@ViewChild和模板引用标识符你可以获取到Angular DOM抽象的引用。围绕DOM元素最简单的包裹是ElementRef。

对于模板(template)而言,你可以通过TemplateRef来创建一个内嵌视图(Embedded View);宿主视图则可以通过ComponentFactoryResolver创建的componentRef来获取。

通过ViewContainerRef我们则可以操作这些视图

结束!哈!