每當我閱讀中遇到,關于Angular中使用DOM的內容時,總會看到一個或幾個這樣的類:ElementRef,TemplateRef,ViewContainerRef等等。 不幸的是,雖然其中的一些被Angular文檔或相關文章所講述,但是我還沒有找到完整的描述以及這些它們是如何工作的。
如果你來自angular.js世界,那么你知道操縱DOM是相當容易的。Angular注入DOM elementRef到構造函數中,你可以查詢組件模板中的任何節點,添加或刪除子節點,修改樣式等。但是,這種方法有一個主要的缺點 - 它緊緊地綁定到瀏覽器平臺。
新的Angular版本運行在不同的平臺上 - 瀏覽器,移動平臺等。 因此,站在平臺特定的API和框架接口之間需要抽象層次。Angular中,這些抽象成為以下引用類型的形式:ElementRef,TemplateRef,ViewRef,ComponentRef和ViewContainerRef。 在本文中,我們將詳細介紹每種引用類型,并展示如何使用它們來操作DOM。
@ViewChild
在我們探索DOM抽象之前,讓我們了解如何在組件/指令類中訪問這些抽象。 Angular提供了一種稱為DOM查詢的機制。 它以@ViewChild和@ViewChildren裝飾器的形式出現。 它們的行為相同,只有前者返回一個引用,后者則返回多個引用作為QueryList對象。 在這篇文章的例子中,我將主要使用ViewChild裝飾器。
通常,這些裝飾器與模板引用變量配對使用。 模板引用變量只是對模板中的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裝飾器的基本語法如下:
@ViewChild([reference from template], {read: [reference type]});
在這個例子中,你可以看到我在html中指定了tref作為模板引用名,并且接收到與這個元素相關的ElementRef。 讀取的第二個參數并不總是必需的,因為Angular可以通過DOM元素的類型來推斷引用類型。 例如,如果它是一個簡單的HTML元素(如span),那么angular將返回ElementRef。 如果它是一個模板元素,它將返回TemplateRef。不過一些引用,如ViewContainerRef不能被推斷,并且必須在讀參數中特別要求。 其他的,像ViewRef不能從DOM返回,必須手動構造。
ElementRef
這是最基本的抽象。 如果你觀察它的類結構,你會發現它只保存了它所關聯的本地元素。 對于訪問本地DOM元素非常有用,我們可以在這里看到:
// outputs `I am span`
console.log(this.tref.nativeElement.textContent);
不過,Angular團隊不鼓勵這種用法。 這不僅會帶來安全風險,還會在應用程序和渲染層之間造成緊密耦合,這使得在多個平臺上運行應用程序變得困難。 我相信這不是對nativeElement的訪問,而是打破了抽象,而是像textContent一樣使用特定的DOM API。 但是后面你會看到,在Angular中實現的DOM操作心智模型幾乎不需要這樣一個較低級別的訪問。
可以使用ViewChild裝飾器為任何DOM元素返回ElementRef。 但是,由于所有組件都駐留在自定義DOM元素中,并且所有指令都應用于DOM元素,因此組件和指令類可以通過DI機制獲取與其主機元素關聯的ElementRef實例:
@Component({selector: 'sample',...
export class SampleComponent{constructor(private hostElement: ElementRef) {//outputs <sample>...</sample>console.log(this.hostElement.nativeElement.outerHTML);}
因此,雖然組件可以通過DI訪問其主機元素,但ViewChild裝飾器通常用于在其視圖(模板)中獲取對DOM元素的引用。 反之亦然,指令沒有視圖,他們通常直接與他們所附的元素。
模板的概念應該是大多數Web開發人員熟悉的。 這是一組DOM元素,在整個應用程序的視圖中被重用。 在HTML5標準引入了模板標簽之前,大多數模板都被包含在script標簽中。
<script id="tpl" type="text/template"><span>I am span in template</span>
</script>
這種方法當然有許多缺點,如語義和手動創建DOM模型的必要性。 使用模板標簽瀏覽器解析HTML并創建DOM樹,但不呈現它。 然后可以通過內容屬性訪問:
<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類來處理模板。 以下是如何使用它:
@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
這種抽象表示Angular視圖。 在Angular世界中,View是應用程序UI的基本構建塊。 它是創造和消滅的最小的元素分組。 Angular哲學鼓勵開發人員將UI視為Views的組合,而不是將其視為獨立的HTML標簽。
Angular支持兩種類型的視圖:
- 嵌入視圖鏈接到模板
- 鏈接到組件的主機視圖
創建嵌入的視圖
一個模板只是一個視圖的藍圖。 一個視圖可以使用前面提到的createEmbeddedView方法從模板實例化,如下所示:
ngAfterViewInit() {let view = this.tpl.createEmbeddedView(null);
}
創建宿主視圖
宿主視圖是在組件動態實例化時創建的。 可以使用ComponentFactoryResolver動態創建一個組件:
constructor(private injector: Injector,private r: ComponentFactoryResolver) {let factory = this.r.resolveComponentFactory(ColorComponent);let componentRef = factory.create(injector);let view = componentRef.hostView;
}
在Angular中,每個組件都綁定到一個注入器的特定實例,所以我們在創建組件時傳遞當前的注入器實例。 此外,不要忘記,動態實例化的組件必須添加到模塊或主機組件的EntryComponents。
所以,我們已經看到如何創建嵌入和宿主視圖。 一旦創建了視圖,就可以使用ViewContainer將其插入到DOM中。 下一節將探討其功能。
ViewContainerRef
表示可以附加一個或多個視圖的容器。
首先要提到的是,任何DOM元素都可以用作視圖容器。有趣的是,Angular不在元素內插入視圖,而是在綁定到ViewContainer的元素之后附加它們。 這與路由器插座如何插入組件類似。
通常,標記應該創建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綁定到通過元素屬性訪問的特定DOM元素。 在這個例子中,ng-container元素被綁定為注釋的示例中,輸出為template bindings = {}。
Manipulating views
ViewContainer為操作視圖提供了一個方便的API:
class ViewContainerRef {...clear() : voidinsert(viewRef: ViewRef, index?: number) : ViewRefget(index: number) : ViewRefindexOf(viewRef: ViewRef) : numberdetach(index?: number) : ViewRefmove(viewRef: ViewRef, currentIndex: number) : ViewRef
}
我們之前已經看到,如何從模板和組件手動創建兩種類型的視圖。 一旦我們有了一個視圖,我們可以使用插入方法將其插入到DOM中。 所以,下面是從模板中創建一個嵌入式視圖并將其插入到由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);}
}
通過這個實現,生成的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: ElementReflength: numbercreateComponent(componentFactory...): ComponentRef<C>createEmbeddedView(templateRef...): EmbeddedViewRef<C>...
}
這些都是我們上面手動完成的簡單包裝。 他們從模板或組件創建一個視圖,并將其插入到指定位置。
ngTemplateOutlet and ngComponentOutlet
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,不同之處在于它創建一個宿主視圖(實例化一個組件),而不是嵌入視圖。 你可以像這樣使用它:
<ng-container *ngComponentOutlet="ColorComponent"></ng-container>
總結
現在,所有這些信息似乎都可以被消化,但實際上這些信息是非常連貫的,并且通過視圖來顯示操縱DOM的清晰模型。 通過使用ViewChild查詢和模板變量引用,您可以獲得對Angular DOM抽象的引用。 圍繞DOM元素的最簡單的包裝是ElementRef。 對于具有TemplateRef的模板,您可以創建嵌入式視圖。 主機視圖可以在使用ComponentFactoryResolver創建的componentRef上訪問。 視圖可以用ViewContainerRef來操作。 有兩個使自動手動過程的指令:ngTemplateOutlet - 用于嵌入視圖,ngComponentOutlet用于宿主視圖(動態組件)。