使用自訂元素

Boris Smus
Boris Smus

簡介

網路嚴重缺乏言論。有興趣的話,不妨試試 Gmail 等「新型」網頁應用程式:

Gmail

沒有關於<div>湯品的現代料理。然而,這就是我們建構網頁應用程式的方式。很傷心。 我們的平台是不是該要求更多因素呢?

Sexy 標記。我們來改變樣子

HTML 提供了我們可用來建構文件的極佳工具,但其詞彙僅可使用 HTML 標準定義的元素。

如果 Gmail 的標記不令人困擾,該怎麼辦?如果圖像精美,該怎麼做:

<hangout-module>
    <hangout-chat from="Paul, Addy">
    <hangout-discussion>
        <hangout-message from="Paul" profile="profile.png"
            profile="118075919496626375791" datetime="2013-07-17T12:02">
        <p>Feelin' this Web Components thing.
        <p>Heard of it?
        </hangout-message>
    </hangout-discussion>
    </hangout-chat>
    <hangout-chat>...</hangout-chat>
</hangout-module>

真厲害!這款應用程式也很合理。答案是「非常實用」、「容易理解」,而且最重要的是「可維護」。我/您之後只要檢視宣告式中骨,就能確切瞭解相關功能。

開始使用

自訂元素 可讓網頁開發人員定義新型 HTML 元素。規格是放置在「網頁元件」傘下的幾個新 API 基元之一,但這應該是最重要的。假如沒有自訂元素解鎖的功能,網頁元件就不存在:

  1. 定義新的 HTML/DOM 元素
  2. 建立延伸自其他元素的元素
  3. 以邏輯方式將自訂功能組合成單一標記
  4. 擴充現有 DOM 元素的 API

註冊新元素

自訂元素是使用 document.registerElement() 建立:

var XFoo = document.registerElement('x-foo');
document.body.appendChild(new XFoo());

document.registerElement() 的第一個引數是元素的標記名稱。名稱必須包含破折號 (-)。舉例來說,<x-tags><my-element><my-awesome-app> 都是有效名稱,<tabs><foo_bar> 則不在此限。這項限制可讓剖析器區分自訂元素與一般元素,但也能確保在 HTML 中加入新標記時能前瞻相容性。

第二個引數是 (選用) 物件,用於說明元素的 prototype。您可以在這裡為元素新增自訂功能 (例如公開屬性和方法)。稍後會瞭解詳情

根據預設,自訂元素會繼承 HTMLElement 的設定。因此,上一個範例相當於:

var XFoo = document.registerElement('x-foo', {
    prototype: Object.create(HTMLElement.prototype)
});

呼叫 document.registerElement('x-foo') 會讓瀏覽器瞭解新元素,並傳回可讓您用於建立 <x-foo> 例項的建構函式。如果不想使用建構函式,您也可以使用其他將元素執行個體化的技術

擴充元素

自訂元素可讓您擴充現有的 (原生) HTML 元素以及其他自訂元素。如要擴充元素,您必須傳遞繼承來源元素的 registerElement() 名稱和 prototype

擴充原生元素

假設你對於一般 Joe <button> 你不滿意,您還想成為「超級按鈕」的強大功能如要擴充 <button> 元素,請建立新元素並沿用 HTMLButtonElementprototypeextends 名稱。在這種情況下,"button":

var MegaButton = document.registerElement('mega-button', {
    prototype: Object.create(HTMLButtonElement.prototype),
    extends: 'button'
});

繼承自原生元素的自訂元素稱為類型額外資訊自訂元素。這些修飾符會繼承特殊版本的 HTMLElement,以便指出「元素 X 為 Y」。

示例:

<button is="mega-button">

擴充自訂元素

如要建立擴充 <x-foo> 自訂元素的 <x-foo-extended> 元素,只要沿用其原型,並說出要繼承的來源標記:

var XFooProto = Object.create(HTMLElement.prototype);
...

var XFooExtended = document.registerElement('x-foo-extended', {
    prototype: XFooProto,
    extends: 'x-foo'
});

