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

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

若是想深刻學習 Angular 如何使用 Renderer 和 View Containers 技術操做 DOM,能夠查閱 YouTube 視頻 my talk at NgVikingshtml

每次我讀到 Angular 如何操做 DOM 相關文章時,總會發現這些文章提到 ElementRefTemplateRefViewContainerRef 和其餘的類。儘管這些類在 Angular 官方文檔或相關文章會有涉及,可是不多會去描述總體思路,這些類如何一塊兒做用的相關示例也不多,而本文就主要描述這些內容。node

若是你來自於 angular.js 世界,很容易明白如何使用 angular.js 操做 DOM。angular.js 會在 link 函數中注入 DOM element,你能夠在組件模板裏查詢任何節點(node),添加或刪除節點(node),修改樣式(styles),等等。然而這種方式有個主要缺陷:與瀏覽器平臺緊耦合api

新版本 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 裝飾器基本語法是: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 使用動態組件建立宿主視圖。

相關文章
相關標籤/搜索