Angular開發實踐(八): 使用ng-content進行元件內容投射

laixiangran發表於2018-04-05

在Angular中,元件屬於特殊的指令,它的特殊之處在於它有自己的模板(html)和樣式(css)。因此使用元件可以使我們的程式碼具有強解耦、可複用、易擴充套件等特性。通常的元件定義如下:

demo.component.ts:

import { Component, OnInit } from '@angular/core';

@Component({
	selector: 'demo-component',
	templateUrl: './demo.component.html',
	styleUrls: ['./demo.component.scss']
})
export class DemoComponent implements OnInit {

	constructor() {
	}

	ngOnInit() {
	}
}
複製程式碼

demo.component.html:

<div class="demo">
	<h2>
		demo-component - 我是一個簡單的元件
	</h2>
</div>
複製程式碼

demo.component.scss:

.demo {
	padding: 10px;
	border: 2px solid red;

	h2 {
		margin: 0;
		color: #262626;
	}
}
複製程式碼

此時我們引用該元件,就會呈現該元件解析之後的內容:

<demo-component></demo-component>
複製程式碼

Angular開發實踐(八): 使用ng-content進行元件內容投射

假設現在有這樣的需求,這個元件能夠接受外部投射進來的內容,也就是說元件最終呈現的內容不僅僅是本身定義的那些,那該怎麼做呢?這時就要請出本文的主角 ng-content

簡單投射

我們先從最簡單開始,在 demo.component.html 中新增 ,修改後的 demo.component.html 和 demo.component.scss 如下:

demo.component.html:

<div class="demo">
	<h2>
		demo-component - 可嵌入外部內容的元件
	</h2>
	<div class="content">
		<ng-content></ng-content>
	</div>
</div>
複製程式碼

demo.component.scss:

.demo {
	padding: 10px;
	border: 2px solid red;

	h2 {
		margin: 0;
		color: #262626;
	}

	.content {
		padding: 10px;
		margin-top: 10px;
		line-height: 20px;
		color: #FFFFFF;
		background-color: #de7d28;
	}
}
複製程式碼

為了效果展示特意將 所在的容器背景色定義為橙色。

這時我們在引用該元件時可以從外部投射內容,外部內容將在橙色區域顯示:

<demo-component>
	我是外部嵌入的內容
</demo-component>
複製程式碼

Angular開發實踐(八): 使用ng-content進行元件內容投射

針對性投射

如果同時存在幾個 ,那外部內容將如何進行投射呢?

我們先看個示例,為了區別,我再新增一個藍色區域的 ,修改後的 demo.component.html 和 demo.component.scss 如下:

demo.component.html:

<div class="demo">
	<h2>
		demo-component - 可嵌入外部內容的元件
	</h2>
	<div class="content">
		<ng-content></ng-content>
	</div>
	<div class="content blue">
		<ng-content></ng-content>
	</div>
</div>
複製程式碼

demo.component.scss:

.demo {
	padding: 10px;
	border: 2px solid red;

	h2 {
		margin: 0;
		color: #262626;
	}

	.content {
		padding: 10px;
		margin-top: 10px;
		line-height: 20px;
		color: #FFFFFF;
		background-color: #de7d28;
		
		&.blue {
			background-color: blue;
		}
	}
}
複製程式碼

引用該元件:

<demo-component>
	我是外部嵌入的內容
</demo-component>
複製程式碼

此時,我們將看到外部內容投射到了藍色區域:

Angular開發實踐(八): 使用ng-content進行元件內容投射

當然,如果你將橙色區域程式碼放在藍色區域程式碼的後面,那麼外部內容就會投射到橙色區域:

Angular開發實踐(八): 使用ng-content進行元件內容投射

所以從上面的示例我們可以看出,如果同時存在簡單的 ,那麼外部內容將投射在元件模板最後的那個 中。

那麼知道這個問題,我們可能會想,能不能將外部內容有針對性的投射相應的 中呢?答案顯然是可以的。

為了處理這個問題, 支援一個 select 屬性,可以讓你在特定的地方投射具體的內容。該屬性支援 CSS 選擇器(標籤選擇器、類選擇器、屬性選擇器、...)來匹配你想要的內容。如果 ng-content 上沒有設定 select 屬性,它將接收全部內容,或接收不匹配任何其他 ng-content 元素的內容。

