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