• 探索使用 ViewContainerRef 的 Angular DOM 操控技术


    探索使用 ViewContainerRef 的 Angular DOM 操控技术

    https://indepth.dev/posts/1052/exploring-angular-dom-manipulation-techniques-using-viewcontainerref

    每当我阅读关于在 Angular 中处理 DOM 的文章的时候,我总是看到这些类型中的某些被提到:

    • ElementRef
    • TemplateRef
    • ViewContainerRef
    • ......

    不幸的是,尽管它们在 Angular 的文档中被说明了,我还是没有找到对概念模型进行全面说明的文档和示例,以及它们是如何被组合使用的。本文尝试说明 Angular 的概念模型。

    如果你正在寻找在 Angular 中使用 Renderer 和 View 来操作 DOM 的深入讨论,请查阅 my talk at NgVikings。或者阅读深入讨论动态 DOM 操控的文章 Working with DOM in Angular: unexpected consequences and optimization techniques

    如果原来使用过 angular.js,你就会知道,处理 DOM 是非常简单的事情。Angular 将 DOM 元素 element 传递给 link() 函数,你可以查询组件模板内的任何节点,增加或者删除子节点,修改样式等等。不过,这种方式有一个重要的缺陷 - 它紧密耦合到浏览器平台上。

    新的 Angular 运行在多种平台上 - 浏览器,移动平台,或者运行在 Web worker 上。所以需要一个抽象层来从平台特定的 API 中抽象出来框架的接口。在 Angular 中,这些抽象通过这些引用类型表示:

    • ElementRef
    • TemplateRef
    • ViewRef
    • ComponentRef
    • ViewContainerRef

    在本文中,我们将深入演练这些抽象类型中每一个,并展示如何使用它们来操控 DOM。

    @ViewChild

    在开始说明这些 DOM 抽象之前,让我们先理解一下,如何在 Component/Directive 类中访问这些抽象。Angular 提供了被称为 DOM query 的机制。它使用了 @ViewChild 和 @ViewChildren 装饰器。两种的行为类似,只是前一种只返回一个引用,而后一种以 QueryList 对象的形式返回多个引用。在本文的示例中,我将主要使用 ViewChild 装饰器,并忽略这个 @ 符号。

    一般来说,这些装饰器与 template reference variables 配套使用,template reference variable 是用来在模板中简单地引用 DOM 元素的方式。你可以想象它类似于 html 元素所提供的 id 特性。使用 template reference variable 来标记一个 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 作为在 html 中的模板引用名称,并通过 ElementRef 类型关联到该元素。第 2 个参数 read 并不总是必须的,因为 Angular 可以通过 DOM 元素的类型推断出来引用类型。例如,如果它是一个简单的 html 元素,例如这里的 <span> 元素,Angular 就返回一个 ElementRef。如果它是一个 <template> 元素,就会返回一个 TemplateRef。有些引用,比如 ViewContainerRef 不能被推断出来,就必须在 read 参数中指定。另外,ViewRef 是不能通过 DOM 返回的,它必须手动构造出来。

    好了,现在我们知道了如何查询到这些引用,现在可以开始说明它们了。

    ElementRef

    它是最为基本的抽象了。如果你查看它的类结构,你会发现它仅仅持有它关联到的原生元素。对于访问原生 DOM 元素来说,它非常有用:

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

    不过,这样的用法是不被 Angular 所鼓励使用的。不仅是带来的安全风险,它还将你的应用程序与渲染层绑定在一起,使得难以运行在其它平台上。我相信访问 nativeElement 不仅破坏了抽象,还使用了特定的 DOM API,比如 textContent。不过随我我们会看到,在 Angular 中的概念模型中,很难用到如此低级的操作。

    ElementRef 可以通过任何 DOM 元素通过 ViewChild 装饰器获得。

    因为所有的 Component 都是寄宿在一个自定义的 DOM 元素之中,而所有的指令都需要通过 DOM 元素来应用,所以,Component 和 Directive 可以通过依赖注入而得到一个其关联寄宿元素的 ElementRef 的实例。

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

    所以,Component 是通过 DI 来访问其计算的元素,而 ViewChild 装饰器更多用于获得 Component 内部模板中的 DOM 元素的引用。不过,对指令则不是,它们是没有模板的,指令直接工作在它们应用的元素之上。

    TemplateRef

    大多数的 Web 开发者都应该熟悉 template。它是一组 DOM 元素,可以跨整个应用程序在视图中重用。在 HTML5 标准引入 <template> 之前,很多模式是通过使用 script 在浏览器中实现的,这需要使用 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 类来使用 <template>。下面是如何使用的示例。

    @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()。不过,该方法非常有用,因为它支持我们创建 View 并返回一个对该 View 的引用 ViewRef。

    ViewRef

    在 Angular 中,ViewRef 表示 Angular 视图 (View) 的抽象表示。在 Angular 的世界中,View 是应用程序的基本构建块。它是在一起被创建或者销毁的最小元素组单位。Angular 哲学鼓励开发者将 UI 界面看作 View 的聚合。而不要看作标准的 HTML 元素树。

    Angular 支持两种 View:

    • Embedded View,指 Template
    • Host View,指 Component

    创建 embedded view

    Template 用来简单地持有一个 View 的蓝图。View 可以通过 createEmbeddedView() 方法,通过 template 实例化出来。

    指通过 <template>元素来创建出来模板,然后通过 createEmbeddedView() 方法实例化出来。

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

    创建 host view

    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 中,每个 Component 都绑定到一个 Injector 的实例上,所以,当创建这个 Component 的时候,我们将当前组件的 Injector 传递进去。另外,不要忘了,动态实例化的 Component 需要加入到 Module 或者托管的 Component 的 EntryComponents 中。

    所以,我们已经看到了可以创建的 embeded 和 host 两种 View。一旦 View 被创建出来,它就可以使用 ViewContainer 插入到 DOM 中。下一节我们就介绍它的功能。

    ViewContainerRef

    ViewContainerRef 表示可以容纳一个或者多个 View 的容器。

    首先需要提醒的是,任何 DOM 元素都可以作为 View 的容器。有趣的是,Angular 不是将 View 插入到元素中,而是绑定到元素的 ViewContainer 中。这类似于 router-outlet 如何插入 Component。

    通常,比较好的将一个位置标记为 ViewContainer 的方式,是创建一个 <ng-container> 元素。它会被渲染为一条 comment,所以不会带来多于的 HTML 元素到 DOM 中。下面是一个示例,演示了在 Component 的模板中创建 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 元素,它被渲染为一个 comment,所以输出就成为 template bindings={}

    操控 Views

    ViewContainer 提供了一系列便捷的 API 来操作 View。

    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
    }
    

    前面我们已经看到过,如何手工创建两种类型的 View,分别是通过 <template> 和 Component。一旦创建了 View,我们就可以使用 insert() 方法将它们插入到容器中。下面是使用 <template> 创建嵌入的 View,并插入到使用 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);
        }
    }
    

    从 DOM 中删除 View,也就是从 ViewContainer 中删除 View,可以使用 detach() 方法。所有其它方法都是自解释的,可以通过下标获得相关的 View,将 View 移动到其它位置,或者删除 Container 中所有的 View。

    创建 View

    ViewContainer 还提供了一个自动创建 View 的 API

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

    它们是上面手工创建方式的简单封装。通过 Template 或者 Component 创建 View,并插入到特定的位置。

    ngTemplateOutlet 和 ngComponentOutlet

    尽管理解底层是如何工作的很重要,通常期望的使用方式总是简单。有两个指令实现快捷操作

    • ngTemplateOutlet
    • ngComponentOutlet

    非常好理解它们的作用。

    ngTemplateOutlet

    它将一个 DOM 元素标记为 ViewContainer,创建 <template> 的 View 实例,并将这个 Embeded View 插入到其中,而不需要在 Component 类中显式用代码完成。这意味着,上面的示例可以重写为如下形式。

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

    如你所见,我们不再需要任何实例化 View 的代码,特别方便。

    ngComponentOutlet

    与 ngTemplateOutlet 指令类似,该指令创建 Host View ( Component 的示例),而不是 Embeded View,你可以仿照下面的示例使用。

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

    总结

    对 Angular 中对通过 View 来操作 DOM,有一个清晰的概念模型非常重要。

  • 相关阅读:
    申论复习路线
    项目管理小拾
    物理隔离卡,双网通用安装
    生成css sprites
    图片压缩
    css预编译 sass
    小杂记
    遮罩层和弹出层(居中)
    布局之并列登高自适应高度解决方案
    slide逻辑
  • 原文地址:https://www.cnblogs.com/haogj/p/16827989.html
Copyright © 2020-2023  润新知