自訂元素 v1 - 可重複使用的網頁元件

自訂元素可讓網頁程式開發人員定義新的 HTML 標記、擴充現有標記,以及建立可重複使用的網頁元件。

網頁程式開發人員可利用自訂元素建立新的 HTML 標記。 強化現有的 HTML 標記,或擴充其他開發人員的元件 。這個 API 是網路 元件。這提供了 建立可重複使用的元件 基本 JS/HTML/CSS。如此一來,您就能減少程式碼、模組化程式碼等 我們的應用程式。

簡介

這個瀏覽器為建構網頁應用程式的絕佳工具。是 。你可能已經聽說了!不僅宣告式、可攜式 且易於使用好比 HTML 包括詞彙和字彙 擴充能力有限HTML 現存的 標準向來無法用來 自動將 JS 行為與標記建立關聯...直到現在為止。

自訂元素是翻新 HTML 的做法,可填補缺少的 以及結合結構與行為如果 HTML 未提供 我們可以建立自訂元素來解決這個問題自訂 元素可教導瀏覽器新的技巧,同時維持 HTML 的優點。

定義新元素

如要定義新的 HTML 元素,我們需要強大的 JavaScript!

customElements 全域用於定義自訂元素和教學內容 要載入瀏覽器的新代碼使用標記名稱呼叫 customElements.define() 您想建立的內容,以及擴充基本 HTMLElement 的 JavaScript class

範例 - 定義行動導覽匣面板 <app-drawer>

class AppDrawer extends HTMLElement {...}
window.customElements.define('app-drawer', AppDrawer);

// Or use an anonymous class if you don't want a named constructor in current scope.
window.customElements.define('app-drawer', class extends HTMLElement {...});

使用範例:

<app-drawer></app-drawer>

請注意,使用自訂元素 您可以使用 <div> 或任何其他元素您可以在頁面上宣告執行個體 也可以附加在 JavaScript 中動態建立、事件接聽程式等。 。

定義元素的 JavaScript API

自訂元素的功能是使用 ES2015 定義的 class敬上 擴充 HTMLElement擴充 HTMLElement 可確保自訂元素 繼承整個 DOM API,並且代表您加入 類別會成為元素 DOM 介面的一部分。基本上,請使用 類別 為代碼建立公開 JavaScript API

範例 - 定義 <app-drawer> 的 DOM 介面:

class AppDrawer extends HTMLElement {

  // A getter/setter for an open property.
  get open() {
    return this.hasAttribute('open');
  }

  set open(val) {
    // Reflect the value of the open property as an HTML attribute.
    if (val) {
      this.setAttribute('open', '');
    } else {
      this.removeAttribute('open');
    }
    this.toggleDrawer();
  }

  // A getter/setter for a disabled property.
  get disabled() {
    return this.hasAttribute('disabled');
  }

  set disabled(val) {
    // Reflect the value of the disabled property as an HTML attribute.
    if (val) {
      this.setAttribute('disabled', '');
    } else {
      this.removeAttribute('disabled');
    }
  }

  // Can define constructor arguments if you wish.
  constructor() {
    // If you define a constructor, always call super() first!
    // This is specific to CE and required by the spec.
    super();

    // Setup a click listener on <app-drawer> itself.
    this.addEventListener('click', e => {
      // Don't toggle the drawer if it's disabled.
      if (this.disabled) {
        return;
      }
      this.toggleDrawer();
    });
  }

  toggleDrawer() {
    // ...
  }
}

customElements.define('app-drawer', AppDrawer);

在這個範例中,我們要建立含有 open 屬性 disabled 的導覽匣 以及 toggleDrawer() 方法也會將屬性反映為 HTML 屬性

自訂元素的其中一項便利功能,在於類別定義中的 this 是指 DOM 元素本身 (即類別的執行個體)。在我們 例如,this 參照 <app-drawer>。這樣 (😉?) 即元素 將 click 事件監聽器附加到本身!而且不限於事件監聽器。 整個 DOM API 就位於元素程式碼內。使用 this存取 元素的屬性、檢查子元素 (this.children)、查詢節點 (this.querySelectorAll('.items')) 等