直接看例子,修改後的 demo.component.html 和 demo.component.scss 如下:

demo.component.html:

<div class="demo">
	<h2>
		demo-component - 可嵌入外部內容的元件
	</h2>
	<div class="content">
		<ng-content></ng-content>
	</div>
	<div class="content blue">
		<ng-content select="header"></ng-content>
	</div>
	<div class="content red">
		<ng-content select=".demo2"></ng-content>
	</div>
	<div class="content green">
		<ng-content select="[name=demo3]"></ng-content>
	</div>
</div>
複製程式碼

demo.component.scss:

.demo {
	padding: 10px;
	border: 2px solid red;

	h2 {
		margin: 0;
		color: #262626;
	}

	.content {
		padding: 10px;
		margin-top: 10px;
		line-height: 20px;
		color: #FFFFFF;
		background-color: #de7d28;

		&.blue {
			background-color: blue;
		}

		&.red {
			background-color: red;
		}

		&.green {
			background-color: green;
		}
	}
}
複製程式碼

從上面程式碼可以看到,藍色區域將接收 標籤 header 那部分內容,紅色區域將接收 class為"demo2"的div 的那部分內容,綠色區域將接收 屬性name為"demo3"的div 的那部分內容,橙色區域將接收其餘的外部內容(開始,我是外部嵌入的內容,結束)。

引用該元件:

<demo-component>
	開始,我是外部嵌入的內容,
	<header>
		我是外部嵌入的內容,我在header中
	</header>
	<div class="demo2">
		我是外部嵌入的內容,我所在div的class為"demo2"
	</div>
	<div name="demo3">
		我是外部嵌入的內容demo,我所在div的屬性name為"demo3"
	</div>
	結束
</demo-component>
複製程式碼

Angular開發實踐(八): 使用ng-content進行元件內容投射

此時,我們將看到外部內容投射到了指定的 中。

擴充套件知識

ngProjectAs

現在我們知道通過 ng-content 的 select 屬性可以指定外部內容投射到指定的 中。

而要能正確的根據 select 屬性投射內容,有個限制就是 - 不管是 標籤 headerclass為"demo2"的div還是 屬性name為"demo3"的div,這幾個標籤都是作為 元件標籤 的直接子節點

那如果不是作為直接子節點,會是什麼情況呢?我們簡單修改下引用 demo-component 元件的程式碼,將 標籤header 放在一個div中,修改如下:

<demo-component>
	開始,我是外部嵌入的內容,
	<div>
		<header>
			我是外部嵌入的內容,我在header中
		</header>
	</div>
	<div class="demo2">
		我是外部嵌入的內容,我所在div的class為"demo2"
	</div>
	<div name="demo3">
		我是外部嵌入的內容demo,我所在div的屬性name為"demo3"
	</div>
	結束
</demo-component>
複製程式碼

Angular開發實踐(八): 使用ng-content進行元件內容投射

此時,我們看到 標籤 header 那部分內容不再投射到藍色區域中了,而是投射到橙色區域中了。原因就是 <ng-content select="header"></ng-content> 無法匹配到之前的 標籤 header,故而將這部分內容投射到了橙色區域的 <ng-content></ng-content> 中了。

為了解決這個問題,我們必須使用 ngProjectAs 屬性,它可以應用於任何元素上。具體如下:

<demo-component>
	開始,我是外部嵌入的內容,
	<div ngProjectAs="header">
		<header>
			我是外部嵌入的內容,我在header中
		</header>
	</div>
	<div class="demo2">
		我是外部嵌入的內容,我所在div的class為"demo2"
	</div>
	<div name="demo3">
		我是外部嵌入的內容demo,我所在div的屬性name為"demo3"
	</div>
	結束
</demo-component>
複製程式碼

通過設定 ngProjectAs 屬性,讓 標籤header 所在的 div 指向了 select="header",此時 標籤 header 那部分內容有投射到藍色區域了:

Angular開發實踐(八): 使用ng-content進行元件內容投射

<ng-content> 不“產生”內容

做個試驗

做個試驗,先定義一個 demo-child-component 元件:

import { Component, OnInit } from '@angular/core';

@Component({
	selector: 'demo-child-component',
	template: '<h3>我是demo-child-component元件</h3>'
})
export class DemoChildComponent implements OnInit {

	constructor() {
	}

	ngOnInit() {
	    console.log('demo-child-component初始化完成!');
	}
}
複製程式碼

demo-component 元件修改為:

import { Component, OnInit } from '@angular/core';

@Component({
	selector: 'demo-component',
	template: `
    	<button (click)="show = !show">
            {{ show ? 'Hide' : 'Show' }}
        </button>
        <div class="content" *ngIf="show">
            <ng-content></ng-content>
        </div>
	`
})
export class DemoComponent implements OnInit {
    show = true;

	constructor() {
	}

	ngOnInit() {
	}
}
複製程式碼

然後在 demo-component 中 投射 demo-child-component:

<demo-component>
	<demo-child-component></demo-child-component>
</demo-component>
複製程式碼

此時,在控制檯我們看到列印出 demo-child-component初始化完成! 這些文字。但是當我們點選按鈕進行切換操作時,demo-child-component初始化完成! 就不再列印了,這意味著我們的 demo-child-component 元件只被例項化了一次 - 從未被銷燬和重新建立。

為什麼會出現這樣的情況呢?

出現原因

<ng-content> 不會 "產生" 內容,它只是投影現有的內容。你可以認為它等價於 node.appendChild(el) 或 jQuery 中的 $(node).append(el) 方法:使用這些方法,節點不被克隆,它被簡單地移動到它的新位置。因此,投影內容的生命週期將被繫結到它被宣告的地方,而不是顯示在地方。

這也從原理解釋了前面那個問題:如果同時存在幾個 ,那外部內容將如何進行投射呢?

這種行為有兩個原因:期望一致性和效能。什麼 "期望的一致性" 意味著作為開發人員,可以基於應用程式的程式碼,猜測其行為。假設我寫了以下程式碼:

<demo-component>
	<demo-child-component></demo-child-component>
</demo-component>
複製程式碼

很顯然 demo-child-component 元件將被例項化一次,但現在假如我們使用第三方庫的元件:

<third-party-wrapper>
    <demo-child-component></demo-child-component>
</third-party-wrapper>
複製程式碼

如果第三方庫能夠控制 demo-child-component 元件的生命週期,我將無法知道它被例項化了多少次。其中唯一方法就是檢視第三方庫的程式碼,瞭解它們的內部處理邏輯。將元件的生命週期被繫結到我們的應用程式元件而不是包裝器的意義是,開發者可以掌控計數器只被例項化一次,而不用瞭解第三方庫的內部程式碼。

效能的原因 更為重要。因為 ng-content 只是移動元素,所以可以在編譯時完成,而不是在執行時,這大大減少了實際應用程式的工作量。

解決方法

為了讓元件能夠控制投射進來的子元件的例項化,我們可以通過兩種方式完成:在我們的內容周圍使用 <ng-template> 元素及 ngTemplateOutlet,或者使用帶有 "*" 語法的結構指令。為簡單起見,我們將在示例中使用 <ng-template> 語法。

demo-component 元件修改為:

import { Component, OnInit } from '@angular/core';

@Component({
	selector: 'demo-component',
	template: `
    	<button (click)="show = !show">
            {{ show ? 'Hide' : 'Show' }}
        </button>
        <div class="content" *ngIf="show">
            <ng-container [ngTemplateOutlet]="template"></ng-container>
        </div>
	`
})
export class DemoComponent implements OnInit {
    @ContentChild(TemplateRef) template: TemplateRef;
    show = true;

	constructor() {
	}

	ngOnInit() {
	}
}
複製程式碼

然後我們將 demo-child-component 包含在 ng-template 中:

<demo-component>
    <ng-template>
        <demo-child-component></demo-child-component>
    </ng-template>
</demo-component>
複製程式碼

此時,我們在點選按鈕進行切換操作時,控制檯都會列印出 demo-child-component初始化完成! 這些文字。

參考資源

ng-content: The hidden docs


轉載請註明出處,謝謝!

Angular開發實踐(八): 使用ng-content進行元件內容投射

相關文章