簡介
網路嚴重缺乏言論。有興趣的話,不妨試試 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 基元之一,但這應該是最重要的。假如沒有自訂元素解鎖的功能,網頁元件就不存在:
- 定義新的 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 中加入新標記時能前瞻相容性。
第二個引數是 (選用) 物件,用於說明元素的 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>
元素,請建立新元素並沿用 HTMLButtonElement
的 prototype
和 extends
名稱。在這種情況下,"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 提供自訂元素:
- 隱藏內心的方法,防止使用者遭遇複雜的實作細節。
- 樣式封裝...完全免費!
透過 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>.
</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>.
</template>
<div class="demoarea">
<x-foo-from-template></x-foo-from-template>
</div>
這幾行程式碼就夠用了現在讓我們一起瞭解發生了什麼事:
- 我們已在 HTML 中註冊新元素:
<x-foo-from-template>
- 元素的 DOM 是透過
<template>
建立 - 使用陰影 DOM 隱藏元素的恐怖細節
- 陰影 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 樣式是一大主題!如要進一步瞭解這項工具,我推薦以下幾篇文章:
- Polymer 說明文件中的「樣式元素指南」。
- 「Shadow DOM 201: CSS & Styling」一文。
使用 :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。我有豐富的成就,已足夠讓你開始前進。