W3Cschool
恭喜您成為首批注冊(cè)用戶
獲得88經(jīng)驗(yàn)值獎(jiǎng)勵(lì)
我們可以通過(guò)描述帶有自己的方法、屬性和事件等的類(lèi)來(lái)創(chuàng)建自定義 HTML 元素。
在 custom elements (自定義標(biāo)簽)定義完成之后,我們可以將其和 HTML 的內(nèi)建標(biāo)簽一同使用。
這是一件好事,因?yàn)殡m然 HTML 有非常多的標(biāo)簽,但仍然是有窮盡的。如果我們需要像 <easy-tabs>
、<sliding-carousel>
、<beautiful-upload>
…… 這樣的標(biāo)簽,內(nèi)建標(biāo)簽并不能滿足我們。
我們可以把上述的標(biāo)簽定義為特殊的類(lèi),然后使用它們,就好像它們本來(lái)就是 HTML 的一部分一樣。
Custom elements 有兩種:
HTMLElement
? 抽象類(lèi).HTMLButtonElement
? 等。我們將會(huì)先創(chuàng)建 autonomous 元素,然后再創(chuàng)建 customized built-in 元素。
在創(chuàng)建 custom elements 的時(shí)候,我們需要告訴瀏覽器一些細(xì)節(jié),包括:如何展示它,以及在添加元素到頁(yè)面和將其從頁(yè)面移除的時(shí)候需要做什么,等等。
通過(guò)創(chuàng)建一個(gè)帶有幾個(gè)特殊方法的類(lèi),我們可以完成這件事。這非常容易實(shí)現(xiàn),我們只需要添加幾個(gè)方法就行了,同時(shí)這些方法都不是必須的。
下面列出了這幾個(gè)方法的概述:
class MyElement extends HTMLElement {
constructor() {
super();
// 元素在這里創(chuàng)建
}
connectedCallback() {
// 在元素被添加到文檔之后,瀏覽器會(huì)調(diào)用這個(gè)方法
//(如果一個(gè)元素被反復(fù)添加到文檔/移除文檔,那么這個(gè)方法會(huì)被多次調(diào)用)
}
disconnectedCallback() {
// 在元素從文檔移除的時(shí)候,瀏覽器會(huì)調(diào)用這個(gè)方法
// (如果一個(gè)元素被反復(fù)添加到文檔/移除文檔,那么這個(gè)方法會(huì)被多次調(diào)用)
}
static get observedAttributes() {
return [/* 屬性數(shù)組,這些屬性的變化會(huì)被監(jiān)視 */];
}
attributeChangedCallback(name, oldValue, newValue) {
// 當(dāng)上面數(shù)組中的屬性發(fā)生變化的時(shí)候,這個(gè)方法會(huì)被調(diào)用
}
adoptedCallback() {
// 在元素被移動(dòng)到新的文檔的時(shí)候,這個(gè)方法會(huì)被調(diào)用
// (document.adoptNode 會(huì)用到, 非常少見(jiàn))
}
// 還可以添加更多的元素方法和屬性
}
在申明了上面幾個(gè)方法之后,我們需要注冊(cè)元素:
// 讓瀏覽器知道我們新定義的類(lèi)是為 <my-element> 服務(wù)的
customElements.define("my-element", MyElement);
現(xiàn)在當(dāng)任何帶有 <my-element>
標(biāo)簽的元素被創(chuàng)建的時(shí)候,一個(gè) MyElement
的實(shí)例也會(huì)被創(chuàng)建,并且前面提到的方法也會(huì)被調(diào)用。我們同樣可以使用 document.createElement('my-element')
在 JavaScript 里創(chuàng)建元素。
Custom element 名稱(chēng)必須包括一個(gè)短橫線 ?
-
?Custom element 名稱(chēng)必須包括一個(gè)短橫線
-
, 比如my-element
和super-button
都是有效的元素名,但myelement
并不是。
這是為了確保 custom element 和內(nèi)建 HTML 元素之間不會(huì)發(fā)生命名沖突。
舉個(gè)例子,HTML 里面已經(jīng)有 <time>
元素了,用于顯示日期/時(shí)間。但是這個(gè)標(biāo)簽本身并不會(huì)對(duì)時(shí)間進(jìn)行任何格式化處理。
讓我們來(lái)創(chuàng)建一個(gè)可以展示適用于當(dāng)前瀏覽器語(yǔ)言的時(shí)間格式的 <time-formatted>
元素:
<script>
class TimeFormatted extends HTMLElement { // (1)
connectedCallback() {
let date = new Date(this.getAttribute('datetime') || Date.now());
this.innerHTML = new Intl.DateTimeFormat("default", {
year: this.getAttribute('year') || undefined,
month: this.getAttribute('month') || undefined,
day: this.getAttribute('day') || undefined,
hour: this.getAttribute('hour') || undefined,
minute: this.getAttribute('minute') || undefined,
second: this.getAttribute('second') || undefined,
timeZoneName: this.getAttribute('time-zone-name') || undefined,
}).format(date);
}
}
customElements.define("time-formatted", TimeFormatted); // (2)
</script>
<!-- (3) -->
<time-formatted datetime="2019-12-01"
year="numeric" month="long" day="numeric"
hour="numeric" minute="numeric" second="numeric"
time-zone-name="short"
></time-formatted>
connectedCallback()
? —— 在 ?<time-formatted>
? 元素被添加到頁(yè)面的時(shí)候,瀏覽器會(huì)調(diào)用這個(gè)方法(或者當(dāng) HTML 解析器檢測(cè)到它的時(shí)候),它使用了內(nèi)建的時(shí)間格式化工具 Intl.DateTimeFormat,這個(gè)工具可以非常好地展示格式化之后的時(shí)間,在各瀏覽器中兼容性都非常好。customElements.define(tag, class)
? 來(lái)注冊(cè)這個(gè)新元素。Custom elements 升級(jí)
如果瀏覽器在
customElements.define
之前的任何地方見(jiàn)到了<time-formatted>
元素,并不會(huì)報(bào)錯(cuò)。但會(huì)把這個(gè)元素當(dāng)作未知元素,就像任何非標(biāo)準(zhǔn)標(biāo)簽一樣。
:not(:defined)
CSS 選擇器可以對(duì)這樣「未定義」的元素加上樣式。
當(dāng)
customElement.define
被調(diào)用的時(shí)候,它們被「升級(jí)」了:一個(gè)新的TimeFormatted
元素為每一個(gè)標(biāo)簽創(chuàng)建了,并且connectedCallback
被調(diào)用。它們變成了:defined
。
我們可以通過(guò)這些方法來(lái)獲取更多的自定義標(biāo)簽的信息:
- ?
customElements.get(name)
? —— 返回指定 custom element ?name
? 的類(lèi)。- ?
customElements.whenDefined(name)
? – 返回一個(gè) promise,將會(huì)在這個(gè)具有給定 ?name
? 的 custom element 變?yōu)橐讯x狀態(tài)的時(shí)候 resolve(不帶值)。
在 ?
connectedCallback
? 中渲染,而不是 ?constructor
? 中在上面的例子中,元素里面的內(nèi)容是在
connectedCallback
中渲染(創(chuàng)建)的。
為什么不在
constructor
中渲染?
原因很簡(jiǎn)單:在
constructor
被調(diào)用的時(shí)候,還為時(shí)過(guò)早。雖然這個(gè)元素實(shí)例已經(jīng)被創(chuàng)建了,但還沒(méi)有插入頁(yè)面。在這個(gè)階段,瀏覽器還沒(méi)有處理/創(chuàng)建元素屬性:調(diào)用getAttribute
將會(huì)得到null
。所以我們并不能在那里渲染元素。
而且,如果你仔細(xì)考慮,這樣作對(duì)于性能更好 —— 推遲渲染直到真正需要的時(shí)候。
在元素被添加到文檔的時(shí)候,它的
connectedCallback
方法會(huì)被調(diào)用。這個(gè)元素不僅僅是被添加為了另一個(gè)元素的子元素,同樣也成為了頁(yè)面的一部分。因此我們可以構(gòu)建分離的 DOM,創(chuàng)建元素并且讓它們?yōu)橹蟮氖褂脺?zhǔn)備好。它們只有在插入頁(yè)面的時(shí)候才會(huì)真的被渲染。
我們目前的 <time-formatted>
實(shí)現(xiàn)中,在元素渲染以后,后續(xù)的屬性變化并不會(huì)帶來(lái)任何影響。這對(duì)于 HTML 元素來(lái)說(shuō)有點(diǎn)奇怪。通常當(dāng)我們改變一個(gè)屬性的時(shí)候,比如 a.href
,我們會(huì)預(yù)期立即看到變化。我們將會(huì)在下面修正這一點(diǎn)。
為了監(jiān)視這些屬性,我們可以在 observedAttributes()
static getter 中提供屬性列表。當(dāng)這些屬性發(fā)生變化的時(shí)候,attributeChangedCallback
會(huì)被調(diào)用。出于性能優(yōu)化的考慮,其他屬性變化的時(shí)候并不會(huì)觸發(fā)這個(gè)回調(diào)方法。
以下是 <time-formatted>
的新版本,它會(huì)在屬性變化的時(shí)候自動(dòng)更新:
<script>
class TimeFormatted extends HTMLElement {
render() { // (1)
let date = new Date(this.getAttribute('datetime') || Date.now());
this.innerHTML = new Intl.DateTimeFormat("default", {
year: this.getAttribute('year') || undefined,
month: this.getAttribute('month') || undefined,
day: this.getAttribute('day') || undefined,
hour: this.getAttribute('hour') || undefined,
minute: this.getAttribute('minute') || undefined,
second: this.getAttribute('second') || undefined,
timeZoneName: this.getAttribute('time-zone-name') || undefined,
}).format(date);
}
connectedCallback() { // (2)
if (!this.rendered) {
this.render();
this.rendered = true;
}
}
static get observedAttributes() { // (3)
return ['datetime', 'year', 'month', 'day', 'hour', 'minute', 'second', 'time-zone-name'];
}
attributeChangedCallback(name, oldValue, newValue) { // (4)
this.render();
}
}
customElements.define("time-formatted", TimeFormatted);
</script>
<time-formatted id="elem" hour="numeric" minute="numeric" second="numeric"></time-formatted>
<script>
setInterval(() => elem.setAttribute('datetime', new Date()), 1000); // (5)
</script>
render()
? 這個(gè)輔助方法里面。attributeChangedCallback
? 在 ?observedAttributes()
? 里的屬性改變的時(shí)候被調(diào)用。在 HTML 解析器構(gòu)建 DOM 的時(shí)候,會(huì)按照先后順序處理元素,先處理父級(jí)元素再處理子元素。例如,如果我們有 <outer><inner></inner></outer>
,那么 <outer>
元素會(huì)首先被創(chuàng)建并接入到 DOM,然后才是 <inner>
。
這對(duì) custom elements 產(chǎn)生了重要影響。
比如,如果一個(gè) custom element 想要在 connectedCallback
內(nèi)訪問(wèn) innerHTML
,它什么也拿不到:
<script>
customElements.define('user-info', class extends HTMLElement {
connectedCallback() {
alert(this.innerHTML); // empty (*)
}
});
</script>
<user-info>John</user-info>
如果你運(yùn)行上面的代碼,alert
出來(lái)的內(nèi)容是空的。
這正是因?yàn)樵谀莻€(gè)階段,子元素還不存在,DOM 還沒(méi)有完成構(gòu)建。HTML 解析器先連接 custom element <user-info>
,然后再處理子元素,但是那時(shí)候子元素還并沒(méi)有加載上。
如果我們要給 custom element 傳入信息,我們可以使用元素屬性。它們是即時(shí)生效的。
或者,如果我們需要子元素,我們可以使用延遲時(shí)間為零的 setTimeout
來(lái)推遲訪問(wèn)子元素。
這樣是可行的:
<script>
customElements.define('user-info', class extends HTMLElement {
connectedCallback() {
setTimeout(() => alert(this.innerHTML)); // John (*)
}
});
</script>
<user-info>John</user-info>
現(xiàn)在 alert
在 (*)
行展示了 「John」,因?yàn)槲覀兪窃?HTML 解析完成之后,才異步執(zhí)行了這段程序。我們?cè)谶@個(gè)時(shí)候處理必要的子元素并且結(jié)束初始化過(guò)程。
另一方面,這個(gè)方案并不是完美的。如果嵌套的 custom element 同樣使用了 setTimeout
來(lái)初始化自身,那么它們會(huì)按照先后順序執(zhí)行:外層的 setTimeout
首先觸發(fā),然后才是內(nèi)層的。
這樣外層元素還是早于內(nèi)層元素結(jié)束初始化。
讓我們用一個(gè)例子來(lái)說(shuō)明:
<script>
customElements.define('user-info', class extends HTMLElement {
connectedCallback() {
alert(`${this.id} 已連接。`);
setTimeout(() => alert(`${this.id} 初始化完成。`));
}
});
</script>
<user-info id="outer">
<user-info id="inner"></user-info>
</user-info>
輸出順序:
我們可以很明顯地看到外層元素并沒(méi)有等待內(nèi)層元素。
并沒(méi)有任何內(nèi)建的回調(diào)方法可以在嵌套元素渲染好之后通知我們。但我們可以自己實(shí)現(xiàn)這樣的回調(diào)。比如,內(nèi)層元素可以分派像 initialized
這樣的事件,同時(shí)外層的元素監(jiān)聽(tīng)這樣的事件并做出響應(yīng)。
我們創(chuàng)建的 <time-formatted>
這些新元素,并沒(méi)有任何相關(guān)的語(yǔ)義。搜索引擎并不知曉它們的存在,同時(shí)無(wú)障礙設(shè)備也無(wú)法處理它們。
但上述兩點(diǎn)同樣是非常重要的。比如,搜索引擎會(huì)對(duì)這些事情感興趣,比如我們真的展示了時(shí)間?;蛘呷绻覀儎?chuàng)建了一個(gè)特別的按鈕,為什么不復(fù)用已有的 <button>
功能呢?
我們可以通過(guò)繼承內(nèi)建元素的類(lèi)來(lái)擴(kuò)展和定制它們。
比如,按鈕是 HTMLButtonElement
的實(shí)例,讓我們?cè)谶@個(gè)基礎(chǔ)上創(chuàng)建元素。
HTMLButtonElement
:class HelloButton extends HTMLButtonElement { /* custom element 方法 */ }
customElements.define
提供定義標(biāo)簽的第三個(gè)參數(shù):customElements.define('hello-button', HelloButton, {extends: 'button'});
這一步是必要的,因?yàn)椴煌臉?biāo)簽會(huì)共享同一個(gè)類(lèi)。
<button>
標(biāo)簽,但添加 is="hello-button"
到這個(gè)元素,這樣就可以使用我們的 custom element:<button is="hello-button">...</button>
下面是一個(gè)完整的例子:
<script>
// 這個(gè)按鈕在被點(diǎn)擊的時(shí)候說(shuō) "hello"
class HelloButton extends HTMLButtonElement {
constructor() {
super();
this.addEventListener('click', () => alert("Hello!"));
}
}
customElements.define('hello-button', HelloButton, {extends: 'button'});
</script>
<button is="hello-button">Click me</button>
<button is="hello-button" disabled>Disabled</button>
我們新定義的按鈕繼承了內(nèi)建按鈕,所以它擁有和內(nèi)建按鈕相同的樣式和標(biāo)準(zhǔn)特性,比如 disabled
屬性。
有兩種 custom element:
HTMLElement
?。定義方式:
class MyElement extends HTMLElement {
constructor() { super(); /* ... */ }
connectedCallback() { /* ... */ }
disconnectedCallback() { /* ... */ }
static get observedAttributes() { return [/* ... */]; }
attributeChangedCallback(name, oldValue, newValue) { /* ... */ }
adoptedCallback() { /* ... */ }
}
customElements.define('my-element', MyElement);
/* <my-element> */
需要多一個(gè) .define
參數(shù),同時(shí) is="..."
在 HTML 中:
class MyButton extends HTMLButtonElement { /*...*/ }
customElements.define('my-button', MyElement, {extends: 'button'});
/* <button is="my-button"> */
Custom element 在各瀏覽器中的兼容性已經(jīng)非常好了。Edge 支持地相對(duì)較差,但是我們可以使用 polyfill https://github.com/webcomponents/webcomponentsjs。
我們已經(jīng)創(chuàng)建了 <time-formatted>
元素用于展示格式化好的時(shí)間。
創(chuàng)建一個(gè) <live-timer>
元素用于展示當(dāng)前時(shí)間:
<time-formatted>
?,不要重復(fù)實(shí)現(xiàn)這個(gè)元素的功能。tick
? 被生成,這個(gè)事件的 ?event.detail
? 屬性帶有當(dāng)前日期。(參考章節(jié) 創(chuàng)建自定義事件 )。使用方式:
<live-timer id="elem"></live-timer>
<script>
elem.addEventListener('tick', event => console.log(event.detail));
</script>
請(qǐng)注意:
setInterval
? 的 timer。這非常重要,否則即使我們不再需要它了,它仍然會(huì)繼續(xù)計(jì)時(shí)。這樣瀏覽器就不能清除這個(gè)元素占用和被這個(gè)元素引用的內(nèi)存了。elem.date
? 屬性得到當(dāng)前時(shí)間。類(lèi)所有的方法和屬性天生就是元素的方法和屬性。Copyright©2021 w3cschool編程獅|閩ICP備15016281號(hào)-3|閩公網(wǎng)安備35020302033924號(hào)
違法和不良信息舉報(bào)電話:173-0602-2364|舉報(bào)郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號(hào)
聯(lián)系方式:
更多建議: