自定義元素 – 在 HTML 中定義新元素

本文翻譯自 Custom Elements: defining new elements in HTML,在保證技術要點表達準確的前提下,行文風格有少許改編和瞎搞。css

原譯文地址html

本文目錄html5

注意!這篇文章介紹的 API 還沒有徹底標準化,而且仍在變更中,在項目中使用這些實驗性 API 時請務必謹慎。web

引言

如今的 web 嚴重缺少表達能力。你只要瞄一眼「現代」的 web 應用,好比 GMail,就會明白個人意思。api

GMail

看看這一坨 DIV,這也叫現代?然而可悲的是,這就是咱們構建 web 應用的方式。難道 web 開發就不能追求更粗更硬更長……哦不對,是更高更快更強的奧林匹克精神?瀏覽器

用時髦標記整點兒像樣的

HTML 爲咱們提供了一個完美的文檔組織工具,然而 HTML 規範定義的元素卻頗有限。app

假如 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>
        <p>Heard of it?</p>
      </hangout-message>
    </hangout-discussion>
  </hangout-chat>
  <hangout-chat>...</hangout-chat>
</hangout-module>

亮瞎狗眼顛覆三觀!這清晰的結構,不識字也看得懂啊!最爽的是,它還有很強的可維護性,只要瞧一眼它的聲明結構就能夠清楚地知道它到底要幹嗎。dom

趕忙開始吧

自定義元素容許開發者定義新的 HTML 元素類型。該規範只是 web 組件模塊提供的衆多新 API 中的一個,但它也極可能是最重要的一個。缺乏自定義元素帶來的如下特性,web 組件根本玩不轉:webapp

  1. 定義新的 HTML/DOM 元素
  2. 基於其餘元素建立擴展元素
  3. 給一個標籤綁定一組自定義功能
  4. 擴展已有 DOM 元素的 API

註冊新元素

使用 document.register() 能夠建立一個自定義元素

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

document.register() 的第一個參數是標籤名,這個標籤名必須包括一個連字符(-)。所以,諸如 <x-tags><my-element><my-awesome-app> 都是合法的標籤名,而 <tabs><foo_bar> 則不是。這個限定使解析器能很容易的區分自定義元素和 HTML 規範定義的元素,同時確保了 HTML 增長新標籤時的向前兼容。

第二個參數是一個可選(譯註:經測試,Chrome 29 中不能省略第二個參數)的對象,用於描述該元素的原型。在這裏能夠爲元素添加自定義功能(公開屬性和方法)。這個到 添加 JS 屬性和方法 一節再細說。

自定義元素默認會繼承 HTMLElement 的原型,所以上一個示例等同於:

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

調用 document.register('x-foo') 向瀏覽器註冊了這個新元素,並返回一個能夠用來建立 <x-foo> 元素實例的構造器。若是你不想使用構造器,也可使用其餘實例化元素的技術。

提示:若是你不但願在 window 全局對象中建立元素構造器,還能夠把它放進命名空間:

var myapp = {};
myapp.XFoo = document.register('x-foo');

擴展原生元素

假設原生 <button> 元素不能知足你的需求,你想將其加強爲一個「超級按鈕」,能夠經過建立一個繼承 HTMLButtonElement 原型的新元素,來擴展 <button> 元素:

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

這類自定義元素被稱爲類型擴展自定義元素。它們以繼承一個特定的 HTMLElement 的方式表達了「元素 X 是一個 Y」。

示例:

<button is="mega-button">

元素如何提高

你有沒有想過爲何 HTML 解析器不會對不是規範定義的標籤報錯?好比咱們在頁面中聲明一個 <randomtag>,一切都很和諧。根據 HTML 規範的表述,非規範定義的元素將使用 HTMLUnknownElement 接口。<randomtag> 不是規範定義的,它會繼承自 HTMLUnknownElement

對自定義元素來講,狀況就不同了。擁有合法元素名的自定義元素將繼承 HTMLElement。你能夠打開控制檯(不知道快捷鍵的都滾粗……),運行下面這段代碼,看看結果是否是 true

// 「tabs」不是一個合法的自定義元素名
document.createElement('tabs').__proto__ === HTMLUnknownElement.prototype

