Angular9 元素

2020-07-01 15:13 更新

概覽

Angular 元素就是打包成自定義元素的 Angular 組件。所謂自定義元素就是一套與具體框架無關(guān)的用于定義新 HTML 元素的 Web 標(biāo)準(zhǔn)。

自定義元素這項(xiàng)特性目前受到了 Chrome、Edge(基于 Chromium 的版本)、Opera 和 Safari 的支持,在其它瀏覽器中也能通過膩?zhàn)幽_本(參見瀏覽器支持)加以支持。 自定義元素?cái)U(kuò)展了 HTML,它允許你定義一個(gè)由 JavaScript 代碼創(chuàng)建和控制的標(biāo)簽。 瀏覽器會(huì)維護(hù)一個(gè)自定義元素的注冊(cè)表 CustomElementRegistry,它把一個(gè)可實(shí)例化的 JavaScript 類映射到 HTML 標(biāo)簽上。

@angular/elements 包導(dǎo)出了一個(gè) createCustomElement() API,它在 Angular 組件接口與變更檢測(cè)功能和內(nèi)置 DOM API 之間建立了一個(gè)橋梁。

把組件轉(zhuǎn)換成自定義元素可以讓所有所需的 Angular 基礎(chǔ)設(shè)施都在瀏覽器中可用。 創(chuàng)建自定義元素的方式簡單直觀,它會(huì)自動(dòng)把你組件定義的視圖連同變更檢測(cè)與數(shù)據(jù)綁定等 Angular 的功能映射為相應(yīng)的原生 HTML 等價(jià)物。

使用自定義元素

自定義元素會(huì)自舉 —— 它們?cè)谔砑拥?DOM 中時(shí)就會(huì)自行啟動(dòng)自己,并在從 DOM 中移除時(shí)自行銷毀自己。一旦自定義元素添加到了任何頁面的 DOM 中,它的外觀和行為就和其它的 HTML 元素一樣了,不需要對(duì) Angular 的術(shù)語或使用約定有任何特殊的了解。

  • Angular 應(yīng)用中的簡易動(dòng)態(tài)內(nèi)容

把組件轉(zhuǎn)換成自定義元素為你在 Angular 應(yīng)用中創(chuàng)建動(dòng)態(tài) HTML 內(nèi)容提供了一種簡單的方式。 在 Angular 應(yīng)用中,你直接添加到 DOM 中的 HTML 內(nèi)容是不會(huì)經(jīng)過 Angular 處理的,除非你使用動(dòng)態(tài)組件來借助自己的代碼把 HTML 標(biāo)簽與你的應(yīng)用數(shù)據(jù)關(guān)聯(lián)起來并參與變更檢測(cè)。而使用自定義組件,所有這些裝配工作都是自動(dòng)的。

  • 富內(nèi)容應(yīng)用

如果你有一個(gè)富內(nèi)容應(yīng)用(比如正在展示本文檔的這個(gè)),自定義元素能讓你的內(nèi)容提供者使用復(fù)雜的 Angular 功能,而不要求他了解 Angular 的知識(shí)。比如,像本文檔這樣的 Angular 指南是使用 Angular 導(dǎo)航工具直接添加到 DOM 中的,但是其中可以包含特殊的元素,比如 <code-snippet>,它可以執(zhí)行復(fù)雜的操作。 你所要告訴你的內(nèi)容提供者的一切,就是這個(gè)自定義元素的語法。他們不需要了解關(guān)于 Angular 的任何知識(shí),也不需要了解你的組件的數(shù)據(jù)結(jié)構(gòu)或?qū)崿F(xiàn)。

工作原理

使用 createCustomElement() 函數(shù)來把組件轉(zhuǎn)換成一個(gè)可注冊(cè)成瀏覽器中自定義元素的類。 注冊(cè)完這個(gè)配置好的類之后,你就可以在內(nèi)容中像內(nèi)置 HTML 元素一樣使用這個(gè)新元素了,比如直接把它加到 DOM 中:

<my-popup message="Use Angular!"></my-popup>

當(dāng)你的自定義元素放進(jìn)頁面中時(shí),瀏覽器會(huì)創(chuàng)建一個(gè)已注冊(cè)類的實(shí)例。其內(nèi)容是由組件模板提供的,它使用 Angular 模板語法,并且使用組件和 DOM 數(shù)據(jù)進(jìn)行渲染。組件的輸入屬性(Property)對(duì)應(yīng)于該元素的輸入屬性(Attribute)。

把組件轉(zhuǎn)換成自定義元素