建立自訂元素的規則

  1. 自訂元素的名稱必須包含破折號 (-)<x-tags> <my-element><my-awesome-app> 都是有效名稱,而 <tabs><foo_bar> 則不是。如此一來,HTML 剖析器就能 以便區分自訂元素和一般元素這也能確保 相容性。
  2. 相同的代碼只能註冊一次。嘗試這麼做 擲回 DOMException。當您向瀏覽器提供新代碼的訊息後 基礎架構無須回收,
  3. 自訂元素不能自動關閉,因為 HTML 僅允許少數 元素 自行關閉一律撰寫結尾標記 (<app-drawer></app-drawer>).

自訂元素回應

自訂元素可定義用於執行程式碼的特殊生命週期掛鉤 有趣的時期這些就是所謂的自訂元素 回應

名稱 呼叫時機
constructor 元素例項是 建立或升級版。有助於初始化 設定事件接聽程式 形成陰影範圍 詳情請參閱 規格 限制您在 constructor 中可執行的操作。
connectedCallback 每次 元素插入 DOM。適合用來執行設定程式碼,例如 擷取資源或轉譯。一般而言,您應設法延遲工作 直到今天。
disconnectedCallback 每次元素從 DOM 移除時呼叫。適用於 執行清理程式碼
attributeChangedCallback(attrName, oldVal, newVal) 在已經觀察到的屬性 已新增、移除、更新或取代同樣適用於初始值 當剖析器建立元素時,或 注意:僅限 observedAttributes 屬性中列出的屬性 即可接收此回呼。
adoptedCallback 自訂元素已移至新的 document (例如 一位名為 document.adoptNode(el) 的人員)。

回應回呼為同步性質。如果有人撥打 el.setAttribute() 瀏覽器會立即呼叫 attributeChangedCallback()。 同樣地,元素 disconnectedCallback() 之後 已從 DOM 中移除 (例如使用者呼叫 el.remove())。

範例:<app-drawer> 中新增自訂元素回應:

class AppDrawer extends HTMLElement {
  constructor() {
    super(); // always call super() first in the constructor.
    // ...
  }

  connectedCallback() {
    // ...
  }

  disconnectedCallback() {
    // ...
  }

  attributeChangedCallback(attrName, oldVal, newVal) {
    // ...
  }
}

視需要定義回應。如果您的元素夠複雜 並開啟 connectedCallback() 中 IndexedDB 的連線,請完成必要的操作 「disconnectedCallback()」中的清理工作但要小心!不能仰賴您的 元素。例如: 如果使用者關閉分頁,系統一律不會呼叫 disconnectedCallback()

屬性與屬性

將屬性反映到屬性

HTML 屬性常會將其值反射回 DOM,作為 HTML 屬性。例如,當 hiddenid 的值變更在 JS:

div.id = 'my-id';
div.hidden = true;

這些值會以屬性的形式套用至即時 DOM:

<div id="my-id" hidden>

這稱為「反映屬性 屬性」。 HTML 中幾乎所有屬性都會執行這項作業。這是因為屬性也能用於 以宣告方式設定元素,以及無障礙功能和 CSS 等特定 API 選取器必須依賴屬性才能運作

在您想要保留元素的 DOM 位置時,反映屬性的做法相當實用 代表與 JavaScript 狀態同步。您可以選擇 反映屬性,因此當 JS 狀態變更時,將會套用使用者定義的樣式。

喚回我們的 <app-drawer>。此元件的使用者可能想要淡出此元件 和/或在停用狀態下防止使用者互動:

app-drawer[disabled] {
  opacity: 0.5;
  pointer-events: none;
}

在 JS 中變更 disabled 屬性時,我們希望這個屬性 新增至 DOM,以使使用者的選取器程式碼相符。這個元素可提供 將值反映到相同名稱的屬性上的行為:

get disabled() {
  return this.hasAttribute('disabled');
}

set disabled(val) {
  // Reflect the value of `disabled` as an attribute.
  if (val) {
    this.setAttribute('disabled', '');
  } else {
    this.removeAttribute('disabled');
  }
  this.toggleDrawer();
}

觀察屬性的變化

HTML 屬性可讓使用者輕鬆宣告初始狀態:

<app-drawer open disabled></app-drawer>

元素只要定義 attributeChangedCallback。每次變更時,瀏覽器都會呼叫這個方法 對應至 observedAttributes 陣列中列出的屬性。

class AppDrawer extends HTMLElement {
  // ...

  static get observedAttributes() {
    return ['disabled', 'open'];
  }

  get disabled() {
    return this.hasAttribute('disabled');
  }

  set disabled(val) {
    if (val) {
      this.setAttribute('disabled', '');
    } else {
      this.removeAttribute('disabled');
    }
  }

  // Only called for the disabled and open attributes due to observedAttributes
  attributeChangedCallback(name, oldValue, newValue) {
    // When the drawer is disabled, update keyboard/screen reader behavior.
    if (this.disabled) {
      this.setAttribute('tabindex', '-1');
      this.setAttribute('aria-disabled', 'true');
    } else {
      this.setAttribute('tabindex', '0');
      this.setAttribute('aria-disabled', 'false');
    }
    // TODO: also react to the open attribute changing.
  }
}

在此範例中,我們會在 <app-drawer> 上設定其他屬性時 disabled 屬性已變更。雖然我們不會在這裡這麼做 同時也使用 attributeChangedCallback,讓 JS 屬性與 屬性

元素升級

漸進式增強 HTML

我們已瞭解自訂元素是由呼叫 customElements.define()。但這不表示您需要定義 + 註冊 自訂元素

在登錄定義「之前」可以使用自訂元素

漸進式強化是自訂元素的功能。也就是說 宣告網頁上大量 <app-drawer> 元素,且從未叫用 customElements.define('app-drawer', ...)後還有更多機會。這是因為 多虧有 unknown,瀏覽器才會以不同的方式處理潛在的自訂元素 標記。呼叫 define() 並中止現有 ,則稱為「元素升級」。

查看代碼名稱定義的時機 window.customElements.whenDefined()。會傳回一個承諾,在 元素。

customElements.whenDefined('app-drawer').then(() => {
  console.log('app-drawer defined');
});

範例 - 等到一組子項元素升級後再執行

<share-buttons>
  <social-button type="twitter"><a href="...">Twitter</a></social-button>
  <social-button type="fb"><a href="...">Facebook</a></social-button>
  <social-button type="plus"><a href="...">G+</a></social-button>
</share-buttons>
// Fetch all the children of <share-buttons> that are not defined yet.
let undefinedButtons = buttons.querySelectorAll(':not(:defined)');

let promises = [...undefinedButtons].map((socialButton) => {
  return customElements.whenDefined(socialButton.localName);
});

// Wait for all the social-buttons to be upgraded.
Promise.all(promises).then(() => {
  // All social-button children are ready.
});

元素定義的內容

自訂元素可使用內部的 DOM API 管理其內容 元素程式碼。回應功能非常實用。

範例 - 建立含有部分預設 HTML 的元素:

customElements.define('x-foo-with-markup', class extends HTMLElement {
  connectedCallback() {
    this.innerHTML = "<b>I'm an x-foo-with-markup!</b>";
  }
  // ...
});

宣告此代碼會產生:

<x-foo-with-markup>
  <b>I'm an x-foo-with-markup!</b>
</x-foo-with-markup>

// TODO:DevSite - 使用內嵌事件處理常式,因此已移除程式碼範例

建立使用 Shadow DOM 的元素

Shadow DOM 讓元素擁有、算繪及設定特定區塊的樣式 與網頁其他部分不同的 DOM。你甚至可以隱藏 使用單一代碼加入整個應用程式:

<!-- chat-app's implementation details are hidden away in Shadow DOM. -->
<chat-app></chat-app>

如要在自訂元素中使用 Shadow DOM,請呼叫 this.attachShadow constructor

let tmpl = document.createElement('template');
tmpl.innerHTML = `
  <style>:host { ... }</style> <!-- look ma, scoped styles -->
  <b>I'm in shadow dom!</b>
  <slot></slot>
`;

customElements.define('x-foo-shadowdom', class extends HTMLElement {
  constructor() {
    super(); // always call super() first in the constructor.

    // Attach a shadow root to the element.
    let shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.appendChild(tmpl.content.cloneNode(true));
  }
  // ...
});

