Angular 元素

2022-06-29 11:57 更新

Angular 元素(Elements)概覽

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

這節(jié)的范例應(yīng)用,請參閱現(xiàn)場演練 / 下載范例。

自定義元素這項特性目前受到了 Chrome、Edge(基于 Chromium 的版本)、Opera 和 Safari 的支持,在其它瀏覽器中也能通過膩子腳本加以支持。 自定義元素擴展了 HTML,它允許你定義一個由 JavaScript 代碼創(chuàng)建和控制的標簽。 瀏覽器會維護一個自定義元素的注冊表 ?CustomElementRegistry?,它把一個可實例化的 JavaScript 類映射到 HTML 標簽上。

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

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

我們正在持續(xù)開發(fā)自定義元素功能,讓它們可以用在由其它框架所構(gòu)建的 Web 應(yīng)用中。 Angular 框架的一個小型的、自包含的版本將會作為服務(wù)注入進去,以提供組件的變更檢測和數(shù)據(jù)綁定功能。 要了解這個開發(fā)方向的更多內(nèi)容,參閱這個視頻演講。

使用自定義元素

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

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

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

工作原理

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

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

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


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

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

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

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


不要將 ?@Component? 的選擇器用作自定義元素的標記名稱。由于 Angular 會為單個 DOM 元素創(chuàng)建兩個組件實例,所以這可能導(dǎo)致意外行為:一個是常規(guī)的 Angular 組件,而另一個是自定義元素。

映射

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

  • 用于創(chuàng)建的 API 會解析該組件,以查找輸入屬性(Property),并在這個自定義元素上定義相應(yīng)的屬性(Attribute)。 它把屬性名轉(zhuǎn)換成與自定義元素兼容的形式(自定義元素不區(qū)分大小寫),生成的屬性名會使用中線分隔的小寫形式。 比如,對于帶有 ?@Input('myInputProp') inputProp? 的組件,其對應(yīng)的自定義元素會帶有一個 ?my-input-prop? 屬性。
  • 組件的輸出屬性會用 HTML 自定義事件的形式進行分發(fā),自定義事件的名字就是這個輸出屬性的名字。 比如,對于帶有 ?@Output() valueChanged = new EventEmitter()? 屬性的組件,其相應(yīng)的自定義元素將會分發(fā)名叫 "valueChanged" 的事件,事件中所攜帶的數(shù)據(jù)存儲在該事件對象的 ?detail ?屬性中。 如果你提供了別名,就改用這個別名。比如,?@Output('myClick') clicks = new EventEmitter<string>();? 會導(dǎo)致分發(fā)名為 "myClick" 事件。

要了解更多,請參閱 Web Components 的文檔:Creating custom events

自定義元素的瀏覽器支持

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

瀏覽器

自定義元素支持

Chrome

原生支持。

Edge (基于 Chromium 的版本)

原生支持。

Firefox

原生支持。

Opera

原生支持。

Safari

原生支持。

要往工作空間中添加 ?@angular/elements? 包,請運行如下命令:

npm install @angular/elements --save

范例:彈窗服務(wù)

以前,如果你要在運行期間把一個組件添加到應(yīng)用中,就得定義成動態(tài)組件,然后還要加載它、把它附加到 DOM 中的元素上,并且裝配所有的依賴、變更檢測和事件處理,詳見動態(tài)組件加載器。

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

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

  • ?popup.component.ts? 定義了一個簡單的彈窗元素,用于顯示一條輸入消息,附帶一些動畫和樣式。
  • ?popup.service.ts? 創(chuàng)建了一個可注入的服務(wù),它提供了兩種方式來執(zhí)行 ?PopupComponent?:作為動態(tài)組件或作為自定義元素。注意動態(tài)組件的方式需要更多的代碼來做搭建工作。
  • ?app.module.ts? 把 ?PopupComponent ?添加到模塊的 ?entryComponents ?列表中,而從編譯過程中排除它,以消除啟動時的警告和錯誤。
  • ?app.component.ts? 定義了該應(yīng)用的根組件,它借助 ?PopupService ?在運行時把這個彈窗添加到 DOM 中。在應(yīng)用運行期間,根組件的構(gòu)造函數(shù)會把 ?PopupComponent ?轉(zhuǎn)換成自定義元素。

為了對比,這個范例中同時演示了這兩種方式。一個按鈕使用動態(tài)加載的方式添加彈窗,另一個按鈕使用自定義元素的方式??梢钥吹剑瑑烧叩慕Y(jié)果是一樣的,其差別只是準備過程不同。

  • popup.component.ts
  • import { Component, EventEmitter, HostBinding, 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()">&#x2716;</button>
      `,
      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 {
      @HostBinding('@state')
      state: 'opened' | 'closed' = 'closed';
    
      @Input()
      get message(): string { return this._message; }
      set message(message: string) {
        this._message = message;
        this.state = 'opened';
      }
      private _message = '';
    
      @Output()
      closed = new EventEmitter<void>();
    }
  • 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);
      }
    }
  • 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';
    
    @NgModule({
      imports: [BrowserModule, BrowserAnimationsModule],
      providers: [PopupService],
      declarations: [AppComponent, PopupComponent],
      bootstrap: [AppComponent],
    })
    export class AppModule {
    }
  • 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()?,會返回一個與指定的參數(shù)相匹配的元素類型。比如,調(diào)用 ?document.createElement('a')? 會返回 ?HTMLAnchorElement?,這樣 TypeScript 就會知道它有一個 ?href ?屬性,而 ?document.createElement('div')? 會返回 ?HTMLDivElement?,這樣 TypeScript 就會知道它沒有 ?href ?屬性。

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

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

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

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

要獲得精確類型,最直白的方式是把相關(guān) DOM 方法的返回值轉(zhuǎn)換成正確的類型。要做到這一點,可以使用 ?NgElement ?和 ?WithProperties ?類型(都導(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 特性(比如類型檢查和自動完成支持)的好辦法,不過如果你要在多個地方使用它,可能會有點啰嗦,因為不得不在每個地方對返回類型做轉(zhuǎn)換。

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

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)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號