【转】使用ViewContainerRef探索Angular DOM操作技术

来自:http://blog.giscafer.com/2017/10/21/exploring-angular-dom-abstractions/

原文

每当我阅读关于在Angular中使用DOM时,总会看到其中提到的一个或几个类:ElementRef,TemplateRef,ViewContainerRef以及一些其他的,不幸的是,尽管其中的一些被包含在Angular文档或相关文章中,我还没有找到整体思维模式的描述和这些如何一起工作的例子。本文旨在描述这样的模型。

如果你有angular.js的开发经验,你会觉得操作DOM是件非常容易的事情,Angular 注入Element 到 link 函数中,你就可以查询组件模板中的任何节点,添加、删除及修改样式等操作。然而,这种方法有一个主要的缺点 - 它紧紧地绑定到一个浏览器平台上。

新的Angular版本运行在不同的平台上———浏览器,移动平台或者在一个WEB worker 中。因此,站在平台特定的API和框架接口之间需要抽象层次。在Angular上,这些抽象来自以下参考类型的形式:
ElementRef,TemplateRef,ViewContainerRef,ComponentRef,ViewContainerRef,在本文中,我们将详细介绍每种引用类型,并展示如何使用它们来操作DOM。

@ViewChild

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

通常,这些装饰器与模板引用变量一起工作。模板引用变量(template reference variable) 仅仅是模板中的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 decorator 的基本语法如下:

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

在这个示例中,您可以看到,我将 tref 指定为html 中的模板引用名称,并接收与此元素关联的
ElementRef 。第二个参数 read 并不总是必需的,因为 Angular 可以通过DOM元素的类型推断引用类型。例如,如果它是一个简单的 html 元素,比如 span,Angular 返回 ElementRef。如果它是一个 template 模板,它将返回 TemplateRef 。一些引用,如 ViewContainerRef 不能被推断,并且必须在
read 参数中被声明。其他的,如 ViewRef 不能从 DOM 接收返回,必须手动构造。

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

ElementRef

这是最基本的抽象概念。如果您观察它的类结构,您将看到它只包含与之关联的原生元素(native element)。它对于访问原生DOM元素非常有用,正如我们在这里看到的:

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

然而,这种用法却被 Angular 团队 所劝阻。它不仅会带来安全风险,而且还会在应用程序和呈现层之间产生紧密耦合,使得在多个平台上运行应用程序变得困难。我认为,它不是访问 nativeElement 来打破抽象,而是使用特定的DOM API,比如 textContent 。但是,稍后您将看到,在 Angular 上实现的DOM操作思想模型几乎不需要这样一个较低级别的访问。

ElementRef 可以通过使用 ViewChild decorator作为任何 DOM元素被返回 。但是由于所有组件都驻留在一个自定义DOM元素中,并且所有的指令都被应用于DOM元素,组件和指令类可以通过DI机制(依赖注入机制)获得与它们的宿主元素(host element)相关联的元素的实例:

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

因此,虽然组件可以通过DI访问它的宿主元素,但 ViewChild decorator 通常会在其视图(模板)(view (template))中获得对DOM元素的引用。指令的副作用——他们没有任何视图模板(views),他们通常直接与他们所依附的元素一起工作。

TemplateRef

对于大多数web开发人员来说,模板的概念应该是熟悉的。模板是一组DOM元素,在应用程序的视图中可以重用。在HTML5标准引入模板标签template之前,大多数模板都是在一个带有一些 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>
<template id="tpl">
    <span>I am span in template</span>
</template>

Angular 拥抱HTML5的这种方法并实现 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中删除模板元素,并在其位置插入注释。这就是呈现时的样子:

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

通过它本身, TemplateRef 类是一个简单的类。它在 elementRef 属性中引用它的宿主元素,并有一个createEmbeddedView 方法。但是,这个方法非常有用,因为它允许我们创建一个视图并返回一个引用作为 ViewRef。

ViewRef

ViewRef 表示一个Angular 视图。在 Angular 框架中,视图(View)是应用程序UI的基本构件。它是构成和毁灭在一起的最小元素组合。Angular 鼓励开发人员将UI看作是视图的组成,而不是独立的html标记树。

Angular 支持两种视图:

  • Embedded Views which are linked to a Template (连接到模板的嵌入视图)
  • Host Views which are linked to a Component (连接到组件的宿主视图)

Creating embedded view (创建嵌入视图)

模板仅包含视图的蓝图。可以使用前面提到的 createEmbeddedView 方法从模板中实例化一个视图:

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

Creating 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 中,每个组件都被绑定到一个注入器(injector)的特定实例,因此我们在创建组件时传递当前的注入器实例。另外,不要忘记必须将动态实例化的组件添加到模块或托管组件的 EntryComponents
中。

因此,我们已经看到了如何创建嵌入式视图和宿主视图。一旦创建了视图,就可以使用 ViewContainer
将其插入到DOM中。下一节将探讨其功能。

ViewContainerRef

表示一个容器,其中可以附加一个或多个视图。

这里要提到的第一件事是,任何DOM元素都可以用作视图容器。有趣的是,Angular 在元素内部没有插入视图,而是在元素绑定到 ViewContainer 之后附加它们。这类似于 router-outlet 插入组件。

通常,一个好的候选对象可以标记一个 ViewContainer 应该被创建的位置,它是 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={} 。

Manipulating views (操作视图)

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方法将它 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>
        <template #tpl>
            <span>I am span in template</span>
        </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方法。所有其他方法都是自解释性的,可用于获取索引视图的引用,将视图移到另一个位置,或者从容器中删除所有视图。

Creating Views (创建视图)

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

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

这些都是我们在上面手工完成的简单方便的包装。它们从模板或组件创建视图,并将其插入指定的位置。

ngTemplateOutlet 和 ngComponentOutlet

虽然知道底层机制是如何工作的总是很好,但通常都希望有某种快捷方式。此快捷方式以两种指令形式出现: ngTemplateOutlet 和 ngComponentOutlet 。在撰写本文时,两者都是实验性的,ngComponentOutlet 将在版本4中可用(angular4+已可以随意使用)。但如果你已经读过上面所有的内容,就很容易理解它们的作用。

ngTemplateOutlet

它将DOM元素标记为 ViewContainer ,并在其中插入一个由模板创建的嵌入视图,而不需要在组件类中显式地这样做。这意味着上面的例子中我们创建了一个视图并将其插入#vc DOM元素,可以这样重写:

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

您可以看到,我们在组件类中不使用任何实例化代码的视图。非常方便。

ngComponentOutlet

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

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

总结

现在,所有这些信息似乎都很容易消化,但实际上它是相当连贯的,并在通过视图操作DOM的过程中形成了一个清晰的理想模型。您可以通过使用 ViewChild 查询和模板变量引用来获得 Angular DOM 抽象的引用。围绕DOM元素的最简单的包装是 ElementRef 。对于模板,您有 TemplateRef,它允许您创建一个嵌入式视图。 可以通过使用 ComponentFactoryResolver创建的 componentRef 访问宿主视图。视图可以使用 ViewContainerRef 进行操作。有两种指令使手动过程变为自动化:ngTemplateOutlet ——操作嵌入视图 和 ngComponentOutlet—— 创建宿主视图(动态组件)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值