// 「x-tabs」是一個合法的自定義元素名
document.createElement('x-tabs').__proto__ == HTMLElement.prototype

注意:在不支持 document.register() 的瀏覽器中,<x-tabs> 仍爲 HTMLUnknownElement

unresolved(未提高)元素

因爲自定義元素是由 JavaScript 代碼 document.register() 註冊的,所以它們可能在元素定義被註冊到瀏覽器以前就已經聲明或建立過了。好比你能夠先在頁面中聲明 <x-tabs>,再調用 document.register('x-tabs')

在被提高到其定義以前,這些元素被稱爲「unresolved 元素」。它們是擁有合法自定義元素名的 HTML 元素,只是尚未註冊成爲自定義元素。

下面這個表格看起來更直觀一些:

類型 繼承自 示例
unresolved 元素 HTMLElement <x-tabs><my-element><my-awesome-app>
未知元素 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> 「是一個」超級按鈕 -->
<button is="mega-button">

在 JS 中建立 DOM:

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

看,這是一個接收第二個參數爲 is 屬性值的 document.createElement() 重載。

使用 new 操做符:

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

如今,咱們已經學習瞭如何使用 document.register() 來向瀏覽器註冊一個新標籤。但這還不夠,接下來咱們要向新標籤添加屬性和方法。

添加 JS 屬性和方法

自定義元素最強大的地方在於,你能夠在元素定義中加入屬性和方法,給元素綁定特定的功能。你能夠把它想象成一種給你的元素建立公開 API 的方法。

下面是一個完整的示例:

var XFooProto = Object.create(HTMLElement.prototype);
// 1. 爲 x-foo 建立 foo() 方法
XFooProto.foo = function() {
  alert('foo() called');
};

// 2. 定義一個只讀屬性 "bar".
Object.defineProperty(XFooProto, "bar", {value: 5});

// 3. 註冊 x-foo
var XFoo = document.register('x-foo', {prototype: XFooProto});

// 4. 建立一個 x-foo 實例.
var xfoo = document.createElement('x-foo');

// 5. 插入頁面
document.body.appendChild(xfoo);

構造原型的方法多種多樣,若是你不喜歡上面這種方式,還有一個更簡潔的例子:

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

以上兩種方式,第一種使用了 ES5 的 Object.defineProperty,第二種則使用了 get/set

生命週期回調方法

元素能夠定義特殊的方法,來注入其生存期內關鍵的時間點。這些方法各自有特定的名稱和用途,它們被恰如其分地命名爲生命週期回調:

回調方法名稱 調用時間點
createdCallback 建立元素實例
enteredDocumentCallback 向文檔插入實例
leftDocumentCallback 從文檔中移除實例
attributeChangedCallback(attrName, oldVal, newVal) 添加,移除,或修改一個屬性

示例:爲 <x-foo> 定義 createdCallback()enteredDocumentCallback()

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

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

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

全部生命週期回調都是可選的,你能夠只在須要關注的時間點定義它們。舉個例子,你有一個很複雜的元素,它會在 createdCallback() 打開一個 indexedDB 鏈接。在將其從 DOM 移除時,leftDocumentCallback() 會作一些必要的清理工做。注意:不要過於依賴這些生命週期方法(若是用戶直接關閉瀏覽器標籤,生命週期方法是沒有機會執行的),僅將其做爲可能的優化點。

另外一個生命週期回調的例子是爲元素設置默認的事件監聽器:

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

添加標記

咱們已經建立好 <x-foo> 並添加了 JavaScript API,但它尚未任何內容。要不咱們給它整點?

生命週期回調在這個時候就派上用場了。咱們甚至能夠用 createdCallback() 給一個元素賦予一些默認的 HTML:

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

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

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

實例化這個標籤並在 DevTools 中觀察,能夠看到以下結構:

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 = "<b>I'm in the element's Shadow DOM!</b>";
};

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

咱們並無直接設置 <x-foo-shadowdom>innerHTML,而是爲其建立了一個用於填充標記的 Shadow Root。在 DevTools 中選中「顯示 Shadow DOM」,你就會看到一個能夠展開的 #document-fragment:

Shadow DOM

這就是 Shadow Root!

從模板建立元素