Angular 提供了 createCustomElement() 函數(shù),以支持把 Angular 組件及其依賴轉(zhuǎn)換成自定義元素。該函數(shù)會(huì)收集該組件的 Observable 型屬性,提供瀏覽器創(chuàng)建和銷毀實(shí)例時(shí)所需的 Angular 功能,還會(huì)對(duì)變更進(jìn)行檢測(cè)并做出響應(yīng)。

這個(gè)轉(zhuǎn)換過程實(shí)現(xiàn)了 NgElementConstructor 接口,并創(chuàng)建了一個(gè)構(gòu)造器類,用于生成該組件的一個(gè)自舉型實(shí)例。

然后用 JavaScript 的 customElements.define() 函數(shù)把這個(gè)配置好的構(gòu)造器和相關(guān)的自定義元素標(biāo)簽注冊(cè)到瀏覽器的 CustomElementRegistry 中。 當(dāng)瀏覽器遇到這個(gè)已注冊(cè)元素的標(biāo)簽時(shí),就會(huì)使用該構(gòu)造器來創(chuàng)建一個(gè)自定義元素的實(shí)例。

映射

寄宿著 Angular 組件的自定義元素在組件中定義的"數(shù)據(jù)及邏輯"和標(biāo)準(zhǔn)的 DOM API 之間建立了一座橋梁。組件的屬性和邏輯會(huì)直接映射到 HTML 屬性和瀏覽器的事件系統(tǒng)中。

  • 用于創(chuàng)建的 API 會(huì)解析該組件,以查找輸入屬性(Property),并在這個(gè)自定義元素上定義相應(yīng)的屬性(Attribute)。 它把屬性名轉(zhuǎn)換成與自定義元素兼容的形式(自定義元素不區(qū)分大小寫),生成的屬性名會(huì)使用中線分隔的小寫形式。 比如,對(duì)于帶有 @Input('myInputProp') inputProp 的組件,其對(duì)應(yīng)的自定義元素會(huì)帶有一個(gè) my-input-prop 屬性。

  • 組件的輸出屬性會(huì)用 HTML 自定義事件的形式進(jìn)行分發(fā),自定義事件的名字就是這個(gè)輸出屬性的名字。 比如,對(duì)于帶有 @Output() valueChanged = new EventEmitter() 屬性的組件,其相應(yīng)的自定義元素將會(huì)分發(fā)名叫 "valueChanged" 的事件,事件中所攜帶的數(shù)據(jù)存儲(chǔ)在該事件對(duì)象的 detail 屬性中。 如果你提供了別名,就改用這個(gè)別名。比如,@Output('myClick') clicks = new EventEmitter<string>(); 會(huì)導(dǎo)致分發(fā)名為 "myClick" 事件。

自定義元素的瀏覽器支持

最近開發(fā)的 Web 平臺(tái)特性:自定義元素目前在一些瀏覽器中實(shí)現(xiàn)了原生支持,而其它瀏覽器或者尚未決定,或者已經(jīng)制訂了計(jì)劃。

瀏覽器 自定義元素支持
Chrome 原生支持。
Edge (基于 Chromium 的) 原生支持。
Firefox 原生支持。
Opera 原生支持。
Safari 原生支持。

對(duì)于原生支持了自定義元素的瀏覽器,該規(guī)范要求開發(fā)人員使用 ES2016 的類來定義自定義元素 —— 開發(fā)人員可以在項(xiàng)目的 TypeScript 配置文件中設(shè)置 target: "es2015" 屬性來滿足這一要求。并不是所有瀏覽器都支持自定義元素和 ES2015,開發(fā)人員也可以選擇使用膩?zhàn)幽_本來讓它支持老式瀏覽器和 ES5 的代碼。

使用 Angular CLI 可以自動(dòng)為你的項(xiàng)目添加正確的膩?zhàn)幽_本:ng add @angular/elements --project=*your_project_name*。

范例:彈窗服務(wù)

以前,如果你要在運(yùn)行期間把一個(gè)組件添加到應(yīng)用中,就不得不定義動(dòng)態(tài)組件。你還要把動(dòng)態(tài)組件添加到模塊的 entryComponents 列表中,以便應(yīng)用在啟動(dòng)時(shí)能找到它,然后還要加載它、把它附加到 DOM 中的元素上,并且裝配所有的依賴、變更檢測(cè)和事件處理,詳見動(dòng)態(tài)組件加載器。

用 Angular 自定義組件會(huì)讓這個(gè)過程更簡單、更透明。它會(huì)自動(dòng)提供所有基礎(chǔ)設(shè)施和框架,而你要做的就是定義所需的各種事件處理邏輯。(如果你不準(zhǔn)備在應(yīng)用中直接用它,還要把該組件在編譯時(shí)排除出去。)

