這篇文章主要介紹使用Angular api 和 CDK Portals兩種方式實現動態建立組件,另外還會講一些跟它相關的知識點,如:Angular多級依賴注入、ViewContainerRef,Portals能夠翻譯爲 門戶 ,我以爲放到這裏叫 入口 更好,能夠理解爲動態建立組件的入口,相似於小程序或者Vue中的Slot.css
cdk全名Component Development Kit 組件開發包,是Angular官方在開發基於Material Design的組件庫時抽象出來單獨的一個開發包,裏面封裝了一些開發組件時的公共邏輯而且跟Material Design 設計無關,能夠用來封裝本身的組件庫或者直接在業務開發中使用,裏面代碼抽象程度很是高,很是值得學習,如今我用到的有Portals、Overlay(打開浮層相關)、SelectionModel、Drag and Drop等.
官方:material.angular.io/
中文翻譯:material.angular.cnhtml
想一想應用的路由,通常配置路由地址的時候都會給這個地址配置一個入口組件,當匹配到這個路由地址的時候就在指定的地方渲染這個組件,動態建立組件相似,在最頁面未接收到用戶行爲的時候,我不知道頁面中這塊區域應該渲染那個組件,當頁面加載時根據數據庫設置或者用戶的操做行爲才能肯定最終要渲染的組件,這時候就要用代碼動態建立組件把目標組件渲染到正確的地方。
示例截圖
git
該路由的入口組件是PortalsEntryConponent組件,如上面截圖所示右側有一塊虛線邊框的區域,裏面具體的渲染組件不肯定。github
先在視圖模板中定義一個佔位的區域,動態組件就要渲染在這個位置,起一個名稱#virtualContainer
文件portals-entry.component.html數據庫
<div class="portals-outlet" >
<ng-container #virtualContainer>
</ng-container>
</div>
複製代碼
經過ViewChild取到這個container對應的邏輯容器
文件portals-entry.component.ts小程序
@ViewChild('virtualContainer', { read: ViewContainerRef })
virtualContainer: ViewContainerRef;
複製代碼
處理單擊事件,單擊按鈕時動態建立一個組件,portals-entry.component.ts完整邏輯api
import { TaskDetailComponent } from '../task/task-detail/task-detail.component';
@Component({
selector: 'app-portals-entry',
templateUrl: './portals-entry.component.html',
styleUrls: ['./portals-entry.component.scss'],
providers: [
]
})
export class PortalsEntryComponent implements OnInit {
@ViewChild('virtualContainer', { read: ViewContainerRef })
virtualContainer: ViewContainerRef;
constructor(
private dynamicComponentService: DynamicComponentService,
private componentFactoryResolver: ComponentFactoryResolver,
private injector: Injector,
) { }
ngOnInit() {
}
openTask() {
const task = new TaskEntity();
task.id = '1000';
task.name = '寫一篇關於Portals的文章';
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(TaskDetailComponent);
const componentRef = this.virtualContainer.createComponent<TaskDetailComponent>(
componentFactory,
null,
this.virtualContainer.injector
);
(componentRef.instance as TaskDetailComponent).task = task; // 傳遞參數
}
}
複製代碼
ViewContainerRef除了createComponent方法外還有一個createEmbeddedView方法,用於建立模板bash
@ViewChild('customTemplate')
customTemplate: TemplateRef<any>;
this.virtualContainer.createEmbeddedView(this.customTemplate, { name: 'pubuzhixing' });
複製代碼
createEmbeddedView方法的第二個參數,用於指定模板的上下文參數,看下模板定義及如何使用參數app
<ng-template #customTemplate let-name="name">
<p>自定義模板,傳入參數name:{{name}}</p>
</ng-template>
複製代碼
此外還能夠經過ngTemplateOutlet直接插入內嵌視圖模板,經過ngTemplateOutletContext指定模板的上下文參數dom
<ng-container [ngTemplateOutlet]="customTemplate" [ngTemplateOutletContext]="{ name:'pubuzhixing' }"></ng-container>
複製代碼
分析下Angular動態建立組件/內嵌視圖的API,動態建立組件首先須要一個被建立的組件定義或模板聲明,另外須要Angular上下文的環境來提供這個組件渲染在那裏以及這個組件的依賴從那獲取,viewContainerRef是動態組件的插入位置而且提供組件的邏輯範圍,此外還須要單獨傳入依賴注入器injector,示例直接使用邏輯容器的injector,是否是很好理解。
示例倉儲:github.com/pubuzhixing…
這裏先對Portal相關的內容作一個簡單的說明,後面會有兩個使用示例,原本這塊內容準備放到最後的,最終仍是決定放在前面,能夠先對Portals有一個簡單的瞭解,若是其中有翻譯不許確請見諒。
地址:material.angular.io/cdk/portal/…
-------- 文檔開始
portals 提供渲染動態內容到應用的可伸縮的實現,其實就是封裝了Angular動態建立組件的過程
這個Portal指是能動態渲染一個指定位置的
Portal<T> 包括動態組件的抽象類,能夠是TemplatePortal(模板)或者ComponentPortal(組件)
複製代碼
方法 | 描述 |
---|---|
attach(PortalOutlet): T | 把當前Portal附加到宿主上 |
detach(): void | 把Portal從宿主上拆離 |
isAttached: boolean | 當前Portal是否已經附加到宿主上 |
PortalOutlet 動態組件的宿主
複製代碼
方法 | 描述 |
---|---|
attach(Portal): any | 附加指定Portal |
detach(): any | 拆離當前附加Portal |
dispose(): void | 永久釋放宿主資源 |
hasAttached: boolean | 當前是否已經裝在Portal |
CdkPortal
<ng-template cdkPortal>
<p>The content of this template is captured by the portal.</p>
</ng-template>
<!-- OR -->
<!-- 經過下面的結構指令語法能夠獲得一樣的結果 -->
<p *cdkPortal>
The content of this template is captured by the portal.
</p>
複製代碼
能夠經過ViewChild、ViewChildren獲取到該Portal,類型應該是CdkPortal,以下所示:
// 模板中的Portal
@ViewChild(CdkPortal) templateCDKPortal: TemplatePortal<any>;
複製代碼
ComponentPortal
組件類型的Portal,須要當前組件在NgModule的entryComponents中配置才能動態建立該組件。
this.userSettingsPortal = new ComponentPortal(UserSettingsComponent);
複製代碼
CdkPortalOutlet
使用指令能夠把portal outlet添加到一個ng-template,cdkPortalOutlet把當前元素指定爲PortalOutlet,下面代碼把userSettingsPortal綁到此portal-outlet上
<!-- Attaches the `userSettingsPortal` from the previous example. -->
<ng-template [cdkPortalOutlet]="userSettingsPortal"></ng-template>
複製代碼
----- 文檔完畢
這裏首先使用新的api完成和最上面示例同樣的需求,在一樣的位置動態渲染TaskDetailComponent組件。
一樣是設置一個宿主元素用於渲染動態組件,可使用指令cdkPortalOutlet掛載一個PortalOutlet在這個ng-container元素上
<div class="portals-outlet">
<ng-container #virtualContainer cdkPortalOutlet>
</ng-container>
</div>
複製代碼
與 使用Angular API動態建立組件 一節使用同一個邏輯元素做爲宿主,只不過這裏的獲取容器的類型是CdkPortalOutlet,代碼以下
@ViewChild('virtualContainer', { read: CdkPortalOutlet })
virtualPotalOutlet: CdkPortalOutlet;
複製代碼
建立一個ComponentPortal類型的Portal,而且將它附加上面獲取的宿主virtualPotalOutlet上,代碼以下
portalOpenTask() {
this.virtualPotalOutlet.detach();
const taskDetailCompoentPortal = new ComponentPortal<TaskDetailComponent>(
TaskDetailComponent
);
const ref = this.virtualPotalOutlet.attach(taskDetailCompoentPortal);
// 此處一樣能夠 經過ref.instance傳遞task參數
}
複製代碼
這裏是使用ComponentPortal的示例實現動態建立組件,Portal還有一個子類TemplatePortal是針對模板實現的,上節 CDK Portal 官方文檔介紹 中有介紹,這裏就不在贅述了。總之使用Portals能夠很大程度上簡化代碼邏輯。
示例倉儲:github.com/pubuzhixing…
上面只是使用Portal的最簡單用法,下面討論下它的源碼實現,以便更好的理解
首先咱們先看一下ComponentPortal類的建立,上面的例子只是指定了一個組件類型做爲參數,其實它還有別的參數能夠配置,先看下ComponentPortal的構造函數定義
export class ComponentPortal<T> extends Portal<ComponentRef<T>> {
constructor(
component: ComponentType<T>,
viewContainerRef?: ViewContainerRef | null,
injector?: Injector | null,
componentFactoryResolver?: ComponentFactoryResolver | null) {
super();
this.component = component;
this.viewContainerRef = viewContainerRef;
this.injector = injector;
this.componentFactoryResolver = componentFactoryResolver;
}
}
複製代碼
ComponentPortal構造函數的另外兩個參數
viewContainerRef和injector
viewContainerRef參數非必填默認附到PortalOutlet上,若是傳入viewContainerRef參數,那麼ComponentPortal就會附到該viewContaierRef上,而不是當前PortalOutlet所在的元素上。
injector參數非必填,默認使用PortalOutlet所在的邏輯容器的injector,若是傳入injector,那麼動態建立的組件就使用傳入的injector做爲注入器。
BasePortalOutlet提供了附加ComponentPortal和TemplatePortal的部分實現,咱們看下attach方法的部分代碼(僅僅展現部分邏輯)
/** Attaches a portal. */
attach(portal: Portal<any>): any {
if (!portal) {
throwNullPortalError();
}
if (portal instanceof ComponentPortal) {
this._attachedPortal = portal;
return this.attachComponentPortal(portal);
} else if (portal instanceof TemplatePortal) {
this._attachedPortal = portal;
return this.attachTemplatePortal(portal);
}
throwUnknownPortalTypeError();
}
複製代碼
attach處理前先根據Portal的類型是確實是組件仍是模板,而後再進行相應的處理,其實最終仍是調用了ViewContainerRef的createComponent或者createEmbeddedView方法,對這塊感興趣看查看源代碼文件portal-directives.ts。
DomPortalOutlet能夠把一個Portal插入到一個Angular應用上下文以外的DOM中,想一想咱們前面的例子,不管本身實現仍是使用CdkPortalOutlet都是把一個模板或者組件插入到一個Angular上下文中的宿主ViewContainerRef中,而DomPortalOutlet就是
if (!this._outlet) {
this._outlet = new DomPortalOutlet(this._document.createElement('div'),
this._componentFactoryResolver, this._appRef, this._injector);
}
const element: HTMLElement = this._template.elementRef.nativeElement;
element.parentNode!.insertBefore(this._outlet.outletElement, element);
this._portal.attach(this._outlet, context);
複製代碼
上面的代碼先建立了DomPortalOutlet類型的對象_outlet,DomPortalOutlet是一個DOM宿主它不在Angular的任何一個ViewContainerRef中,如今看下它的四個構造函數參數
參數名 | 類型 | 說明 |
---|---|---|
outletElement
|
Element | 建立的document元素 |
_componentFactoryResolver
|
ComponentFactoryResolver | 剛開始一直不理解這個實例對象是幹什麼的,後來查了資料,它大概的做用是對要建立的組件或者模板進行編譯 |
_appRef
|
ApplicationRef | 當前Angular應用的一個關聯對象 |
_defaultInjector
|
Injector | 注入器對象 |
說明:這節講的
脫離Angular上下文是不太準肯定,任何模板或者組件都不能脫離Angular的運行環境,這裏應該是脫離了實際渲染的Component Tree,單獨渲染到指定dom中。
爲ComponentPortal傳入PortalInjector對象,PortalInjector實例對象配置一個其它業務組件的injector而且配置tokens,下面簡單說明下邏輯結構,有興趣的可看完整示例。
文件task-list.component.ts
@Component({,
selector: 'app-task-list',
templateUrl: './task-list.component.html',
styleUrls: ['./task-list.component.scss'],
providers: [TaskListService]
})
export class TaskListComponent implements OnInit {
constructor(public taskListService: TaskListService) {}
}
複製代碼
組件級提供商配置了TaskListService
用於獲取任務列表數據,並保存在屬性tasks中
在模板中直接綁定taskListService.tasks屬性數據
由於PortalOutlet是在父組件中,因此單擊任務列表建立動態組件的邏輯是從父組件響應的
portals-entry.component.ts
@ViewChild('taskListContainer', { read: TaskListComponent })
taskListComponent: TaskListComponent;
ngOnInit() {
this.taskListComponent.openTask = task => {
this.portalCreatTaskModel(task);
};
}
portalCreatTaskModel(task: TaskEntity) {
this.virtualPotalOutlet.detach();
const customerTokens = new WeakMap();
customerTokens.set(TaskEntity, task);
const portalInjector = new PortalInjector(
this.taskListViewContainerRef.injector,
customerTokens
);
const taskModelCompoentPortal = new ComponentPortal<TaskModelComponent>(
TaskModelComponent,
null,
portalInjector
);
this.virtualPotalOutlet.attach(taskModelCompoentPortal);
}
複製代碼
給ComponentPortal的構造函數傳遞了PortalInjector類型的參數portalInjector,PortalInjector繼承自Injector
@ViewChild('taskListContainer', { read: ViewContainerRef })
taskListViewContainerRef: ViewContainerRef;
複製代碼
也就是新的組件的注入器來自於TaskListComponentcustomerTokens.set(TaskEntity, task);
。task-model.component.ts
constructor(
public task: TaskEntity,
private taskListService: TaskListService
) {}
複製代碼
沒錯,是經過注入器注入的方式獲取TaskEntity實例和服務TaskListService的實例taskListService。
這個例子相對複雜,只是想說明能夠給動態建立的組件傳入特定的injector。
想寫Portals的使用主要是看了咱們組件庫中模態框ThyDialog的實現,以爲這些用法比較巧妙,因此想分享出來。
示例倉儲:github.com/pubuzhixing…
組件庫倉儲:github.com/worktile/ng…
angula.cn解釋:表示能夠將一個或多個視圖附着到組件中的容器,能夠包含宿主視圖(當用 createComponent() 方法實例化組件時建立)和內嵌視圖(當用 createEmbeddedView() 方法實例化 TemplateRef 時建立)。
我這裏的理解ViewContainerRef是Angular中的一個邏輯單元,簡單理解它與組件或者頁面中的html元素一一對應只是邏輯形態不一樣,它也有層級只是層級與組件樹的層級不是一一對應,這點我的感受有些難理解,就拿Portals裏面ComponentPortal的實現來講,構造函數裏面能夠傳入一個viewContainerRef,代碼片斷
/**
* A `ComponentPortal` is a portal that instantiates some Component upon attachment.
*/
export class ComponentPortal<T> extends Portal<ComponentRef<T>> {
/**
* [Optional] Where the attached component should live in Angular's *logical* component tree. * 可選參數 關聯的組件應該寄宿的邏輯組件樹的位置 * This is different from where the component *renders*, which is determined by the PortalOutlet. * 這跟組件真正渲染的位置是不一樣的,真正的位置由PortalOutlet決定 * The origin is necessary when the host is outside of the Angular application context. * 當宿主是在Angular上下文環境以外這個參數是必填項 */ viewContainerRef?: ViewContainerRef | null; constructor( component: ComponentType<T>, viewContainerRef?: ViewContainerRef | null, injector?: Injector | null, componentFactoryResolver?: ComponentFactoryResolver | null) { // ... } } 複製代碼
對其中viewContainerRef的註釋進行了簡單的翻譯,但仍是不知道它是怎麼實現邏輯組件樹與真實渲染組件樹設置不一樣層級,通過本身的嘗試當設置viewContainerRef後,組件就渲染在了傳入的viewContainerRef裏面。
屬性
最初由於不瞭解WeakMap而對這個實現迷惑不解,查了WeakMap的相關資料
WeakMap 對象是一組鍵/值對的集合,其中的鍵是弱引用的。其鍵名必須是對象,而值能夠是任意的。
鍵名是對象的弱引用,當對象被回收後,WeakMap自動移除對應的鍵值對,WeakMap結構有助於防止內存泄漏。
能夠與Map對比理解,Map中key能夠是各類類型,而WeakMap必須是對象。
這樣WeakMap就能夠用來在不修改原引用類型對象的基礎上,而擴充該對象的屬性值,而且不影響引用類型對象的垃圾回收,隨該對象的消失,擴充屬性隨之消失。
本文做者:Worktile工程師 楊振興