[译] 探索 Angular 使用 ViewContainerRef 操做 DOM

原文连接:Exploring Angular DOM manipulation techniques using ViewContainerRef html

若是想深刻学习 Angular 如何使用 Renderer 和 View Containers 技术操做 DOM,能够查阅 YouTube 视频 my talk at NgVikingsnode

每次我读到 Angular 如何操做 DOM 相关文章时,总会发现这些文章提到 ElementRefTemplateRefViewContainerRef 和其余的类。尽管这些类在 Angular 官方文档或相关文章会有涉及,可是不多会去描述总体思路,这些类如何一块儿做用的相关示例也不多,而本文就主要描述这些内容。api

若是你来自于 angular.js 世界,很容易明白如何使用 angular.js 操做 DOM。angular.js 会在 link 函数中注入 DOM element,你能够在组件模板里查询任何节点(node),添加或删除节点(node),修改样式(styles),等等。然而这种方式有个主要缺陷:与浏览器平台紧耦合浏览器

新版本 Angular 须要在不一样平台上运行,如 Browser 平台,Mobile 平台或者 Web Worker 平台,因此,就须要在特定平台的 API 和框架接口之间进行一层抽象(abstraction)。Angular 中的这层抽象就包括这些引用类型:ElementRefTemplateRefViewRefComponentRefViewContainerRef。本文将详细讲解每个引用类型(reference type)和该引用类型如何操做 DOM。安全

@ViewChild

在探索 DOM 抽象类前,先了解下如何在组件/指令中获取这些抽象类。Angular 提供了一种叫作 DOM Query 的技术,主要来源于 @ViewChild@ViewChildren 装饰器(decorators)。二者基本功能相同,惟一区别是 @ViewChild 返回单个引用,@ViewChildren 返回由 QueryList 对象包装好的多个引用。本文示例中主要以 ViewChild 为例,而且描述时省略 @bash

一般这两个装饰器与模板引用变量(template reference variable)一块儿使用,模板引用变量仅仅是对模板(template)内 DOM 元素命名式引用(a named reference),相似于 html 元素的 id 属性。你可使用模板引用(template reference)来标记一个 DOM 元素,并在组件/指令类中使用 ViewChild 装饰器查询(query)它,好比:框架

@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 装饰器基本语法是:dom

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

上例中你能够看到,我把 tref 做为模板引用名称,并将 ElementRef 与该元素联系起来。第二个参数 read 是可选的,由于 Angular 会根据 DOM 元素的类型推断出该引用类型。例如,若是它(#tref)挂载的是相似 span 的简单 html 元素,Angular 返回 ElementRef;若是它挂载的是 template 元素,Angular 返回 TemplateRef。一些引用类型如 ViewContainerRef 就不能够被 Angular 推断出来,因此必须在 read 参数中显式申明。其余的如 ViewRef 不能够挂载在 DOM 元素中,因此必须手动在构造函数中编码构造出来。ide

如今,让咱们看看应该如何获取这些引用,一块儿去探索吧。函数

ElementRef

这是最基本的抽象类,若是你查看它的类结构,就发现它只包含所挂载的元素对象,这对访问原生 DOM 元素颇有用,好比:

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

然而,Angular 团队不鼓励这种写法,不但由于这种方式会暴露安全风险,并且还会让你的程序与渲染层(rendering layers)紧耦合,这样就很难在多平台运行你的程序。我认为这个问题并非使用 nativeElement 而是使用特定的 DOM API 形成的,如 textContent。可是后文你会看到,Angular 实现了操做 DOM 的总体思路模型,这样就再也不须要低阶 API,如 textContent

使用 ViewChild装饰的 DOM 元素会返回 ElementRef,可是因为全部组件挂载于自定义 DOM 元素,全部指令做用于 DOM 元素,因此组件和指令均可以经过 DI(Dependency Injection)获取宿主元素的ElementRef 对象。好比:

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

因此组件经过 DI(Dependency Injection)能够访问到它的宿主元素,但 ViewChild 装饰器常常被用来获取模板视图中的 DOM 元素。然而指令却相反,由于指令没有视图模板,因此主要用来获取指令挂载的宿主元素。

TemplateRef

对于大部分开发者来讲,模板概念很熟悉,就是跨程序视图内一堆 DOM 元素的组合。在 HTML5 引入 template 标签前,浏览器经过在 script 标签内设置 type 属性来引入模板,好比:

<script id="tpl" type="text/template">
  <span>I am span in template</span>
</script>
复制代码

这种方式不只有语义缺陷,还须要手动建立 DOM 模型,然而经过 template 标签,浏览器能够解析 html 并建立 DOM 树,但不会渲染它,该 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 采用 template 标签这种方式,实现了 TemplateRef 抽象类来和 template 标签一块儿合做,看看它是如何使用的(译者注: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);
    }
}
复制代码