這個(gè)彈窗服務(wù)的范例應(yīng)用(見后面)定義了一個(gè)組件,你可以動(dòng)態(tài)加載它也可以把它轉(zhuǎn)換成自定義組件。

  • "popup.component.ts" 定義了一個(gè)簡單的彈窗元素,用于顯示一條輸入消息,附帶一些動(dòng)畫和樣式。

  • "popup.service.ts" 創(chuàng)建了一個(gè)可注入的服務(wù),它提供了兩種方式來執(zhí)行 PopupComponent:作為動(dòng)態(tài)組件或作為自定義元素。注意動(dòng)態(tài)組件的方式需要更多的代碼來做搭建工作。

  • "app.module.ts" 把 PopupComponent 添加到模塊的 entryComponents 列表中,而從編譯過程中排除它,以消除啟動(dòng)時(shí)的警告和錯(cuò)誤。

  • "app.component.ts" 定義了該應(yīng)用的根組件,它借助 PopupService 在運(yùn)行時(shí)把這個(gè)彈窗添加到 DOM 中。在應(yīng)用運(yùn)行期間,根組件的構(gòu)造函數(shù)會(huì)把 PopupComponent 轉(zhuǎn)換成自定義元素。

為了對(duì)比,這個(gè)范例中同時(shí)演示了這兩種方式。一個(gè)按鈕使用動(dòng)態(tài)加載的方式添加彈窗,另一個(gè)按鈕使用自定義元素的方式。可以看到,兩者的結(jié)果是一樣的,其差別只是準(zhǔn)備過程不同。

  1. Path:"src/app/popup.component.ts"。

    import { Component, EventEmitter, Input, Output } from '@angular/core';
    import { animate, state, style, transition, trigger } from '@angular/animations';


    @Component({
      selector: 'my-popup',
      template: `
        <span>Popup: {{message}}</span>
        <button (click)="closed.next()">✖</button>
      `,
      host: {
        '[@state]': 'state',
      },
      animations: [
        trigger('state', [
          state('opened', style({transform: 'translateY(0%)'})),
          state('void, closed', style({transform: 'translateY(100%)', opacity: 0})),
          transition('* => *', animate('100ms ease-in')),
        ])
      ],
      styles: [`
        :host {
          position: absolute;
          bottom: 0;
          left: 0;
          right: 0;
          background: #009cff;
          height: 48px;
          padding: 16px;
          display: flex;
          justify-content: space-between;
          align-items: center;
          border-top: 1px solid black;
          font-size: 24px;
        }


        button {
          border-radius: 50%;
        }
      `]
    })
    export class PopupComponent {
      state: 'opened' | 'closed' = 'closed';


      @Input()
      set message(message: string) {
        this._message = message;
        this.state = 'opened';
      }
      get message(): string { return this._message; }
      _message: string;


      @Output()
      closed = new EventEmitter();
    }

  1. Path:"src/app/popup.service.ts"。

    import { ApplicationRef, ComponentFactoryResolver, Injectable, Injector } from '@angular/core';
    import { NgElement, WithProperties } from '@angular/elements';
    import { PopupComponent } from './popup.component';




    @Injectable()
    export class PopupService {
      constructor(private injector: Injector,
                  private applicationRef: ApplicationRef,
                  private componentFactoryResolver: ComponentFactoryResolver) {}


      // Previous dynamic-loading method required you to set up infrastructure
      // before adding the popup to the DOM.
      showAsComponent(message: string) {
        // Create element
        const popup = document.createElement('popup-component');


        // Create the component and wire it up with the element
        const factory = this.componentFactoryResolver.resolveComponentFactory(PopupComponent);
        const popupComponentRef = factory.create(this.injector, [], popup);


        // Attach to the view so that the change detector knows to run
        this.applicationRef.attachView(popupComponentRef.hostView);


        // Listen to the close event
        popupComponentRef.instance.closed.subscribe(() => {
          document.body.removeChild(popup);
          this.applicationRef.detachView(popupComponentRef.hostView);
        });


        // Set the message
        popupComponentRef.instance.message = message;


        // Add to the DOM
        document.body.appendChild(popup);
      }


      // This uses the new custom-element method to add the popup to the DOM.
      showAsElement(message: string) {
        // Create element
        const popupEl: NgElement & WithProperties<PopupComponent> = document.createElement('popup-element') as any;


        // Listen to the close event
        popupEl.addEventListener('closed', () => document.body.removeChild(popupEl));


        // Set the message
        popupEl.message = message;


        // Add to the DOM
        document.body.appendChild(popupEl);
      }
    }

  1. Path:"src/app/app.module.ts"。

    import { NgModule } from '@angular/core';
    import { BrowserModule } from '@angular/platform-browser';
    import { BrowserAnimationsModule } from '@angular/platform-browser/animations';


    import { AppComponent } from './app.component';
    import { PopupComponent } from './popup.component';
    import { PopupService } from './popup.service';


    // Include the `PopupService` provider,
    // but exclude `PopupComponent` from compilation,
    // because it will be added dynamically.


    @NgModule({
      imports: [BrowserModule, BrowserAnimationsModule],
      providers: [PopupService],
      declarations: [AppComponent, PopupComponent],
      bootstrap: [AppComponent],
      entryComponents: [PopupComponent],
    })
    export class AppModule {
    }

  1. Path:"app.component.ts"。

    import { Component, Injector } from '@angular/core';
    import { createCustomElement } from '@angular/elements';
    import { PopupService } from './popup.service';
    import { PopupComponent } from './popup.component';


    @Component({
      selector: 'app-root',
      template: `
        <input #input value="Message">
        <button (click)="popup.showAsComponent(input.value)">Show as component</button>
        <button (click)="popup.showAsElement(input.value)">Show as element</button>
      `,
    })
    export class AppComponent {
      constructor(injector: Injector, public popup: PopupService) {
        // Convert `PopupComponent` to a custom element.
        const PopupElement = createCustomElement(PopupComponent, {injector});
        // Register the custom element with the browser.
        customElements.define('popup-element', PopupElement);
      }
    }