使用範例:

<x-foo-shadowdom>
  <p><b>User's</b> custom text</p>
</x-foo-shadowdom>

<!-- renders as -->
<x-foo-shadowdom>
  #shadow-root
  <b>I'm in shadow dom!</b>
  <slot></slot> <!-- slotted content appears here -->
</x-foo-shadowdom>

使用者的自訂文字

// TODO:DevSite - 使用內嵌事件處理常式,因此已移除程式碼範例

<template> 建立元素

對於不熟悉的人,<template> 元素 可讓您宣告在載入網頁時剖析及傳入的 DOM 片段。 因此無法在稍後執行階段啟用這是另一個網路上的 API 基本功能 元件系列範本很適合用來宣告 自訂元素的結構

範例:註冊元素,並將透過 <template>

<template id="x-foo-from-template">
  <style>
    p { color: green; }
  </style>
  <p>I'm in Shadow DOM. My markup was stamped from a &lt;template&gt;.</p>
</template>

<script>
  let tmpl = document.querySelector('#x-foo-from-template');
  // If your code is inside of an HTML Import you'll need to change the above line to:
  // let tmpl = document.currentScript.ownerDocument.querySelector('#x-foo-from-template');

  customElements.define('x-foo-from-template', class extends HTMLElement {
    constructor() {
      super(); // always call super() first in the constructor.
      let shadowRoot = this.attachShadow({mode: 'open'});
      shadowRoot.appendChild(tmpl.content.cloneNode(true));
    }
    // ...
  });
</script>

這幾行程式碼就是一大堆。現在我們來瞭解 已開啟:

  1. 我們要在 HTML 中定義新元素:<x-foo-from-template>
  2. 元素的 Shadow DOM 是從 <template> 建立
  3. 因為 Shadow DOM 的關係,元素的 DOM 與元素區域相近
  4. 因為 Shadow DOM 的關係,元素的內部 CSS 範圍限定在 元素

我在 Shadow DOM 中我的標記引用自 <template>。

// TODO:DevSite - 使用內嵌事件處理常式,因此已移除程式碼範例

設定自訂元素的樣式

即使元素使用 Shadow DOM 定義自己的樣式,使用者還是可以設定樣式 將自訂元素從頁面中定義出來這些就是「使用者定義的樣式」。

<!-- user-defined styling -->
<style>
  app-drawer {
    display: flex;
  }
  panel-item {
    transition: opacity 400ms ease-in-out;
    opacity: 0.3;
    flex: 1;
    text-align: center;
    border-radius: 50%;
  }
  panel-item:hover {
    opacity: 1.0;
    background: rgb(255, 0, 255);
    color: white;
  }
  app-panel > panel-item {
    padding: 5px;
    list-style: none;
    margin: 0 7px;
  }
</style>

<app-drawer>
  <panel-item>Do</panel-item>
  <panel-item>Re</panel-item>
  <panel-item>Mi</panel-item>
</app-drawer>

您可能會想:如果元素設有樣式,CSS 特異性會如何運作 在 Shadow DOM 中定義的明確來說,使用者樣式勝出。他們會 一律覆寫元素定義的樣式。請參閱建立元素

針對未註冊的元素預先設定樣式

元素升級之前,您可以使用 :defined 虛擬類別。這有助於預先設定元件樣式。適用對象 舉例來說,建議您隱藏未定義的 並在定義時從畫面上淡入

範例 - 在定義前隱藏 <app-drawer>

app-drawer:not(:defined) {
  /* Pre-style, give layout, replicate app-drawer's eventual styles, etc. */
  display: inline-block;
  height: 100vh;
  opacity: 0;
  transition: opacity 0.3s ease-in-out;
}

定義 <app-drawer> 後,選取器 (app-drawer:not(:defined)) 不符合條件。

擴充元素

Custom Elements API 可以用來建立新的 HTML 元素,但也 ,以協助您擴充其他自訂元素或甚至瀏覽器內建的 HTML。

擴充自訂元素

擴充另一個自訂元素的方法是擴充其類別定義。

範例:建立擴充 <app-drawer><fancy-app-drawer>

