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

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

有了自訂元素,網頁開發人員就能建立新的 HTML 標記、建立新的 HTML 標記,或是擴充其他開發人員編寫的元件。API 是網頁元件的基礎。它可讓您使用以網頁標準為基礎的方法,只使用基本 JS/HTML/CSS 建立可重複使用的元件。因此,我們可以減少程式碼、模組程式碼,並在應用程式中重複使用更多程式碼。

引言

瀏覽器也是我們建構網路應用程式的絕佳工具。它稱為 HTML您可能聽過這款遊戲!此為宣告式、可攜性、支援性且易於使用和 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

自訂元素的功能是以擴充 HTMLElement 的 ES2015 class 定義。擴充 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 剖析器能夠區分自訂元素與一般元素。在 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()。同樣地,在元素從 DOM 移除後 (例如使用者呼叫 el.remove()),您會立即收到 disconnectedCallback()

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

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

  connectedCallback() {
    // ...
  }

  disconnectedCallback() {
    // ...
  }

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

定義回應是否合理。如果元素夠複雜,且在 connectedCallback() 中開啟 IndexedDB 的連線,請在 disconnectedCallback() 中執行必要的清除工作。但要小心!在某些情況下,您無法完全仰賴從 DOM 中移除的元素。舉例來說,如果使用者關閉分頁,系統一律不會呼叫 disconnectedCallback()

屬性與屬性

反映屬性與屬性

HTML 屬性會將值反回 DOM 作為 HTML 屬性,這是很常見的情況。舉例來說,當 JS 中的 hiddenid 值變更時:

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.
  }
}

在這個範例中,我們將在 disabled 屬性發生變更時在 <app-drawer> 上設定其他屬性。雖然我們在這裡不執行此操作,但您也可以使用 attributeChangedCallback 來讓 JS 屬性與屬性保持同步

元素升級

逐步強化 HTML

我們已瞭解自訂元素是透過呼叫 customElements.define() 來定義。不過,這並不代表您必須一次定義 + 自訂元素。

自訂元素在註冊定義「之前」可以使用

漸進式強化是自訂元素的功能,換句話說,您可以在頁面上宣告多個 <app-drawer> 元素,直到稍後才叫用 customElements.define('app-drawer', ...)。這是因為瀏覽器會因為不明標記,以不同方式處理潛在的自訂元素。呼叫 define() 並終止具有類別定義的現有元素的程序,稱為「元素升級」。

若要得知何時會定義標記名稱,可以使用 window.customElements.whenDefined()。這個方法會傳回 Promise,並在定義元素時解析。

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>

// 待辦事項:DevSite - 因使用內嵌事件處理常式而移除的程式碼範例

建立使用 Shadow DOM 的元素

Shadow DOM 可讓元素擁有、轉譯並設定與網頁其他部分分隔的 DOM 區塊,並設定其樣式。您甚至可以在單一標記內隱藏整個應用程式:

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

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

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>

使用者的自訂文字

// 待辦事項:DevSite - 因使用內嵌事件處理常式而移除的程式碼範例

透過 <template> 建立元素

針對這種情況,<template> 元素可讓您宣告 DOM 片段,這些片段將在網頁載入時進行剖析、在網頁載入時進行插入,並稍後在執行階段啟用。這是網路元件系列中的另一個 API 原始版本。範本是宣告自訂元素結構的理想預留位置

範例:使用從 <template> 建立的 Shadow DOM 內容註冊元素:

<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. 由於陰影 DOM 讓元素的 DOM 成為元素本機位置
  4. 元素的內部 CSS 受到 Shadow DOM 的範圍內,有 僅限 元素

我位於 Shadow DOM。我的標記是從 <template> 蓋章。

// 待辦事項: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>

如果元素在 Shadow DOM 中定義樣式,你可能會想知道 CSS 的特異性如何。就具體而言,使用者樣式的勝出。這些元素一律會覆寫元素定義的樣式。請參閱「建立使用 Shadow DOM 的元素」一節。

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

在元素升級之前,您可以使用 :defined 虛擬類別在 CSS 中指定該元素。如要預先設定元件樣式,這項功能就非常實用。舉例來說,您可以隱藏未定義的元件,並在元件被定義時淡入淡出,避免版面配置或其他視覺 FOUC。

範例 - 在定義前隱藏 <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 介面。<section><address><em> (另外還有其他) 會共用 HTMLElement<q><blockquote> 都會共用 HTMLQuoteElement;以此類推... 指定 {extends: 'blockquote'} 可讓瀏覽器知道你建立的是啟用的 <blockquote>,而非 <q>。如需 HTML 的 DOM 介面完整清單,請參閱 HTML 規格

自訂內建元素的使用者可透過多種方式使用。開發人員可以在原生標記中新增 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() 的 Custom Elements API。這現在視為已淘汰的標準版本,稱為 v0。customElements.define() 是新的熱點,以及瀏覽器廠商即將開始實作的內容。這項技術名為「自訂元素 v1」。

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

瀏覽器支援

Chrome 54 (狀態)、Safari 10.1 (狀態) 和 Firefox 63 (狀態) 具有自訂元素 v1。Edge 已開始開發

如要使用功能偵測自訂元素,請檢查 window.customElements 是否存在:

const supportsCustomElementsV1 = 'customElements' in window;

聚酯纖維

在瀏覽器支援廣泛支援之前,自訂元素 v1 皆可使用獨立的 polyfill。不過,建議您使用 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>) 結合,我們就能開始瞭解網頁元件的全貌:

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