4

每当我阅读中遇到,关于Angular中使用DOM的内容时,总会看到一个或几个这样的类:ElementRef,TemplateRef,ViewContainerRef等等。 不幸的是,虽然其中的一些被Angular文档或相关文章所讲述,但是我还没有找到完整的描述以及这些它们是如何工作的。

如果你来自angular.js世界,那么你知道操纵DOM是相当容易的。Angular注入DOM elementRef到构造函数中,你可以查询组件模板中的任何节点,添加或删除子节点,修改样式等。但是,这种方法有一个主要的缺点 - 它紧紧地绑定到浏览器平台。

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

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

在这个例子中,你可以看到我在html中指定了tref作为模板引用名,并且接收到与这个元素相关的ElementRef。 读取的第二个参数并不总是必需的,因为Angular可以通过DOM元素的类型来推断引用类型。 例如,如果它是一个简单的HTML元素(如span),那么angular将返回ElementRef。 如果它是一个模板元素,它将返回TemplateRef。不过一些引用,如ViewContainerRef不能被推断,并且必须在读参数中特别要求。 其他的,像ViewRef不能从DOM返回,必须手动构造。

ElementRef

这是最基本的抽象。 如果你观察它的类结构,你会发现它只保存了它所关联的本地元素。 对于访问本地DOM元素非常有用,我们可以在这里看到:

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

不过,Angular团队不鼓励这种用法。 这不仅会带来安全风险,还会在应用程序和渲染层之间造成紧密耦合,这使得在多个平台上运行应用程序变得困难。 我相信这不是对nativeElement的访问,而是打破了抽象,而是像textContent一样使用特定的DOM API。 但是后面你会看到,在Angular中实现的DOM操作心智模型几乎不需要这样一个较低级别的访问。

可以使用ViewChild装饰器为任何DOM元素返回ElementRef。 但是,由于所有组件都驻留在自定义DOM元素中,并且所有指令都应用于DOM元素,因此组件和指令类可以通过DI机制获取与其主机元素关联的ElementRef实例:

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

因此,虽然组件可以通过DI访问其主机元素,但ViewChild装饰器通常用于在其视图(模板)中获取对DOM元素的引用。 反之亦然,指令没有视图,他们通常直接与他们所附的元素。

模板的概念应该是大多数Web开发人员熟悉的。 这是一组DOM元素,在整个应用程序的视图中被重用。 在HTML5标准引入了模板标签之前,大多数模板都被包含在script标签中。

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

这种方法当然有许多缺点,如语义和手动创建DOM模型的必要性。 使用模板标签浏览器解析HTML并创建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类来处理模板。 以下是如何使用它:

@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

这种抽象表示Angular视图。 在Angular世界中,View是应用程序UI的基本构建块。 它是创造和消灭的最小的元素分组。 Angular哲学鼓励开发人员将UI视为Views的组合,而不是将其视为独立的HTML标签。

Angular支持两种类型的视图:

  • 嵌入视图链接到模板
  • 链接到组件的主机视图

创建嵌入的视图

一个模板只是一个视图的蓝图。 一个视图可以使用前面提到的createEmbeddedView方法从模板实例化,如下所示:

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

创建宿主视图

宿主视图是在组件动态实例化时创建的。 可以使用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。

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

ViewContainerRef

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

首先要提到的是,任何DOM元素都可以用作视图容器。有趣的是,Angular不在元素内插入视图,而是在绑定到ViewContainer的元素之后附加它们。 这与路由器插座如何插入组件类似。

通常,标记应该创建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绑定到通过元素属性访问的特定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
}

我们之前已经看到,如何从模板和组件手动创建两种类型的视图。 一旦我们有了一个视图,我们可以使用插入方法将其插入到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方法。 所有其他方法都是自解释性的,可用于通过索引获取对视图的引用,将视图移至其他位置或从容器中移除所有视图。

Creating Views

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

class ViewContainerRef {
    element: ElementRef
    length: number

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

这些都是我们上面手动完成的简单包装。 他们从模板或组件创建一个视图,并将其插入到指定位置。

ngTemplateOutlet and ngComponentOutlet

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>
        <ng-template #tpl>
            <span>I am span in template</span>
        </ng-template>
    `
})
export class SampleComponent {}

正如你所看到的,我们不使用任何视图实例化组件类中的代码。 非常便利。

ngComponentOutlet

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

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

总结

现在,所有这些信息似乎都可以被消化,但实际上这些信息是非常连贯的,并且通过视图来显示操纵DOM的清晰模型。 通过使用ViewChild查询和模板变量引用,您可以获得对Angular DOM抽象的引用。 围绕DOM元素的最简单的包装是ElementRef。 对于具有TemplateRef的模板,您可以创建嵌入式视图。 主机视图可以在使用ComponentFactoryResolver创建的componentRef上访问。 视图可以用ViewContainerRef来操作。 有两个使自动手动过程的指令:ngTemplateOutlet - 用于嵌入视图,ngComponentOutlet用于宿主视图(动态组件)。


xthought
339 声望12 粉丝

引用和评论

0 条评论