class FancyDrawer extends AppDrawer {
  constructor() {
    super(); // always call super() first in the constructor. This also calls the extended class' constructor.
    // ...
  }

  toggleDrawer() {
    // Possibly different toggle implementation?
    // Use ES2015 if you need to call the parent method.
    // super.toggleDrawer()
  }

  anotherMethod() {
    // ...
  }
}

customElements.define('fancy-app-drawer', FancyDrawer);

擴充原生 HTML 元素

假設您想要建立 <button> 專用揚聲器,您不必複製 <button> 的行為和功能。 使用自訂元素強化現有元素。

自訂內建元素是可擴充 瀏覽器內建的 HTML 標記擴充現有的 目的在於取得其所有功能 (DOM 屬性、方法、無障礙功能)。 想要寫出漸進式網路,有沒有更好的方法? 應用程式,再逐步強化現有 HTML 的功能 元素

如要擴充元素,您必須建立沿用 正確的 DOM 介面例如,自訂元素是 <button> 需要從 HTMLButtonElement 繼承 (而非 HTMLElement)。 同樣地,擴充 <img> 的元素需要擴充 HTMLImageElement

範例 - 擴充 <button>

// See https://html.spec.whatwg.org/multipage/indices.html#element-interfaces
// for the list of other DOM interfaces.
class FancyButton extends HTMLButtonElement {
  constructor() {
    super(); // always call super() first in the constructor.
    this.addEventListener('click', e => this.drawRipple(e.offsetX, e.offsetY));
  }

  // Material design ripple animation.
  drawRipple(x, y) {
    let div = document.createElement('div');
    div.classList.add('ripple');
    this.appendChild(div);
    div.style.top = `${y - div.clientHeight/2}px`;
    div.style.left = `${x - div.clientWidth/2}px`;
    div.style.backgroundColor = 'currentColor';
    div.classList.add('run');
    div.addEventListener('transitionend', (e) => div.remove());
  }
}

customElements.define('fancy-button', FancyButton, {extends: 'button'});

請注意,擴充原生格式時,對 define() 的呼叫會稍微變更 元素。必要的第三個參數會告訴瀏覽器 這是因為許多 HTML 標記共用相同的 DOM 存取 API<section><address><em> (和其他人) 都分享 HTMLElement;<q><blockquote> 會共用 HTMLQuoteElement;等等... 指定 {extends: 'blockquote'} 可讓瀏覽器知道您正在建立 卻使用 <blockquote> 取代 <q>。詳情請參閱 HTML 規格 以取得 HTML 的 DOM 介面完整清單。

自訂內建元素的消費者可以透過多種方式使用。他們可以 請在原生標記中新增 is="" 屬性來宣告該屬性:

<!-- This <button> is a fancy button. -->
<button is="fancy-button" disabled>Fancy button!</button>

在 JavaScript 中建立執行個體:

// Custom elements overload createElement() to support the is="" attribute.
let button = document.createElement('button', {is: 'fancy-button'});
button.textContent = 'Fancy button!';
button.disabled = true;
document.body.appendChild(button);

或使用 new 運算子:

let button = new FancyButton();
button.textContent = 'Fancy button!';
button.disabled = true;

以下是另一個擴充 <img> 的範例。

範例 - 擴充 <img>

customElements.define('bigger-img', class extends Image {
  // Give img default size if users don't specify.
  constructor(width=50, height=50) {
    super(width * 10, height * 10);
  }
}, {extends: 'img'});

使用者宣告此元件為:

<!-- This <img> is a bigger img. -->
<img is="bigger-img" width="15" height="20">

或在 JavaScript 中建立執行個體:

const BiggerImage = customElements.get('bigger-img');
const image = new BiggerImage(15, 20); // pass constructor values like so.
console.assert(image.width === 150);
console.assert(image.height === 200);

其他詳細資料

不明元素與未定義的自訂元素

HTML 篇幅很寬鬆,適合用來作業。舉例來說,宣告 網頁上的 <randomtagthatdoesntexist> 和瀏覽器效能極佳 接受。為何非標準代碼如何運作?答案是 HTML 規格 並允許系統會將未由規格定義的元素剖析為 HTMLUnknownElement

