探索Angular DOM操作技术

每当我读到关于Angular DOM操作的的文章时总是看到这些类被提到:ElementRef, TemplateRef,ViewContainerRef等等。不幸的是,尽管其中一些内容已在Angular文档或相关文章中介绍,但我还没找到对它们整体模型的介绍以及它们如何协同工作的示例,本文旨在描述这种模型。

新版本的Angular可以运行在不同的平台中——在浏览器中,在移动平台上或在Web worker中。因此,特定级别的抽象是需要的来链接特定平台的API和框架之间的接口。Angular的这些抽象由以下类型组成:ElementRef, TemplateRef, ViewRef, ComponentRef and ViewContainerRef,在本文中,我们将详细研究每种引用类型,并展示如何将它们用于操作DOM。

1. @ViewChild

在探索DOM抽象之前,让我们了解如何在组件/指令类中访问这些抽象。Angular提供了一种称为DOM查询的机制。它以@ViewChild@ViewChildren装饰器的形式出现。它们的行为相同,只有前者返回一个引用,而后者返回多个引用作为QueryList对象。在本文的示例中,我将主要使用ViewChild装饰器,并且@在其之前将不使用符号。

通常,这些装饰器与模板引用变量配对使用,一个模版引用变量只是一个引用模版中DOM元素的名字。类似于HTMl元素中的id属性,你可以使用模版引用变量标记一个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作为一个模版引用名称,并且得到了一个与之相关的ElementRef,第二个参数read不一定总是需要的,因为angular可以通过DOM元素的类型来推断它的引用类型。例如,如果它是一个简单的html元素,例如span,angular返回ElementRef。如果它是一个template元素,则返回TemplateRef。有些引用(例如ViewContainerRef无法推断)必须在read参数中指定类型。其它的,例如ViewRef不能够从DOM获取,则必须手动的创建。

好的,现在我们知道了如何查询引用,让我们开始探索它们。

2. ElementRef

这是最基本的抽象。如果观察它的类结构,您将看到它仅包含与其关联的本地元素。我们可以在这里看到它对于访问本地DOM元素很有用:

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

但是,Angular团队不鼓励这种用法。它不仅带来安全风险,而且还导致应用程序和呈现层之间的紧密耦合,这使得在多个平台上运行应用程序变得困难。我认为并不是获取本地元素破坏了抽象,而是使用诸如的特定DOM API textContent破坏了抽象。但是,正如您稍后将看到的那样,在Angular中实现的DOM操作思维模型几乎不需要这种较低级别的访问。

可以使用ViewChild为任何DOM元素返回ElementRef,但是,由于所有组件都托管在自定义DOM元素中,并且所有指令都应用于​​DOM元素,因此组件和指令类可以通过依赖注入的方式获取它们的host元素的ElementRef

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

因此,尽管组件可以通过DI访问其宿主元素,但ViewChild装饰器最常用于在其视图(模板)中获取对DOM元素的引用。指令于此相反,它们没有views,它们通常直接与其附加的元素一起使用。

3. TemplateRef

大多数Web开发人员都应该熟悉模板的概念。这是一组DOM元素,可在整个应用程序的视图中重复使用。在HTML5标准引入模板标签之前,大多数模板都以具有某些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类,以便与模板一起使用。使用方法如下:

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

框架从DOM中移除template元素,并在其位置插入注释。这是呈现时的样子:

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

TemplateRef该类本身就是一个简单的类。它在elementRef属性中拥有对其宿主元素的引用,并具有一种方法createEmbeddedView。然而此方法非常有用,因为它允许我们创建视图并将其引用返回为ViewRef

4. ViewRef

这种类型的抽象表示Angular视图。在Angular世界中,视图是应用程序UI的基本构建块。它是一起创建和销毁的最小元素组。Angular哲学鼓励开发人员将UI视为视图的组合,而不是独立的html标签树。

Angular支持两种类型的视图:

  • 链接到模版的内嵌视图
  • 链接到组件的宿主视图

