自定義元素–爲你的HTML代碼定義新元素

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

引言

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

gmail-自定義元素

堆砌 <div> 一點都不現代。然而可悲的是,這就是咱們構建 web 應用的方式。在現有基礎上咱們不該該有更高的追求嗎?html

您還能夠參考如下HTML5/CSS3相關文章:
使用CSS3開啓GPU硬件加速提高網站動畫渲染性能
HTML5 WebSockets 基礎使用教程
關於HTML 5 canvas 的基礎教程
讓全部IE支持HTML5的解決方案html5

時髦的標記,行動起來!

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

假如 GMail 的標記不那麼糟糕,而是像下面這樣漂亮,那會怎樣?web

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

 

查看演示:canvas

 

真是使人耳目一新!這個應用太合理了,既有意義,又容易理解。最妙的是,它是可維護的,只要查看聲明結構就能夠清楚地知道它的做用。瀏覽器

自定義元素,救救咱們!就期望你了!websocket

趕忙開始吧

自定義元素容許開發者定義新的 HTML 元素類型。該規範只是 Web 組件模塊提供的衆多新 API 中的一個,但它也極可能是最重要的一個。沒有自定義元素帶來的如下特性,Web 組件都不會存在:app

  • 定義新的 HTML/DOM 元素
  • 基於其餘元素建立擴展元素
  • 給一個標籤綁定一組自定義功能
  • 擴展已有 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 規範定義的元素,同時確保了 HTML 增長新標籤時的向前兼容。

第二個參數是一個(可選的)對象,用於描述該元素的 prototype。在這裏能夠爲元素添加自定義功能(例如:公開屬性和方法)。稍後詳述。

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

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

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

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

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

擴展原生元素

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

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

提示:要建立擴展自元素 B 的元素 A,元素 A 必須繼承元素 B 的 prototype。

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

示例:

<button is="mega-button">

元素如何提高

你有沒有想過爲何 HTML 解析器對非標準標籤不報錯?好比,咱們在頁面中聲明一個 <randomtag>,一切都很和諧。根據 HTML 規範的表述:

非規範定義的元素必須使用 HTMLUnknownElement 接口。
– HTML 規範

<randomtag> 是非標準的,它會繼承 HTMLUnknownElement

對自定義元素來講,狀況就不同了。擁有合法元素名的自定義元素將繼承 HTMLElement。你能夠按 Ctrl+Shift+J(Mac 系統爲 Cmd+Opt+J)打開控制檯,運行下面這段代碼,獲得的結果將是 true:

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

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

UNRESOLVED 元素
因爲自定義元素是經過腳本執行 document.registerElement() 註冊的,所以 它們可能在元素定義被註冊到瀏覽器以前就已經聲明或建立過了。例如:你能夠先在頁面中聲明 <x-tabs>,之後再調用 document.registerElement('x-tabs')

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

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

類型 繼承自 示例
unresolved 元素 HTMLElement <x-tabs><my-element><my-awesome-app>
未知元素 HTMLUnknownElement <tabs><foo_bar>

把 unresolved 元素想象成尚處於中間狀態,它們都是等待被瀏覽器提高的潛在候選者。瀏覽器說:「你具有一個新元素的所有特徵,我保證會在賦予你定義的時候將你提高爲一個元素」。

實例化元素

咱們建立普通元素用到的一些技術也能夠用於自定義元素。和全部標準定義的元素同樣,自定義元素既能夠在 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.registerElement() 來向瀏覽器註冊一個新標籤。但這還不夠,接下來咱們要向新標籤添加屬性和方法。

添加 JS 屬性和方法

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

這裏有一個完整的示例:

var XFooProto = Object.create(HTMLElement.prototype);
 
// 1. 爲 x-foo 建立 foo() 方法
XFooProto.foo function({
  alert('foo() called');
};
 
// 2. 定義一個只讀的「bar」屬性
Object.defineProperty(XFooProto"bar"{value5});
 
// 3. 註冊 x-foo 的定義
var XFoo = document.registerElement('x-foo'{prototype: XFooProto});
 
// 4. 建立一個 x-foo 實例
var xfoo = document.createElement('x-foo');
 
// 5. 插入頁面
document.body.appendChild(xfoo);

構造 prototype 的方法多種多樣,若是你不喜歡上面這種方式,再看一個更簡潔的例子:

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

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

生命週期回調方法

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

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

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

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

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

若是你的元素太笨重,是不會有人用它的。生命週期回調能夠幫你大忙!

添加標記

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

生命週期回調在這個時候就派上用場了。咱們甚至能夠用 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.registerElement('x-foo-with-markup'{prototype: XFooProto});

實例化這個標籤並在 DevTools 中觀察(右擊,選擇「審查元素」),能夠看到以下結構:

<x-foo-with-markup>
   <b>I'm an x-foo-with-markup!</b>
 </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. 爲元素附加一個 shadow root。
  var shadow this.createShadowRoot();
 
  // 2. 填入標記。
  shadow.innerHTML "<b>I'm in the element's Shadow DOM!</b>";
};
 
var XFoo = document.registerElement('x-foo-shadowdom'{prototype: XFooProto});

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

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

這就是 Shadow Root!

從模板建立元素

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

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

示例:註冊一個由 <template> 和 Shadow DOM 建立的元素:

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

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

  1. 咱們在 HTML 中註冊了一個新元素:<x-foo-from-template>
  2. 這個元素的 DOM 是從一個 <template> 建立的
  3. Shadow DOM 隱藏了該元素可怕的細節
  4. Shadow DOM 也對元素的樣式進行了隔離(好比 {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 已經原生支持 CSS :unresolved 僞類。

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

x-foo {
  opacity1;
  transition: opacity 300ms;
}
x-foo:unresolved {
  opacity0;
}

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

<style> /* 給全部 unresolved 元素添加邊框 */ :unresolved { border: 1px dashed red; display: inline-block; } /* unresolved 元素 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 文檔《元素樣式指南》

瀏覽器支持

Chrome 27 和 Firefox 23 都提供了對 document.registerElement() 的支持,不過以後規範又有一些演化。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 實現了以 <polymer-element> 的形式聲明式地註冊元素。這是怎麼作到的?它用的正是 document.registerElement('polymer-element') 以及我在從模板建立元素一節介紹的技術。

結語

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

若是你對使用 web 組件感興趣,建議你看看 Polymer,它已經有了你想要的一切。

英文原文:《Custom Elements: defining new elements in HTML
轉載原文:http://forcefront.com/2013/custom-elements-defining-new-elements-in-html

相關文章
相關標籤/搜索