為自定義元素添加類型支持

一般的 DOM API,比如 document.createElement()document.querySelector(),會(huì)返回一個(gè)與指定的參數(shù)相匹配的元素類型。比如,調(diào)用 document.createElement('a') 會(huì)返回 HTMLAnchorElement,這樣 TypeScript 就會(huì)知道它有一個(gè) href 屬性,而 document.createElement('div') 會(huì)返回 HTMLDivElement,這樣 TypeScript 就會(huì)知道它沒有 href 屬性。

當(dāng)調(diào)用未知元素(比如自定義的元素名 popup-element)時(shí),該方法會(huì)返回泛化類型,比如 HTMLELement,這時(shí)候 TypeScript 就無法推斷出所返回元素的正確類型。

用 Angular 創(chuàng)建的自定義元素會(huì)擴(kuò)展 NgElement 類型(而它擴(kuò)展了 HTMLElement)。除此之外,這些自定義元素還擁有相應(yīng)組件的每個(gè)輸入屬性。比如,popup-element 元素具有一個(gè) string 型的 message 屬性。

如果你要讓你的自定義元素獲得正確的類型,還可使用一些選項(xiàng)。假設(shè)你要?jiǎng)?chuàng)建一個(gè)基于下列組件的自定義元素 my-dialog

@Component(...)
class MyDialog {
  @Input() content: string;
}

獲得精確類型的最簡單方式是把相關(guān) DOM 方法的返回值轉(zhuǎn)換成正確的類型。要做到這一點(diǎn),你可以使用 NgElementWithProperties 類型(都導(dǎo)出自 @angular/elements):

const aDialog = document.createElement('my-dialog') as NgElement & WithProperties<{content: string}>;
aDialog.content = 'Hello, world!';
aDialog.content = 123;  // <-- ERROR: TypeScript knows this should be a string.
aDialog.body = 'News';  // <-- ERROR: TypeScript knows there is no `body` property on `aDialog`.

這是一種讓你的自定義元素快速獲得 TypeScript 特性(比如類型檢查和自動(dòng)完成支持)的好辦法,不過如果你要在多個(gè)地方使用它,可能會(huì)有點(diǎn)啰嗦,因?yàn)椴坏貌辉诿總€(gè)地方對(duì)返回類型做轉(zhuǎn)換。

另一種方式可以對(duì)每個(gè)自定義元素的類型只聲明一次。你可以擴(kuò)展 HTMLElementTagNameMap,TypeScript 會(huì)在 DOM 方法(如 document.createElement()document.querySelector() 等)中用它來根據(jù)標(biāo)簽名推斷返回元素的類型。

declare global {
  interface HTMLElementTagNameMap {
    'my-dialog': NgElement & WithProperties<{content: string}>;
    'my-other-element': NgElement & WithProperties<{foo: 'bar'}>;
    ...
  }
}

現(xiàn)在,TypeScript 就可以像內(nèi)置元素一樣推斷出它的正確類型了:

document.createElement('div')               //--> HTMLDivElement (built-in element)
document.querySelector('foo')               //--> Element        (unknown element)
document.createElement('my-dialog')         //--> NgElement & WithProperties<{content: string}> (custom element)
document.querySelector('my-other-element')  //--> NgElement & WithProperties<{foo: 'bar'}>      (custom element)
以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)