4.1 创建内嵌视图

模板只是保存视图的蓝图。可以使用上述createEmbeddedView方法从模板实例化视图:

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

4.2 创建宿主视图

宿主视图是在动态实例化组件时创建的。可以使用ComponentFactoryResolver方法动态创建组件:

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

在Angular中,每个组件都绑定到特定的注入器实例,因此在创建组件时我们将传递当前的注入器实例。另外,不要忘记必须将动态实例化的组件添加到模块或宿主组件的EntryComponents中。

所以,我们已经了解了如何创建嵌入式视图和宿主视图。创建视图后,可以使用将其插入DOM ViewContainer。下一节将探讨其功能。

5. ViewContainerRef

表示一个可以附加一个或多个视图的容器。

这里首先要提到的是,任何DOM元素都可以用作视图容器。有趣的是Angular不会在元素内插入视图,而是在绑定到ViewContainer的元素后追加视图.这类似于router-outlet插入组件的方式。

通常ng-container元素是一个好的选择去创建一个容器,它呈现为注释,因此不会在DOM中引入多余的html元素。这是一个在组件模板中特定位置创建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元素,并被渲染为一个注释,因此输出是:template bindings={}

5.1 操作视图

ViewContainer提供了用于处理视图的便捷API:

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
}

前面我们已经看到了如何从模板和组件手动创建两种视图。一旦有了视图,就可以使用insert方法将其插入DOM 。以下是从模板创建内嵌视图并将其插入到由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);
    }
}

通过此实现,结果html如下所示:

<sample>
    <span>I am first span</span>
    <!--template bindings={}-->
    <span>I am span in template</span>

    <span>I am last span</span>
    <!--template bindings={}-->
</sample>

要从DOM中删除视图,我们可以使用detach方法。所有其他方法都是类似的,可通过索引获取对视图的引用,将视图移动到另一个位置或从容器中删除所有视图。

5.2 创建视图

ViewContainer 还提供API以自动创建视图:

class ViewContainerRef {
    element: ElementRef
    length: number

    createComponent(componentFactory...): ComponentRef<C>
    createEmbeddedView(templateRef...): EmbeddedViewRef<C>
    ...
}

这些只是我们上面手动创建视图方法的便捷包装。他们从模板或组件创建视图,并将其插入指定的位置。

6. ngTemplateOutlet 和 ngComponentOutlet

虽然了解底层机制的工作原理总是很不错的,但是通常希望有某种捷径。此快捷方式以两种指令的形式出现:ngTemplateOutletngComponentOutlet。并且ngComponentOutlet将从版本4开始可用。但是,如果您已阅读上面的所有内容,将很容易理解它们的作用。

6.1 ngTemplateOutlet

这个标记将一个DOM元素标记为ViewContainer,并且通过模版创建一个内嵌视图并插入其中,而不需要在组件类中手动去这些,这意味着上面的示例创建了一个视图并将其插入到#vcDOM元素中,可以像这样重写:

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

如您所见,我们在组件类中不使用任何视图实例化代码。非常便利。

6.2 ngComponentOutlet

该指令类似于ngTemplateOutlet它,它创建一个宿主视图(实例化一个组件),而不是一个嵌入式视图。您可以像这样使用它:

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

7. 总结

现在,所有这些信息似乎都有很多需要消化的地方,但是实际上,它们非常连贯,并为通过视图操作DOM奠定了清晰的思维模型。你可以通过ViewChild查询模版引用变量来获得对Angular DOM的抽象,最简单的对DOM元素的包装就是ElementRef,对于模版可以使用TemplateRef来创建内嵌视图,可以通过ComponentFactoryResolver来创建一个componentRef,并且通过componentRef来获取它的宿主视图,可以通过ViewContainerRef来操作视图。有两种指令可以避免到手动创建的麻烦:ngTemplateOutlet——创建内嵌视图,ngComponentOutlet——创建宿主视图(动态组件)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值