【轉】使用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—— 創建宿主視圖(動態組件)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM