Angular Elements 及其運做原理

如今,Angular Elements 這個項目已經在社區引發必定程度的討論。這是顯而易見的,由於 Angular Elements 提供了不少開箱即用的、十分強大的功能:javascript

  • 經過使用原生的 HTML 語法來使用 Angular Elements —— 這意味着再也不須要了解 Angular 的相關知識
  • 它是自啓動的,而且一切均可以按預期那樣運做
  • 它符合 Web Components 規範,這意味着它能夠在任何地方使用
  • 雖然你沒有使用 Angular 開發整個網站,但你仍然能夠從 Angular Framework 這個龐大的體系中收益

@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(自定義元素)

要了解更多關於 Custom Elements 的知識,能夠經過 developers.google 中的這篇文章進行學習,文章詳細介紹了與 Custom Elements API 相關的內容。github

這裏針對 Custom Elements,咱們使用一句話來歸納:web

使用 Custom Elements,web 開發者能夠建立一個新的 HTML 標籤、增長已有的 HTML 標籤以及繼承其餘開發者所開發的組件。

原生 Custom Elements

讓咱們來看看下面的例子,咱們想要建立一個擁有 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。函數

將 Angular 組件導出爲 Custom Element

既然咱們已經瞭解了關於實現一個 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) {
  }
}

下一步,咱們要作的是橋接 HelloComponentHelloComponentClass。它們之間的橋會將 Angular Component 和 Custom Element 鏈接起來,如圖所示:

clipboard.png

要完成這座橋,讓咱們來依次實現 Custom Elements API 中所要求的每一個方法,並在這個方法中編寫關於綁定 Angular 的代碼:

callback summary angular part
constructor 初始化內部狀態 進行一些準備工做
connectedCallback 初始化視圖、事件監聽器 加載 Angular 組件
disconnectedCallback 清除視圖、事件監聽器 註銷 Angular 組件
attributeChangedCallback 處理屬性變化 處理 @Input 變化

1. constructor()

咱們須要在 connectedCallback() 方法中初始化 HelloComponent,可是在這以前,咱們須要在 constructor 方法中進行一些準備工做。

順便,關於如何動態構造 Angular 組件能夠經過閱讀Dynamic Components in Angular這篇文章進行了解。它其中闡述的運做機制和咱們這裏使用的如出一轍。

因此,要讓咱們的 Angular 動態組件可以正常工做(須要 componentFactory 可以被編譯),咱們須要將 HelloComponent 添加到 NgModuleentryComponents 屬性(它是一個列表)中去:

@NgModule({
  imports: [
    BrowserModule
  ],
  declarations: [HelloComponent],
  entryComponents: [HelloComponent]
})
export class CustomElementsModule {
  ngDoBootstrap() {}
}

基本上,調用 prepare() 方法會完成兩件事:

  • 它會基於組件的定義初始化一個 factoryComponent 工廠方法
  • 它會基於 Angular 組件的 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); 
  }
}

2. connectedCallback()

在這個回調函數中,咱們將看到:

  • 初始化咱們的 Angular 組件(就如建立動態組件那樣)
  • 設置組件的初始 input 值
  • 在渲染組件時,觸發髒檢查機制
  • 最後,將 HostView 增長到 ApplicationRef

以下是實戰代碼:

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);
  }
}

3. disconnectedCallback()

這個十分容易,咱們僅須要在其中註銷 componentRef 便可:

class AngularCustomElementBridge {
  destroy() {
    this.componentRef.destroy();
  }
}

4. attributeChangedCallback()

當元素屬性發生改變時,咱們須要相應地更新 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();
  }
}

5. Finally, we register the Custom Element

customElements.define('hello-elem', HelloComponentClass);

這是一個可運行的例子連接

總結

這就是根本思想。經過在 Angular 中使用動態組件,咱們簡單實現了 Angular Elements 所提供的基礎功能,重要的是,沒有使用 @angular/element 這個庫。

固然,不要誤解 —— Angular Elements 的功能十分強大。文章中所涉及的全部實現邏輯在 Angular Elements 都已被抽象化,使用這個庫可使咱們的代碼更優雅,可讀性和維護性也更好,同時也更易於擴展。

如下是關於 Angular Elements 中一些模塊的概要以及它們與這篇文章的關聯性:

  • create-custom-element.ts:這個模塊實現了咱們在這篇文章中討論的關於 Custom Element 的幾個回調函數,同時它還會初始化一個 NgElementStrategy 策略類,這個類會做爲鏈接 Angular Component 和 Custom Elements 的橋樑。當前,咱們僅有一個策略 —— component-factory-strategy.ts —— 它的運做機制與本文例子中演示的大同小異。在未來,咱們可能會有其餘策略,而且咱們還能夠實現自定義策略。
  • component-factory-strategy.ts:這個模塊使用一個 component 工廠函數來建立和銷燬組件引用。同時它還會在 input 改變時觸發髒檢查。這個運做過程在上文的例子中也有被說起。

下次咱們將闡述 Angular Elements 經過 Custom Events 輸出事件。

相關文章
相關標籤/搜索