HTML Template 是另外一組跟自定義元素完美融合的新 API。

模板元素可用於聲明 DOM 片斷。它們能夠被解析並在頁面加載後插入,以及延遲到運行時才進行實例化。模板是聲明自定義元素結構的理想方案。

示例:註冊一個由模板和 Shadow DOM 建立的元素:

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

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

短短几行作了不少事情,咱們挨個來看都發生了些什麼:

  1. 咱們在 HTML 中註冊了一個新元素:<x-foo-from-template>
  2. 這個元素的 DOM 是從一個模板建立的
  3. Shadow DOM 隱藏了該元素的實現細節
  4. Shadow 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 爲元素增長了樣式封裝的特性。Shadow Root 中定義的樣式不會暴露到宿主外部或對頁面產生影響。對自定義元素來講,元素自己是宿主。樣式封裝的屬性也使得自定義元素可以爲本身定義默認樣式。

Shadow DOM 的樣式是一個很大的話題!若是你想更多地瞭解它,推薦你閱讀我寫的其餘文章:

使用 :unresolved 僞類避免無樣式內容閃爍(FOUC)

爲了緩解無樣式內容閃爍的影響,自定義元素規範提出了一個新的 CSS 僞類 :unresolved。在瀏覽器調用你的createdCallback()(請看生命週期回調方法一節)以前,這個僞類均可以匹配到 unresolved 元素。一旦產生調用,就意味着元素已經完成提高,成爲它被定義的形態,該元素就再也不是一個 unresolved 元素。

Chrome 29 已經原生支持 :unresolved 僞類。

示例:註冊後漸顯的 <x-foo> 標籤:

x-foo {
  opacity: 1;
  transition: opacity 300ms;
}
x-foo:unresolved {
  opacity: 0;
}

請記住 :unresolved 僞類只能用於 unresolved 元素,而不能用於繼承自 HTMLUnkownElement 的元素(請看元素如何提高一節)。

<style>
  /* 給全部未提高元素添加邊框 */
  :unresolved {
    border: 1px dashed red;
    display: inline-block;
  }
  /* 未提高的 x-panel 文本內容爲紅色 */
  x-panel:unresolved {
    color: red;
  }
  /* 完成註冊的 x-panel 文本內容爲綠色 */
  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>

瞭解更多 :unresolved 的知識,請看 Polymer 文檔《元素樣式指南》

歷史和瀏覽器支持

特性檢測

特性檢測就是檢查瀏覽器是否提供了 document.register() 接口:

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

if (supportsCustomElements()) {
  // 使用自定義元素 API
} else {
  // 使用其餘類庫建立組件
}

瀏覽器支持

Chrome 27 和 Firefox 23 都提供了對 document.register() 的支持,不過以後規範又有一些演化。Chrome 31 將是第一個支持新規範的版本。提示:在 Chrome 31 中使用自定義元素,須要開啓 about:flags 中的「實驗性 web 平臺特性(Experimental Web Platform features)」選項。

在瀏覽器支持穩定以前,也有一些很好的過渡方案:

HTMLElementElement 怎麼了?

緊跟過標準的人都知道曾經有一個 <element> 標籤。它很是好用,你只要像下面這樣就能夠聲明式的註冊一個新元素:

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

不幸的是,在它的提高過程、邊界案例,以及末日般的複雜場景中,須要處理大量的時序問題。<element> 所以被迫擱置。2013 年 8 月,Dimitri Glazkov 在 public-webapps 郵件組中宣告廢棄 <element>,至少目前看來是廢掉了。

值得注意的是,Polymer 實現了用 形式聲明式地註冊元素。這是怎麼作到的?它用的正是 document.register('polymer-element') 以及 從模板建立元素一節介紹的技術。

結語

自定義元素爲咱們提供了一個工具,經過它咱們能夠擴展 HTML 的詞彙,賦予它新的特性,並把不一樣的 web 平臺鏈接在一塊兒。結合其餘新的基本平臺,如 Shadow DOM 和模板,咱們領略了 web 組件的宏偉藍圖。標記語言將再次變得很時髦!

若是你對使用 web 組件感興趣,建議你去看看 Polymer 框架,從它開始玩吧。

相關文章
相關標籤/搜索