如要進一步瞭解如何建立元素原型,請參閱下方的「新增 JS 屬性和方法」一節。

元素的升級方式

您是否曾好奇,HTML 剖析器不適用於非標準代碼的原因? 舉例來說,如果在網頁上宣告 <randomtag>,那就太棒了。根據 HTML 規格

<randomtag>,很抱歉!你並非標準,而是繼承自 HTMLUnknownElement

自訂元素也是如此。含有有效自訂元素名稱的元素會沿用 HTMLElement如要確認上述資訊是否屬實,您可以啟動主控台:Ctrl + Shift + J (在 Mac 上則為 Cmd + Opt + J),然後貼上以下幾行程式碼,該程式碼會傳回 true

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

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

未解析的元素

由於自訂元素是透過 document.registerElement() 的指令碼註冊,因此在瀏覽器註冊其定義「之前」,就可以宣告或建立自訂元素。舉例來說,您可以在頁面上宣告 <x-tabs>,但稍後會叫用 document.registerElement('x-tabs')

元素升級至定義之前,稱為「未解析的元素」。這些 HTML 元素包含有效的自訂元素名稱,但尚未註冊。

您可以參考下表資訊:

名稱 沿用自 示例
未解析的元素 HTMLElement <x-tabs><my-element>
不明元素 HTMLUnknownElement <tabs><foo_bar>

建立元素例項

建立元素的常見技巧仍適用於自訂元素。與任何標準元素一樣,您可以在 HTML 中宣告這類元素,或是使用 JavaScript 在 DOM 中建立。

建立自訂標記例項

宣告這些元素:

<x-foo></x-foo>

在 JS 中建立 DOM

var xFoo = document.createElement('x-foo');
xFoo.addEventListener('click', function(e) {
    alert('Thanks!');
});

使用 new 運算子

var xFoo = new XFoo();
document.body.appendChild(xFoo);

建立類型擴充功能元素的例項

類型額外資訊樣式自訂元素例項化的方式與自訂代碼非常接近。

宣告這些元素:

<!-- <button> "is a" mega button -->
<button is="mega-button">

在 JS 中建立 DOM

var megaButton = document.createElement('button', 'mega-button');
// megaButton instanceof MegaButton === true

如您所見,現在有一個 document.createElement() 的超載版本,會將 is="" 屬性做為第二個參數。

使用 new 運算子

var megaButton = new MegaButton();
document.body.appendChild(megaButton);

到目前為止,我們已學會如何使用 document.registerElement() 向瀏覽器發出新標記,但做不到的事情。現在來新增屬性和方法。

新增 JS 屬性和方法

自訂元素的重要功能,就是您可以在元素定義上定義屬性和方法,將量身打造的功能與元素組合在一起。您可以將此視為為元素建立公用 API 的方法。

以下是完整範例:

var XFooProto = Object.create(HTMLElement.prototype);

// 1. Give x-foo a foo() method.
XFooProto.foo = function() {
    alert('foo() called');
};

// 2. Define a property read-only "bar".
Object.defineProperty(XFooProto, "bar", {value: 5});

// 3. Register x-foo's definition.
var XFoo = document.registerElement('x-foo', {prototype: XFooProto});

// 4. Instantiate an x-foo.
var xfoo = document.createElement('x-foo');

// 5. Add it to the page.
document.body.appendChild(xfoo);

當然,建構 prototype 的方法有 1,000 種。如果您不喜歡建立這類原型,以下是相同內容更精簡的版本:

var XFoo = document.registerElement('x-foo', {
  prototype: Object.create(HTMLElement.prototype, {
    bar: {
      get: function () {
        return 5;
      }
    },
    foo: {
      value: function () {
        alert('foo() called');
      }
    }
  })
});

第一種格式允許使用 ES5 Object.defineProperty。第二種允許使用 get/set

生命週期回呼方法

元素可以定義特殊方法,利用元素存在的有趣時間。這些方法已適當命名為生命週期回呼。每個選項都有特定的名稱和用途:

回呼名稱 呼叫時機
createdCallback 系統會建立該元素的
attachedCallback 執行個體已插入文件中
detachedCallback 已從文件中移除執行個體
attributeChangedCallback(attrName, oldVal, newVal) 新增了、移除或更新屬性

範例:<x-foo> 上定義 createdCallback()attachedCallback()

var proto = Object.create(HTMLElement.prototype);

proto.createdCallback = function() {...};
proto.attachedCallback = function() {...};

var XFoo = document.registerElement('x-foo', {prototype: proto});

所有生命週期回呼都是選用項目,但可視情況定義。舉例來說,假設您的元素非常複雜,且在 createdCallback() 中開啟了索引資料庫的連線。從 DOM 移除之前,請在 detachedCallback() 中執行必要的清理工作。注意:您不應仰賴這一點,例如使用者關閉分頁,但將其視為可能的最佳化掛鉤。

另一個用途生命週期回呼是在元素上設定預設事件監聽器:

proto.createdCallback = function() {
  this.addEventListener('click', function(e) {
    alert('Thanks!');
  });
};

正在新增標記

我們已為 JavaScript API 建立 <x-foo>,但沒有任何內容!該讓程式碼改成一些 HTML 來轉譯嗎?

生命週期回呼在這裡非常方便。具體來說,我們可以使用 createdCallback() 透過某些預設 HTML 結束元素:

var XFooProto = Object.create(HTMLElement.prototype);

XFooProto.createdCallback = function() {
    this.innerHTML = "**I'm an x-foo-with-markup!**";
};

var XFoo = document.registerElement('x-foo-with-markup', {prototype: XFooProto});

將這個標記例項化,並在開發人員工具中進行檢查 (按一下滑鼠右鍵並選取「檢查元素」) 應會顯示:

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

在 Shadow DOM 封裝內部

Shadow DOM 本身是功能強大的工具,可用於封裝內容。只要將這個工具搭配自訂元素,就能發揮神奇效果!

Shadow DOM 提供自訂元素:

  1. 隱藏內心的方法,防止使用者遭遇複雜的實作細節。
  2. 樣式封裝...完全免費!

透過 Shadow DOM 建立元素就像建立能呈現基本標記的一樣。差異在 createdCallback() 中:

var XFooProto = Object.create(HTMLElement.prototype);

XFooProto.createdCallback = function() {
    // 1. Attach a shadow root on the element.
    var shadow = this.createShadowRoot();

    // 2. Fill it with markup goodness.
    shadow.innerHTML = "**I'm in the element's Shadow DOM!**";
};

var XFoo = document.registerElement('x-foo-shadowdom', {prototype: XFooProto});

我不是設定元素的 .innerHTML,而是為 <x-foo-shadowdom> 建立「Shadow Root」,然後加入標記。在開發人員工具中啟用「顯示陰影 DOM」設定後,您會看到可展開的 #shadow-root

▾<x-foo-shadowdom>
  ▾#shadow-root
    **I'm in the element's Shadow DOM!**
</x-foo-shadowdom>

那是影子根!

使用範本建立元素

HTML 範本是另一種能夠完美融入自訂元素世界的全新 API 基本功能。

範例:註冊從 <template> 和 Shadow DOM 建立的元素:

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

<script>
  var proto = Object.create(HTMLElement.prototype, {
    createdCallback: {
      value: function() {
        var t = document.querySelector('#sdtemplate');
        var clone = document.importNode(t.content, true);
        this.createShadowRoot().appendChild(clone);
      }
    }
  });
  document.registerElement('x-foo-from-template', {prototype: proto});
</script>

<template id="sdtemplate">
  <style>:host p { color: orange; }</style>
  <p>I'm in Shadow DOM. My markup was stamped from a <template&gt;.
</template>

<div class="demoarea">
  <x-foo-from-template></x-foo-from-template>
</div>

這幾行程式碼就夠用了現在讓我們一起瞭解發生了什麼事:

  1. 我們已在 HTML 中註冊新元素:<x-foo-from-template>
  2. 元素的 DOM 是透過 <template> 建立
  3. 使用陰影 DOM 隱藏元素的恐怖細節
  4. 陰影 DOM 提供元素樣式封裝 (例如,p {color: orange;} 不會讓整個頁面變成橘色)

