如今,Angular Elements 這個項目已經在社區引發必定程度的討論。這是顯而易見的,由於 Angular Elements 提供了不少開箱即用的、十分強大的功能:javascript
@angular/elements
這個包提供可將 Angular 組件轉化爲原生 Web Components 的功能,它基於瀏覽器的 Custom Elements API 實現。Angular Elements 提供一種更簡潔、對開發者更友善、更快樂地開發動態組件的方式 —— 在幕後它基於一樣的機制(指建立動態組件),但隱藏了許多樣板代碼。java
關於如何經過 @angular/elements
建立一個 Custom Element,已經有大量的文章進行闡述,因此在這篇文章將深刻一點,對它在 Angular 中的具體工做原理進行剖析。這也是咱們開始研究 Angular Elements 的一系列文章的緣由,咱們將在其中詳細解釋 Angular 如何在 Angular Elements 的幫助下實現 Custom Elements API。git
要了解更多關於 Custom Elements 的知識,能夠經過 developers.google 中的這篇文章進行學習,文章詳細介紹了與 Custom Elements API 相關的內容。github
這裏針對 Custom Elements,咱們使用一句話來歸納:web
使用 Custom Elements,web 開發者能夠建立一個新的 HTML 標籤、增長已有的 HTML 標籤以及繼承其餘開發者所開發的組件。
讓咱們來看看下面的例子,咱們想要建立一個擁有 name
屬性的 app-hello
HTML 標籤。能夠經過 Custom Elements API 來完成這件事。在文章的後續章節,咱們將演示如何使用 Angular 組件的 @Input
裝飾器與 這個 name
屬性保持同步。可是如今,咱們不須要使用 Angular Elements 或者 ShadowDom 或者使用任何關於 Angular 的東西來建立一個 Custom Element,咱們僅使用原生的 Custom Components API。瀏覽器
首先,這是咱們的 HTML 標記:app
<hello-elem name="Custom Elements"></hello-elem>
要實現一個 Custom Element,咱們須要分別實現以下在標準中定義的 hooks:dom
callback | summary |
---|---|
constructor | 若是須要的話,可在其中初始化 state 或者 shadowRoot,在這篇文章中,咱們不須要 |
connectedCallback | 在元素被添加到 DOM 中時會被調用,咱們將在這個 hook 中初始化咱們的 DOM 結構和事件監聽器 |
disconnectedCallback | 在元素從 DOM 中被移除時被調用,咱們將在這個 hook 中清除咱們的 DOM 結構和事件監聽器 |
attributeChangedCallback | 在元素屬性變化時被調用,咱們將在這個 hook 中更新咱們內部的 dom 元素或者基於屬性改變後的狀態 |
以下是咱們關於 Hello
Custom Element 的實現代碼:ide
class AppHello extends HTMLElement { constructor() { super(); } // 這裏定義了那些須要被觀察的屬性,當這些屬性改變時,attributeChangedCallback 這個 hook 會被觸發 static get observedAttributes() {return ['name']; } // getter to do a attribute -> property reflection get name() { return this.getAttribute('name'); } // setter to do a property -> attribute reflection // 經過 setter 來完成類屬性到元素屬性的映射操做 set name(val) { this.setAttribute('name', val); } connectedCallback() { this.div = document.createElement('div'); this.text = document.createTextNode(this.name || ''); this.div.appendChild(this.text); this.appendChild(this.div); } disconnectedCallback() { this.removeChild(this.div); } attributeChangedCallback(attrName, oldVal, newVal) { if (attrName === 'name' && this.text) { this.text.textContent = newVal; } } } customElements.define('hello-elem', AppHello);
這裏是可運行實例的連接。這樣咱們就實現了初版的 Custom Element,回顧一下,這個 app-hellp
標籤包含一個文本節點,而且這個節點將會渲染經過 app-hello
標籤 name
屬性傳遞進來的任何內容,這一切僅僅基於原生 javascript。函數
既然咱們已經瞭解了關於實現一個 HTML Custom Element 所涉及的內容,讓咱們來使用 Angular實現一個相同功能的組件,以後再使它成爲一個可用的 Custom Element。
首先,讓咱們從一個簡單的 Angular 組件開始:
import { Component, Input } from '@angular/core'; @Component({ selector: 'app-hello', template: `<div>{{name}}</div>` }) export class HelloComponent { @Input() name: string; }
正如你所見,它和上面的例子在功能上如出一轍。
如今,要將這個組件包裝爲一個 Custom Element,咱們須要建立一個 wrapper class 並實現全部 Custom Elements 中定義的 hooks:
class HelloComponentClass extends HTMLElement { constructor() { super(); } static get observedAttributes() { } connectedCallback() { } disconnectedCallback() { } attributeChangedCallback(attrName, oldVal, newVal) { } }
下一步,咱們要作的是橋接 HelloComponent
和 HelloComponentClass
。它們之間的橋會將 Angular Component 和 Custom Element 鏈接起來,如圖所示:
要完成這座橋,讓咱們來依次實現 Custom Elements API 中所要求的每一個方法,並在這個方法中編寫關於綁定 Angular 的代碼:
callback | summary | angular part |
---|---|---|
constructor | 初始化內部狀態 | 進行一些準備工做 |
connectedCallback | 初始化視圖、事件監聽器 | 加載 Angular 組件 |
disconnectedCallback | 清除視圖、事件監聽器 | 註銷 Angular 組件 |
attributeChangedCallback | 處理屬性變化 | 處理 @Input 變化 |
咱們須要在 connectedCallback()
方法中初始化 HelloComponent,可是在這以前,咱們須要在 constructor 方法中進行一些準備工做。
順便,關於如何動態構造 Angular 組件能夠經過閱讀Dynamic Components in Angular這篇文章進行了解。它其中闡述的運做機制和咱們這裏使用的如出一轍。
因此,要讓咱們的 Angular 動態組件可以正常工做(須要 componentFactory
可以被編譯),咱們須要將 HelloComponent
添加到 NgModule
的 entryComponents
屬性(它是一個列表)中去:
@NgModule({ imports: [ BrowserModule ], declarations: [HelloComponent], entryComponents: [HelloComponent] }) export class CustomElementsModule { ngDoBootstrap() {} }
基本上,調用 prepare()
方法會完成兩件事:
inputs
初始化 observedAttributes
,以便咱們在 attributeChangedCallback()
中完成咱們須要作的事class AngularCustomElementBridge { prepare(injector, component) { this.componentFactory = injector.get(ComponentFactoryResolver).resolveComponentFactory(component); // 咱們使用 templateName 來處理 @Input('aliasName') 這種情形 this.observedAttributes = componentFactory.inputs.map(input => input.templateName); } }
在這個回調函數中,咱們將看到:
以下是實戰代碼:
class AngularCustomElementBridge { initComponent(element: HTMLElement) { // 首先咱們須要 componentInjector 來初始化組件 // 這裏的 injector 是 Custom Element 外部的注入器實例,調用者能夠在這個實例中註冊 // 他們本身的 providers const componentInjector = Injector.create([], this.injector); this.componentRef = this.componentFactory.create(componentInjector, null, element); // 而後咱們要檢查是否須要初始化組件的 input 的值 // 在本例中,在 Angular Element 被加載以前,user 可能已經設置了元素的屬性 // 這些值被保存在 initialInputValues 這個 map 結構中 this.componentFactory.inputs.forEach(prop => this.componentRef.instance[prop.propName] = this.initialInputValues[prop.propName]); // 以後咱們會觸發髒檢查,這樣組件在事件循環的下一個週期會被渲染 this.changeDetectorRef.detectChanges(); this.applicationRef = this.injector.get(ApplicationRef); // 最後,咱們使用 attachView 方法將組件的 HostView 添加到 applicationRef 中 this.applicationRef.attachView(this.componentRef.hostView); } }
這個十分容易,咱們僅須要在其中註銷 componentRef
便可:
class AngularCustomElementBridge { destroy() { this.componentRef.destroy(); } }
當元素屬性發生改變時,咱們須要相應地更新 Angular 組件並觸發髒檢查:
class AngularCustomElementBridge { setInputValue(propName, value) { if (!this.componentRef) { this.initialInputValues[propName] = value; return; } if (this.componentRef[propName] === value) { return; } this.componentRef[propName] = value; this.changeDetectorRef.detectChanges(); } }
customElements.define('hello-elem', HelloComponentClass);
這是一個可運行的例子連接。
這就是根本思想。經過在 Angular 中使用動態組件,咱們簡單實現了 Angular Elements 所提供的基礎功能,重要的是,沒有使用 @angular/element 這個庫。
固然,不要誤解 —— Angular Elements 的功能十分強大。文章中所涉及的全部實現邏輯在 Angular Elements 都已被抽象化,使用這個庫可使咱們的代碼更優雅,可讀性和維護性也更好,同時也更易於擴展。
如下是關於 Angular Elements 中一些模塊的概要以及它們與這篇文章的關聯性:
component-factory-strategy.ts
—— 它的運做機制與本文例子中演示的大同小異。在未來,咱們可能會有其餘策略,而且咱們還能夠實現自定義策略。下次咱們將闡述 Angular Elements 經過 Custom Events 輸出事件。