Angular 框架从 DOM 中移除 template 元素,并在其位置插入注释,这是渲染后的样子:

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

TemplateRef 是一个结构简单的抽象类,它的 elementRef 属性是对其宿主元素的引用,还有一个 createEmbeddedView 方法。然而 createEmbeddedView 方法颇有用,由于它能够建立一个视图(view)并返回该视图的引用对象 ViewRef

ViewRef

该抽象表示一个 Angular 视图(View),在 Angular 世界里,视图(View)是一堆元素的组合,一块儿被建立和销毁,是构建程序 UI 的基石。Angular 鼓励开发者把 UI 做为一堆视图(View)的组合,而不只仅是 html 标签组成的树。

Angular 支持两种类型视图:

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

建立嵌入视图

模板仅仅是视图的蓝图,能够经过以前提到的 createEmbeddedView 方法建立视图,好比:

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;
}
复制代码

在 Angular 中,每个组件绑定着一个注入器(Injector)实例,因此建立 ColorComponent 组件时传入当前组件(即 SampleComponent)的注入器。另外,别忘了,动态建立组件时须要在模块(module)或宿主组件的 EntryComponents 属性添加被建立的组件。

如今,咱们已经看到嵌入视图和宿主视图是如何被建立的,一旦视图被建立,它就可使用 ViewContainer 插入 DOM 树中。下文主要探索这个功能。

ViewContainerRef

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

首先须要说的是,任何 DOM 元素均可以做为视图容器,然而有趣的是,对于绑定 ViewContainer 的 DOM 元素,Angular 不会把视图插入该元素的内部,而是追加到该元素后面,这相似于 router-outlet 插入组件的方式。

一般,比较好的方式是把 ViewContainer 绑定在 ng-container 元素上,由于 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);
    }
}
复制代码

如同其余抽象类同样,ViewContainer 经过 element 属性绑定 DOM 元素,好比上例中,绑定的是 会被渲染为注释的 ng-container 元素,因此输出也将是 template bindings={}

操做视图

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 标记的地方插入该视图(译者注:从上文中知道是追加到ng-container后面,而不是插入到该 DOM 元素内部)。

@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>
复制代码

能够经过 detach 方法从视图中移除 DOM,其余的方法能够经过方法名知道其含义,如经过索引获取视图引用对象,移动视图位置,或者从视图容器中移除全部视图。

建立视图

ViewContainer 也提供了手动建立视图 API :

class ViewContainerRef {
    element: ElementRef
    length: number

    createComponent(componentFactory...): ComponentRef<C>
    createEmbeddedView(templateRef...): EmbeddedViewRef<C>
    ...
}
复制代码

上面两个方法是个很好的封装,能够传入模板引用对象或组件工厂对象来建立视图,并将该视图插入视图容器中特定位置。

ngTemplateOutlet 和 ngComponentOutlet

尽管知道 Angular 操做 DOM 的内部机制是好事,可是要是有某种快捷方式就更好了啊。没错,Angular 提供了两种快捷指令:ngTemplateOutletngComponentOutlet。写做本文时这两个指令都是实验性的,ngComponentOutlet 也将在版本 4 中可用(译者注:如今版本 5.* 也是实验性的,也均可用)。若是你读完了上文,就很容易知道这两个指令是作什么的。

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 很类似,区别是 ngComponentOutlet 建立的是由组件实例化生成的宿主视图,不是嵌入视图。你能够这么使用:

<ng-container *ngComponentOutlet="ColorComponent"></ng-container> 复制代码

总结

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

相关文章
相关标签/搜索