超棒!

設定自訂元素的樣式

就像任何 HTML 標記一樣,自訂標記的使用者也可以使用選取器設定樣式:

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

<app-panel>
    <li is="x-item">Do</li>
    <li is="x-item">Re</li>
    <li is="x-item">Mi</li>
</app-panel>

使用 Shadow DOM 設定元素樣式

當您將 Shadow DOM 加入集合時,兔子洞會更大幅增加。使用 Shadow DOM 的自訂元素繼承優點。

陰影 DOM 將元素封裝為樣式。在陰影根中定義的樣式不會從主機外流,也不會從頁面流出。如果是自訂元素,元素本身就是主機。樣式封裝的屬性也可讓自訂元素自行定義預設樣式。

陰影 DOM 樣式是一大主題!如要進一步瞭解這項工具,我推薦以下幾篇文章:

使用 :unresolved 防範 FOUC

為減少 FOUC,自訂元素會指定新的 CSS 虛擬類別 :unresolved。使用此項目指定未解析的元素,直到瀏覽器叫用 createdCallback() 的點為止 (請參閱「生命週期方法」)。在這種情況下,元素就不會再變成未解析的元素。升級程序已完成,且元素已轉換為其定義。

範例:註冊時,在「x-foo」標記中淡入:

<style>
  x-foo {
    opacity: 1;
    transition: opacity 300ms;
  }
  x-foo:unresolved {
    opacity: 0;
  }
</style>

請注意,:unresolved 僅適用於未解析的元素,不適用於從 HTMLUnknownElement 繼承的元素 (請參閱「元素的升級方式」一節)。

<style>
  /* apply a dashed border to all unresolved elements */
  :unresolved {
    border: 1px dashed red;
    display: inline-block;
  }
  /* x-panel's that are unresolved are red */
  x-panel:unresolved {
    color: red;
  }
  /* once the definition of x-panel is registered, it becomes green */
  x-panel {
    color: green;
    display: block;
    padding: 5px;
    display: block;
  }
</style>

<panel>
    I'm black because :unresolved doesn't apply to "panel".
    It's not a valid custom element name.
</panel>

<x-panel>I'm red because I match x-panel:unresolved.</x-panel>

記錄和瀏覽器支援

功能偵測

功能偵測是用來檢查 document.registerElement() 是否存在:

function supportsCustomElements() {
    return 'registerElement' in document;
}

if (supportsCustomElements()) {
    // Good to go!
} else {
    // Use other libraries to create components.
}

瀏覽器支援

document.registerElement() 首次在 Chrome 27 版和 Firefox 23 版中率先抵達旗標。然而,規格從那時起已有相當長的發展。Chrome 31 是最先支援更新後的規格。

在瀏覽器支援卓越之前,Google 的 Polymer 和 Mozilla X-Tag 都採用 polyfill

HTMLElementElement 怎麼了?

如果已遵循標準化工作,您得知現在發生過 <element>。是蜜蜂的膝蓋。您可以用它來宣告新元素:

<element name="my-element">
    ...
</element>

不幸的是,在升級程序、邊緣案例和類似 Armageddon 的情境中,有太多時間問題無法全部解決。<element>必須是草坪養成。2013 年 8 月,Dmitri Glazkov 將發布於 public-applications,至少要宣布移除作業。

值得注意的是,Polymer 使用 <polymer-element> 實作宣告式元素註冊方式。怎麼做呢?這項工具會使用 document.registerElement('polymer-element') 和我在透過範本建立元素中所述的技術。

結論

有了自訂元素,我們就能利用自訂元素擴充 HTML 的詞彙、傳授新技巧,並快速完成網路平台的各種挑戰。只要將這些程式庫與其他新平台原始功能 (例如 Shadow DOM 和 <template>) 結合,我們就能開始瞭解網頁元件的樣貌。現在可以再也很性感標記了!

如果您有興趣開始使用網頁元件,建議參考 Polymer。我有豐富的成就,已足夠讓你開始前進。