原文連接: Exploring Angular DOM manipulation techniques using ViewContainerRef若是想深刻學習 Angular 如何使用 Renderer 和 View Containers 技術操做 DOM,能夠查閱 YouTube 視頻 my talk at NgVikings。html
每次我讀到 Angular 如何操做 DOM 相關文章時,總會發現這些文章提到 ElementRef
、TemplateRef
、ViewContainerRef
和其餘的類。儘管這些類在 Angular 官方文檔或相關文章會有涉及,可是不多會去描述總體思路,這些類如何一塊兒做用的相關示例也不多,而本文就主要描述這些內容。node
若是你來自於 angular.js 世界,很容易明白如何使用 angular.js 操做 DOM。angular.js 會在 link
函數中注入 DOM element
,你能夠在組件模板裏查詢任何節點(node),添加或刪除節點(node),修改樣式(styles),等等。然而這種方式有個主要缺陷:與瀏覽器平臺緊耦合。api
新版本 Angular 須要在不一樣平臺上運行,如 Browser 平臺,Mobile 平臺或者 Web Worker 平臺,因此,就須要在特定平臺的 API 和框架接口之間進行一層抽象(abstraction)。Angular 中的這層抽象就包括這些引用類型:ElementRef
、TemplateRef
、ViewRef
、ComponentRef
和 ViewContainerRef
。本文將詳細講解每個引用類型(reference type)和該引用類型如何操做 DOM。瀏覽器
在探索 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
如今,讓咱們看看應該如何獲取這些引用,一塊兒去探索吧。函數
這是最基本的抽象類,若是你查看它的類結構,就發現它只包含所掛載的元素對象,這對訪問原生 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 元素。然而指令卻相反,由於指令沒有視圖模板,因此主要用來獲取指令掛載的宿主元素。
對於大部分開發者來講,模板概念很熟悉,就是跨程序視圖內一堆 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
。
該抽象表示一個 Angular 視圖(View),在 Angular 世界裏,視圖(View)是一堆元素的組合,一塊兒被建立和銷燬,是構建程序 UI 的基石。Angular 鼓勵開發者把 UI 做爲一堆視圖(View)的組合,而不只僅是 html 標籤組成的樹。
Angular 支持兩種類型視圖:
Template
提供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 樹中。下文主要探索這個功能。
視圖容器就是掛載一個或多個視圖的容器。
首先須要說的是,任何 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> ... }
上面兩個方法是個很好的封裝,能夠傳入模板引用對象或組件工廠對象來建立視圖,並將該視圖插入視圖容器中特定位置。
儘管知道 Angular 操做 DOM 的內部機制是好事,可是要是有某種快捷方式就更好了啊。沒錯,Angular 提供了兩種快捷指令:ngTemplateOutlet
和 ngComponentOutlet
。寫做本文時這兩個指令都是實驗性的,ngComponentOutlet
也將在版本 4 中可用(譯者注:如今版本 5.* 也是實驗性的,也均可用)。若是你讀完了上文,就很容易知道這兩個指令是作什麼的。
該指令會把 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
很類似,區別是 ngComponentOutlet
建立的是由組件實例化生成的宿主視圖,不是嵌入視圖。你能夠這麼使用:
<ng-container *ngComponentOutlet="ColorComponent"></ng-container>
看似有不少新知識須要消化啊,但實際上 Angular 經過視圖操做 DOM 的思路模型是很清晰和連貫的。你可使用 ViewChild
查詢模板引用變量來得到 Angular DOM 抽象類。DOM 元素的最簡單封裝是 ElementRef
;而對於模板,你可使用 TemplateRef
來建立嵌入視圖;而對於組件,可使用 ComponentRef
來建立宿主視圖,同時又可使用 ComponentFactoryResolver
建立 ComponentRef
。這兩個建立的視圖(即嵌入視圖和宿主視圖)又會被 ViewContainerRef
管理。最後,Angular 又提供了兩個快捷指令自動化這個過程:ngTemplateOutlet
指令使用模板建立嵌入視圖;ngComponentOutlet
使用動態組件建立宿主視圖。