每當我閱讀中遇到,關於Angular中使用DOM的內容時,總會看到一個或幾個這樣的類:ElementRef,TemplateRef,ViewContainerRef等等。 不幸的是,雖然其中的一些被Angular文檔或相關文章所講述,可是我尚未找到完整的描述以及這些它們是如何工做的。 html
若是你來自angular.js世界,那麼你知道操縱DOM是至關容易的。Angular注入DOM elementRef到構造函數中,你能夠查詢組件模板中的任何節點,添加或刪除子節點,修改樣式等。可是,這種方法有一個主要的缺點 - 它牢牢地綁定到瀏覽器平臺。瀏覽器
新的Angular版本運行在不一樣的平臺上 - 瀏覽器,移動平臺等。 所以,站在平臺特定的API和框架接口之間須要抽象層次。Angular中,這些抽象成爲如下引用類型的形式:ElementRef,TemplateRef,ViewRef,ComponentRef和ViewContainerRef。 在本文中,咱們將詳細介紹每種引用類型,並展現如何使用它們來操做DOM。安全
在咱們探索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裝飾器的基本語法以下:this
@ViewChild([reference from template], {read: [reference type]});
在這個例子中,你能夠看到我在html中指定了tref做爲模板引用名,而且接收到與這個元素相關的ElementRef。 讀取的第二個參數並不老是必需的,由於Angular能夠經過DOM元素的類型來推斷引用類型。 例如,若是它是一個簡單的HTML元素(如span),那麼angular將返回ElementRef。 若是它是一個模板元素,它將返回TemplateRef。不過一些引用,如ViewContainerRef不能被推斷,而且必須在讀參數中特別要求。 其餘的,像ViewRef不能從DOM返回,必須手動構造。spa
這是最基本的抽象。 若是你觀察它的類結構,你會發現它只保存了它所關聯的本地元素。 對於訪問本地DOM元素很是有用,咱們能夠在這裏看到:code
// outputs `I am span` console.log(this.tref.nativeElement.textContent);
不過,Angular團隊不鼓勵這種用法。 這不只會帶來安全風險,還會在應用程序和渲染層之間形成緊密耦合,這使得在多個平臺上運行應用程序變得困難。 我相信這不是對nativeElement的訪問,而是打破了抽象,而是像textContent同樣使用特定的DOM API。 可是後面你會看到,在Angular中實現的DOM操做心智模型幾乎不須要這樣一個較低級別的訪問。component
可使用ViewChild裝飾器爲任何DOM元素返回ElementRef。 可是,因爲全部組件都駐留在自定義DOM元素中,而且全部指令都應用於DOM元素,所以組件和指令類能夠經過DI機制獲取與其主機元素關聯的ElementRef實例:htm
@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的形式返回一個引用。
這種抽象表示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中。 下一節將探討其功能。
表示能夠附加一個或多個視圖的容器。
首先要提到的是,任何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 = {}。
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 }
咱們以前已經看到,如何從模板和組件手動建立兩種類型的視圖。 一旦咱們有了一個視圖,咱們可使用插入方法將其插入到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方法。 全部其餘方法都是自解釋性的,可用於經過索引獲取對視圖的引用,將視圖移至其餘位置或從容器中移除全部視圖。
ViewContainer還提供API來自動建立視圖:
class ViewContainerRef { element: ElementRef length: number createComponent(componentFactory...): ComponentRef<C> createEmbeddedView(templateRef...): EmbeddedViewRef<C> ... }
這些都是咱們上面手動完成的簡單包裝。 他們從模板或組件建立一個視圖,並將其插入到指定位置。
這個將一個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 {}
正如你所看到的,咱們不使用任何視圖實例化組件類中的代碼。 很是便利。
該指令相似於ngTemplateOutlet,不一樣之處在於它建立一個宿主視圖(實例化一個組件),而不是嵌入視圖。 你能夠像這樣使用它:
<ng-container *ngComponentOutlet="ColorComponent"></ng-container>
如今,全部這些信息彷佛均可以被消化,但實際上這些信息是很是連貫的,而且經過視圖來顯示操縱DOM的清晰模型。 經過使用ViewChild查詢和模板變量引用,您能夠得到對Angular DOM抽象的引用。 圍繞DOM元素的最簡單的包裝是ElementRef。 對於具備TemplateRef的模板,您能夠建立嵌入式視圖。 主機視圖能夠在使用ComponentFactoryResolver建立的componentRef上訪問。 視圖能夠用ViewContainerRef來操做。 有兩個使自動手動過程的指令:ngTemplateOutlet - 用於嵌入視圖,ngComponentOutlet用於宿主視圖(動態組件)。