簡介
網路嚴重缺乏表現。請看「新型」網頁應用程式 (例如 Gmail),瞭解我的意思:
<div>
湯並非現代料理,然而,這就是我們建構網頁應用程式的方式。很遺憾。我們不該為平台提出更多廣告需求嗎?
性感的標記。我們一起來挑戰吧
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 元素。這個規格是 Web Components 中幾個新 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 元素,以及其他自訂元素。如要擴充元素,您必須傳遞該元素繼承的來源元素名稱和 prototype
,registerElement()
才能加以傳遞。
擴充原生元素
假設你對「常規喬<button>
」不太滿意,您想要增強其功能成「Mega 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
的方法其實有十萬種。如果您不喜歡建立這類原型,以下是同樣功能的較精簡版本:
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()
中開啟至 IndexedDB 的連線。在從 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>
建立了陰影根目錄,然後使用標記填入。在開發人員工具中啟用「Show Shadow 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>
建立 - 使用 Shadow DOM 即可隱藏元素的可怕細節
- 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 DOM 會為元素注入樣式封裝。在陰影根層級中定義的樣式不會從主機外流,也不會從網頁上流出。如果是自訂元素,則元素本身就是主機。樣式封裝的屬性也允許自訂元素為自己定義預設樣式。
陰影 DOM 樣式是個很大的主題!如要進一步瞭解這項功能,建議您參閱以下幾篇文章:
- 請參閱 Polymer 說明文件中的「A Guide to Styling Elements」(設定元素樣式指南) 一文。
- 請參閱「Shadow DOM 201:CSS 和樣式」一文。
使用 :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 月,Dimitri Glazkov 在 public-webapps 發布公告,宣布至少目前已移除這項功能。
值得一提的是,Polymer 會使用 <polymer-element>
實作宣告式元素註冊表單。該怎麼做呢?其中包含 document.registerElement('polymer-element')
和使用範本建立元素中所述的技巧。
結論
自訂元素可讓我們擴充 HTML 詞彙、教導新技巧,並快速體驗網路平台中的蟲子。將這些元素與其他新的平台基本元素 (例如 Shadow DOM 和 <template>
) 結合,我們就能開始實現 Web 元件的圖像。標記再好不過!
如果想要開始使用網頁元件,建議您參考 Polymer。總值足以讓你繼續前進。