[译] 探究 Angular 运用 ViewContainerRef 操纵 DOM

原文链接:
Exploring Angular DOM manipulation techniques using ViewContainerRef

假如想深切进修 Angular 怎样运用 Renderer 和 View Containers 手艺操纵 DOM,能够查阅 YouTube 视频 my talk at NgVikings

每次我读到 Angular 怎样操纵 DOM 相干文章时,总会发明这些文章提到 ElementRefTemplateRefViewContainerRef 和其他的类。只管这些类在 Angular 官方文档或相干文章会有触及,然则很少会去形貌团体思绪,这些类怎样一同作用的相干示例也很少,而本文就重要形貌这些内容。

假如你来自于 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 为例,而且形貌时省略 @

一般这两个装潢器与模板援用变量(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 装潢器基础语法是:

@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 元素中,所以必需手动在组织函数中编码组织出来。

如今,让我们看看应当怎样猎取这些援用,一同去探究吧。

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 运用动态组件建立宿主视图。

    原文作者:lx1036
    原文地址: https://segmentfault.com/a/1190000013860896
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