摘要:這些依賴對象也進一步暴露了其設計思想。關鍵功能包括在上下文內掛載在上下文外掛載在上下文外共享數據。在構造必須依賴,所以可以直接創建嵌入視圖,然后手動強制執行變更檢測。提供了兩個指令和。
@angular/material 是 Angular 官方根據 Material Design 設計語言提供的 UI 庫,開發人員在開發 UI 庫時發現很多 UI 組件有著共同的邏輯,所以他們把這些共同邏輯抽出來多帶帶做一個包 @angular/cdk,這個包與 Material Design 設計語言無關,可以被任何人按照其他設計語言構建其他風格的 UI 庫。學習 @angular/material 或 @angular/cdk 這些包的源碼,主要是為了學習大牛們是如何高效使用 TypeScript 語言的;學習他們如何把 RxJS 這個包使用的這么出神入化;最主要是為了學習他們是怎么應用 Angular 框架提供的技術。只有深入研究這些大牛們寫的代碼,才能更快提高自己的代碼質量,這是一件事半功倍的事情。Portal 是什么
最近在學習 React 時,發現 React 提供了 Portals 技術,該技術主要用來把子節點動態的顯示到父節點外的 DOM 節點上,該技術的一個經典用例應該就是 Dialog 了。設想一下在設計 Dialog 時所需要的主要功能點:當點擊一個 button 時,一般需要在 body 標簽前動態掛載一個組件視圖;該 dialog 組件視圖需要共享數據。由此看出,Portal 核心就是在任意一個 DOM 節點內動態生成一個視圖,該 視圖卻可以置于框架上下文環境之外。那 Angular 中有沒有類似相關技術來解決這個問題呢?
Angular Portal 就是用來在任意一個 DOM 節點內動態生成一個視圖,該視圖既可以是一個組件視圖,也可以是一個模板視圖,并且生成的視圖可以掛載在任意一個 DOM 節點,甚至該節點可以置于 Angular 上下文環境之外,也同樣可以與該視圖共享數據。該 Portal 技術主要就涉及兩個簡單對象:PortalOutlet 和 Portal
Portal
抽象類 BasePortalOutlet 是 PortalOutlet 的基本實現,同時包含了三個重要方法:attach 表示把 Portal 掛載到 PortalOutlet 上,并定義了兩個抽象方法,來具體實現掛載組件視圖還是模板視圖:
abstract attachComponentPortal(portal: ComponentPortal ): ComponentRef ; abstract attachTemplatePortal (portal: TemplatePortal ): EmbeddedViewRef ;
detach 表示從 PortalOutlet 中拆卸出該 Portal,而 PortalOutlet 中可以掛載多個 Portal,dispose 表示整體并永久銷毀 PortalOutlet。其中,還有一個重要類 DomPortalOutlet 是 BasePortalOutlet 的子類,可以在 Angular 上下文之外 創建一個 PortalOutlet,并把 Portal 掛載到該 PortalOutlet 上,比如將 body 最后子元素 div 包裝為一個 PortalOutlet,然后將組件視圖或模板視圖掛載到該掛載點上。這里的的難點就是如果該掛載點在 Angular 上下文之外,那掛載點內的 Portal 如何與 Angular 上下文內的組件共享數據。 DomPortalOutlet 還實現了上面的兩個抽象方法:attachComponentPortal 和 attachTemplatePortal,如果對代碼細節感興趣可接著看下文。
現在已經知道了 @angular/cdk/portal 中最重要的兩個核心,即 Portal 和 PortalOutlet,接下來寫一個 demo 看看如何使用 Portal 和 PortalOutlet 來在 Angular 上下文之外 創建一個 ComponentPortal 和 TemplatePortal。
Demo 關鍵功能包括:在 Angular 上下文內 掛載 TemplatePortal/ComponentPortal;在 Angular 上下文外 掛載 TemplatePortal/ComponentPortal;在 Angular 上下文外 共享數據。接下來讓我們逐一實現每個功能點。
Angular 上下文內掛載 Portal在 Angular 上下文內掛載 Portal 比較簡單,首先需要做的第一步就是實例化出一個掛載容器 PortalOutlet,可以通過實例化 DomPortalOutlet 得到該掛載容器。查看 DomPortalOutlet 的構造依賴主要包括:掛載的元素節點 Element,可以通過 @ViewChild DOM 查詢得到該組件內的某一個 DOM 元素;組件工廠解析器 ComponentFactoryResolver,可以通過當前組件構造注入拿到,該解析器是為了當 Portal 是 ComponentPortal 時解析出對應的 Component;當前程序對象 ApplicationRef,主要用來掛載組件視圖;注入器 Injector,這個很重要,如果是在 Angular 上下文外掛載組件視圖,可以用 Injector 來和組件視圖共享數據。
第二步就是使用 ComponentPortal 和 TemplatePortal 包裝對應的組件和模板,需要留意的是 TemplatePortal 還必須依賴 ViewContainerRef 對象來調用 createEmbeddedView() 來創建嵌入視圖。
第三步就是調用 PortalOutlet 的 attach() 方法掛載 Portal,進而根據 Portal 是 ComponentPortal 還是 TemplatePortal 分別調用 attachComponentPortal() 和 attachTemplatePortal() 方法。
通過以上三步,就可以知道該如何設計代碼:
@Component({ selector: "portal-dialog", template: `Component Portal
` }) export class DialogComponent {} @Component({ selector: "app-root", template: `
Open a ComponentPortal Inside Angular Context
Open a TemplatePortal Inside Angular Context
`, }) export class AppComponent { private _appRef: ApplicationRef; constructor(private _componentFactoryResolver: ComponentFactoryResolver, private _injector: Injector, @Inject(DOCUMENT) private _document) {} @ViewChild("_openComponentPortalInsideAngularContext", {read: ViewContainerRef}) _openComponentPortalInsideAngularContext: ViewContainerRef; openComponentPortalInsideAngularContext() { if (!this._appRef) { this._appRef = this._injector.get(ApplicationRef); } // instantiate a DomPortalOutlet const portalOutlet = new DomPortalOutlet(this._openComponentPortalInsideAngularContext.element.nativeElement, this._componentFactoryResolver, this._appRef, this._injector); // instantiate a ComponentPortal Template Portal Inside Angular Context
const componentPortal = new ComponentPortal(DialogComponent); // attach a ComponentPortal to a DomPortalOutlet portalOutlet.attach(componentPortal); } @ViewChild("_templatePortalInsideAngularContext", {read: TemplateRef}) _templatePortalInsideAngularContext: TemplateRef ; @ViewChild("_openTemplatePortalInsideAngularContext", {read: ViewContainerRef}) _openTemplatePortalInsideAngularContext: ViewContainerRef; openTemplatePortalInsideAngularContext() { if (!this._appRef) { this._appRef = this._injector.get(ApplicationRef); } // instantiate a DomPortalOutlet const portalOutlet = new DomPortalOutlet(this._openTemplatePortalInsideAngularContext.element.nativeElement, this._componentFactoryResolver, this._appRef, this._injector); // instantiate a TemplatePortal<> const templatePortal = new TemplatePortal(this._templatePortalInsideAngularContext, this._openTemplatePortalInsideAngularContext); // attach a TemplatePortal to a DomPortalOutlet portalOutlet.attach(templatePortal); } }
查閱上面設計的代碼,發現沒有什么太多新的東西。通過 @ViewChild DOM 查詢到模板對象和視圖容器對象,注意該裝飾器的第二個參數 {read:},用來指定具體查詢哪種標識如 TemplateRef 還是 ViewContainerRef。當然,最重要的技術點還是 attach() 方法的實現,該方法的源碼解析可以接著看下文。
完整代碼可見 demo。
Angular 上下文外掛載 Portal從上文可知道,如果想要把 Portal 掛載到 Angular 上下文外,關鍵是 PortalOutlet 的依賴 outletElement 得處于 Angular 上下文之外。這個 HTMLElement 可以通過 _document.body.appendChild(element) 來手動創建:
let container = this._document.createElement("div"); container.classList.add("component-portal"); container = this._document.body.appendChild(container);
有了處于 Angular 上下文之外的一個 Element,后面的設計步驟就和上文完全一樣:實例化一個處于 Angular 上下文之外的 PortalOutlet,然后掛載 ComponentPortal 和 TemplatePortal:
@Component({ selector: "app-root", template: `Open a ComponentPortal Outside Angular Context
Open a TemplatePortal Outside Angular Context
`, }) export class AppComponent { ... openComponentPortalOutSideAngularContext() { let container = this._document.createElement("div"); container.classList.add("component-portal"); container = this._document.body.appendChild(container); if (!this._appRef) { this._appRef = this._injector.get(ApplicationRef); } // instantiate a DomPortalOutlet const portalOutlet = new DomPortalOutlet(container, this._componentFactoryResolver, this._appRef, this._injector); // instantiate a ComponentPortal Template Portal Outside Angular Context
const componentPortal = new ComponentPortal(DialogComponent); // attach a ComponentPortal to a DomPortalOutlet portalOutlet.attach(componentPortal); } @ViewChild("_templatePortalOutsideAngularContext", {read: TemplateRef}) _template: TemplateRef ; @ViewChild("_templatePortalOutsideAngularContext", {read: ViewContainerRef}) _viewContainerRef: ViewContainerRef; openTemplatePortalOutSideAngularContext() { let container = this._document.createElement("div"); container.classList.add("template-portal"); container = this._document.body.appendChild(container); if (!this._appRef) { this._appRef = this._injector.get(ApplicationRef); } // instantiate a DomPortalOutlet const portalOutlet = new DomPortalOutlet(container, this._componentFactoryResolver, this._appRef, this._injector); // instantiate a TemplatePortal<> const templatePortal = new TemplatePortal(this._template, this._viewContainerRef); // attach a TemplatePortal to a DomPortalOutlet portalOutlet.attach(templatePortal); } ...
通過上面代碼,就可以在 Angular 上下文之外創建一個視圖,這個技術對創建 Dialog 會非常有用。
完整代碼可見 demo。
Angular 上下文外共享數據最難點還是如何與處于 Angular 上下文外的 Portal 共享數據,這個問題需要根據 ComponentPortal 還是 TemplatePortal 分別處理。其中,如果是 TemplatePortal,解決方法卻很簡單,注意觀察 TemplatePortal 的構造依賴,發現存在第三個可選參數 context,難道是用來向 TemplatePortal 里傳送共享數據的?沒錯,的確如此。可以查看 DomPortalOutlet.attachTemplatePortal() 的 75 行,就是把 portal.context 傳給組件視圖內作為共享數據使用,既然如此,TemplatePortal 共享數據問題就很好解決了:
@Component({ selector: "app-root", template: `Open a TemplatePortal Outside Angular Context with Sharing Data
`, }) export class AppComponent { sharingTemplateData: string = "lx1035"; @ViewChild("_templatePortalOutsideAngularContextWithSharingData", {read: TemplateRef}) _templateWithSharingData: TemplateRef Template Portal Outside Angular Context, the Sharing Data is {{name}}
; @ViewChild("_templatePortalOutsideAngularContextWithSharingData", {read: ViewContainerRef}) _viewContainerRefWithSharingData: ViewContainerRef; setTemplateSharingData(value) { this.sharingTemplateData = value; } openTemplatePortalOutSideAngularContextWithSharingData() { let container = this._document.createElement("div"); container.classList.add("template-portal-with-sharing-data"); container = this._document.body.appendChild(container); if (!this._appRef) { this._appRef = this._injector.get(ApplicationRef); } // instantiate a DomPortalOutlet const portalOutlet = new DomPortalOutlet(container, this._componentFactoryResolver, this._appRef, this._injector); // instantiate a TemplatePortal const templatePortal = new TemplatePortal(this._templateWithSharingData, this._viewContainerRefWithSharingData, {name: this.sharingTemplateData}); // <--- key point // attach a TemplatePortal to a DomPortalOutlet portalOutlet.attach(templatePortal); } ...
那 ComponentPortal 呢?查看 ComponentPortal 的第三個構造依賴 Injector,它依賴的是注入器。TemplatePortal 的第三個參數 context 解決了共享數據問題,那 ComponentPortal 可不可以通過第三個參數注入器解決共享數據問題?沒錯,完全可以。可以構造一個自定義的 Injector,把共享數據存儲到 Injector 里,然后 ComponentPortal 從 Injector 中取出該共享數據。查看 Portal 的源碼包,官方還很人性的提供了一個 PortalInjector 類供開發者實例化一個自定義注入器。現在思路已經有了,看看代碼具體實現:
let DATA = new InjectionToken("Sharing Data with Component Portal"); @Component({ selector: "portal-dialog-sharing-data", template: ` Component Portal Sharing Data is: {{data}}
` }) export class DialogComponentWithSharingData { constructor(@Inject(DATA) public data: any) {} // <--- key point } @Component({ selector: "app-root", template: `
Open a ComponentPortal Outside Angular Context with Sharing Data
`, }) export class AppComponent { ... sharingComponentData: string = "lx1036"; setComponentSharingData(value) { this.sharingComponentData = value; } openComponentPortalOutSideAngularContextWithSharingData() { let container = this._document.createElement("div"); container.classList.add("component-portal-with-sharing-data"); container = this._document.body.appendChild(container); if (!this._appRef) { this._appRef = this._injector.get(ApplicationRef); } // Sharing data by Injector(Dependency Injection) const map = new WeakMap(); map.set(DATA, this.sharingComponentData); // <--- key point const injector = new PortalInjector(this._injector, map); // instantiate a DomPortalOutlet const portalOutlet = new DomPortalOutlet(container, this._componentFactoryResolver, this._appRef, injector); // <--- key point // instantiate a ComponentPortalconst componentPortal = new ComponentPortal(DialogComponentWithSharingData); // attach a ComponentPortal to a DomPortalOutlet portalOutlet.attach(componentPortal); }
通過 Injector 就可以實現 ComponentPortal 與 AppComponent 共享數據了,該技術對于 Dialog 實現尤其重要,設想對于 Dialog 彈出框,需要在 Dialog 中展示來自于外部組件的數據依賴,同時 Dialog 還需要把數據傳回給外部組件。Angular Material 官方就在 @angular/cdk/portal 基礎上構造一個 @angular/cdk/overlay 包,專門處理類似覆蓋層組件的共同問題,這些類似覆蓋層組件如 Dialog, Tooltip, SnackBar 等等。
完整代碼可見 demo。
解析 attach() 源碼不管是 ComponentPortal 還是 TemplatePortal,PortalOutlet 都會調用 attach() 方法把 Portal 掛載進來,具體掛載過程是怎樣的?查看 BasePortalOutlet 的 attach() 的源碼實現:
/** Attaches a portal. */ attach(portal: Portal): any { ... if (portal instanceof ComponentPortal) { this._attachedPortal = portal; return this.attachComponentPortal(portal); } else if (portal instanceof TemplatePortal) { this._attachedPortal = portal; return this.attachTemplatePortal(portal); } ... }
attach() 主要邏輯就是根據 Portal 類型分別調用 attachComponentPortal 和 attachTemplatePortal 方法。下面將分別查看兩個方法的實現。
attachComponentPortal()還是以 DomPortalOutlet 類為例,如果掛載的是組件視圖,就會調用 attachComponentPortal() 方法,第一步就是通過組件工廠解析器 ComponentFactoryResolver 解析出組件工廠對象:
attachComponentPortal(portal: ComponentPortal ): ComponentRef { let componentFactory = this._componentFactoryResolver.resolveComponentFactory(portal.component); let componentRef: ComponentRef ; ...
然后如果 ComponentPortal 定義了 ViewContainerRef,就調用 ViewContainerRef.createComponent 創建組件視圖,并依次插入到該視圖容器中,最后設置 ComponentPortal 銷毀回調:
if (portal.viewContainerRef) { componentRef = portal.viewContainerRef.createComponent( componentFactory, portal.viewContainerRef.length, portal.injector || portal.viewContainerRef.parentInjector); this.setDisposeFn(() => componentRef.destroy()); }
如果 ComponentPortal 沒有定義 ViewContainerRef,就用上文的組件工廠 ComponentFactory 來創建組件視圖,但還不夠,還需要把組件視圖掛載到組件樹上,并設置 ComponentPortal 銷毀回調,回調包括需要從組件樹中拆卸出該視圖,并銷毀該組件:
else { componentRef = componentFactory.create(portal.injector || this._defaultInjector); this._appRef.attachView(componentRef.hostView); this.setDisposeFn(() => { this._appRef.detachView(componentRef.hostView); componentRef.destroy(); }); }
需要注意的是 this._appRef.attachView(componentRef.hostView);,當把組件視圖掛載到組件樹時會自動觸發變更檢測(change detection)。
目前組件視圖只是掛載到視圖容器里,最后還需要在 DOM 中渲染出來:
this.outletElement.appendChild(this._getComponentRootNode(componentRef));
這里需要了解的是,視圖容器 ViewContainerRef、視圖 ViewRef、組件視圖 ComponentRef.hostView、嵌入視圖 EmbeddedViewRef 的關系。組件視圖和嵌入視圖都是視圖對象的具體形態,而視圖是需要掛載到視圖容器內才能正常工作,視圖容器內可以掛載多個視圖,而所謂的視圖容器就是包裝任意一個 DOM 元素所生成的對象。視圖容器可以通過 @ViewChild 或者當前組件構造注入獲得,如果是通過 @ViewChild 查詢拿到當前組件模板內某個元素如 div,那 Angular 就會根據這個 div 元素生成一個視圖容器;如果是當前組件構造注入獲得,那就根據當前組件掛載點如 app-root 生成視圖容器。所有的視圖都會依次作為子節點掛載到容器內。attachTemplatePortal()
根據上文的類似設計,掛載 TemplatePortal 的源碼 就很簡單了。在構造 TemplatePortal 必須依賴 ViewContainerRef,所以可以直接創建嵌入視圖 EmbeddedViewRef,然后手動強制執行變更檢測。不像上文 this._appRef.attachView(componentRef.hostView); 會檢測整個組件樹,這里 viewRef.detectChanges(); 只檢測該組件及其子組件:
attachTemplatePortal(portal: TemplatePortal ): EmbeddedViewRef { let viewContainer = portal.viewContainerRef; let viewRef = viewContainer.createEmbeddedView(portal.templateRef, portal.context); viewRef.detectChanges();
最后在 DOM 渲染出視圖:
viewRef.rootNodes.forEach(rootNode => this.outletElement.appendChild(rootNode));
現在,就可以理解了如何把 Portal 掛載到 PortalOutlet 容器內的具體過程,它并不復雜。
Portal 快捷指令讓我們重新回顧下 Portal 技術要解決的問題以及如何實現:Portal 是為了解決可以在 Angular 框架執行上下文之外動態創建子視圖,首先需要先實例化出 PortalOutlet 對象,然后實例化出一個 ComponentPortal 或 TemplatePortal,最后把 Portal 掛載到 PortalOutlet 上。整個過程非常簡單,但是難道 @angular/cdk/portal 沒有提供什么快捷方式,避免讓開發者寫大量重復代碼么?有。@angular/cdk/portal 提供了兩個指令:CdkPortal 和 CdkPortalOutlet。該兩個指令會隱藏所有實現細節,開發者只需要簡單調用就行,使用方式可以查看官方 demo。
demo 實踐過程中,發現兩個問題:組件視圖都會多產生一個 p 標簽;AppComponent 模板中掛載點作為 ViewContainerRef 時,掛載點還不能為 ng-template 和 ng-container,和印象中有出入。有時間在查找,誰知道原因,也可留言幫助解答,先謝了。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/107849.html
摘要:模塊庫開發實例隨著前端框架的誕生,也會隨之出現一些組件庫,方便日常業務開發。在瀏覽器中,渲染是將模型映射到視圖的過程。然而視圖可以是頁面中的段落表單按鈕等其他元素,這些頁面元素內部使用來表示。 angular模塊庫開發實例 隨著前端框架的誕生,也會隨之出現一些組件庫,方便日常業務開發。今天就聊聊angular4組件庫開發流程。 下圖是button組件的基礎文件。 showImg(htt...
摘要:但是,最后一步,事件怎么綁定呢這塊沒有深入研究了,不過我想,應該這樣去實現也是沒有問題的。的具體做法是,把方法放到了一個叫做的組件上去實現這個功能,然后再把內容放進這個組件。其他的邏輯比如顯示隱藏之類,全部都放到組件自身上去實現。 1、Dialog組件提供什么功能,解決什么問題? zent的Dialog組件,使用姿勢是這樣的(代碼摘自zent官方文檔:https://www.youza...
摘要:谷歌在之后的版本加入了服務。但對于不能訪問谷歌服務器的地區,問題就來了如果谷歌谷歌服務認為網絡無法聯網,就不會自動連接到該熱點。并且讓網絡的標志上面顯示感嘆號標志。這個感嘆號會使廣大強迫癥晚期患者無法接受。 本文原作者 長鳴鳥 ,未經同意,轉載不帶名的嚴重鄙視。谷歌在Android5.0之后的版本加入了CaptivePotalLogin服務。本服務的功能是檢查網絡連接互聯網情況,主要針...
摘要:一例子看到一個有趣的現象,就是多層嵌套的數組經過后,平鋪成了,接下來以該例解析二作用源碼進行基本的判斷和初始化后,調用該方法就是重命名了,即解析注意,該數組在里面滾了一圈后,會結果三作用的包裹器源碼第一次第二次如果字符串中有連續多個的話 showImg(https://segmentfault.com/img/remote/1460000019968077?w=1240&h=698);...
閱讀 3428·2021-10-20 13:49
閱讀 2803·2021-09-29 09:34
閱讀 3700·2021-09-01 11:29
閱讀 3087·2019-08-30 11:01
閱讀 844·2019-08-29 17:10
閱讀 883·2019-08-29 12:48
閱讀 2786·2019-08-29 12:40
閱讀 1358·2019-08-29 12:30