自訂元素也是如此。剖析可能的自訂元素 如果建立時使用的是有效名稱 (包含「-」),則設為 HTMLElement。個人中心 可以在支援自訂元素的瀏覽器中進行檢查。啟動主控台: 按下 Ctrl + Shift + J 鍵 (在 Mac 上則是 Cmd + Opt + J 鍵),然後貼上 導入以下程式碼:

// "tabs" is not a valid custom element name
document.createElement('tabs') instanceof HTMLUnknownElement === true

// "x-tabs" is a valid custom element name
document.createElement('x-tabs') instanceof HTMLElement === true

API 參考資料

customElements 全域定義了自訂工作的實用方法 元素。

define(tagName, constructor, options)

定義瀏覽器的新自訂元素。

範例

customElements.define('my-app', class extends HTMLElement { ... });
customElements.define(
    'fancy-button', class extends HTMLButtonElement { ... }, {extends: 'button'});

get(tagName)

請將有效的自訂元素標記名稱傳回,然後傳回該元素的建構函式。 如果尚未註冊任何元素定義,則傳回 undefined

範例

let Drawer = customElements.get('app-drawer');
let drawer = new Drawer();

whenDefined(tagName)

傳回在定義自訂元素時解析的 Promise。如果 元素已定義,並立即解析。在非標記名稱的情況下拒絕 有效的自訂元素名稱

範例

customElements.whenDefined('app-drawer').then(() => {
  console.log('ready!');
});

記錄和瀏覽器支援

如果您近幾年來持續追蹤網頁元件 瞭解 Chrome 36 以上版本實作了 document.registerElement() 取代 customElements.define()。目前已經解決 視為已淘汰的標準版 v0 customElements.define() 是最新的熱門產品,以及瀏覽器供應商 。名為「自訂元素 v1」。

如果您想瞭解舊版 v0 規格,請參閱 html5rocks 一文。

瀏覽器支援

Chrome 54 (狀態)、 Safari 10.1 (狀態) 和 Firefox 63 (狀態) 已 自訂元素 v1。邊緣已開始

如要讓功能偵測自訂元素,請檢查該項目是否存在 window.customElements

const supportsCustomElementsV1 = 'customElements' in window;

聚合物

在此之前 獨立的 polyfill 適用於自訂元素 v1不過,我們建議您使用 webcomponents.js 載入器 以最佳方式載入網頁元件 polyfill。載入器 使用功能偵測功能,僅以非同步方式載入必要的輪詢填入作業 瀏覽器要求

安裝:

npm install --save @webcomponents/webcomponentsjs

使用方式:

<!-- Use the custom element on the page. -->
<my-element></my-element>

<!-- Load polyfills; note that "loader" will load these async -->
<script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js" defer></script>

<!-- Load a custom element definitions in `waitFor` and return a promise -->
<script type="module">
  function loadScript(src) {
    return new Promise(function(resolve, reject) {
      const script = document.createElement('script');
      script.src = src;
      script.onload = resolve;
      script.onerror = reject;
      document.head.appendChild(script);
    });
  }

  WebComponents.waitFor(() => {
    // At this point we are guaranteed that all required polyfills have
    // loaded, and can use web components APIs.
    // Next, load element definitions that call `customElements.define`.
    // Note: returning a promise causes the custom elements
    // polyfill to wait until all definitions are loaded and then upgrade
    // the document in one batch, for better performance.
    return loadScript('my-element.js');
  });
</script>

結論

自訂元素提供我們用來定義瀏覽器新 HTML 標記的新工具,以及 建立可重複使用的元件並搭配其他新平台 和 Shadow DOM 和 <template> 等基本功能,我們開始實現 網頁元件圖片:

  • 跨瀏覽器 (網路標準),可建立及擴充可重複使用的元件。
  • 不需要程式庫或架構,就能開始使用。Vanilla JS/HTML FTW!
  • 提供熟悉的程式設計模型。其實就是 DOM/CSS/HTML
  • 適合其他新的網頁平台功能 (Shadow DOM、<template>、CSS 自訂屬性等)
  • 與瀏覽器的開發人員工具緊密整合。
  • 運用現有的無障礙功能,