/

Web Components 自訂元素

Web Components 自訂元素

Web Components 自訂元素初學教學

我使用了一個叫作 CSS Doodle 的 CSS 函式庫創建了我的第一個自訂元素。這是一個很棒的應用,它使用自訂元素讓你創建令人驚艷的 CSS 動畫效果。這激發了我對於了解底層原理的慾望。所以我決定更深入地研究 Web Components,這是一個很多人請我寫的主題。

自訂元素讓我們可以創建新的 HTML 標籤。

我一開始無法想像這有什麼用處,直到我使用了 CSS Doodle 函式庫。畢竟,我們已經有很多標籤了。

這篇教學涵蓋了 Custom Elements 第 1 版,也就是寫作時間的最新版本。

使用自訂元素,我們可以創建一個自訂的 HTML 標籤,並與 CSS 和 JavaScript 相關聯。

這不是 React、Angular 或 Vue 等框架的替代方案,而是一個全新的概念。

window 全局物件暴露了 customElements 屬性,這讓我們可以訪問 CustomElementRegistry 物件。

CustomElementRegistry 物件

這個物件有幾個方法可以用來註冊自訂元素和查詢已註冊的自訂元素:

  • define() 用於定義新的自訂元素
  • get() 用於獲取自訂元素的構造函數(如果不存在則返回 undefined)
  • upgrade() 用於升級自訂元素
  • whenDefined() 用於獲取自訂元素的構造函數。類似於 get(),但它返回的是一個 promise,當該項目可用時解析。

如何創建自訂元素

在我們呼叫 window.customElements.define() 方法之前,我們必須通過創建一個新的類來定義一個新的 HTML 元素,並繼承 HTMLElement 內建類:

1
2
3
class CustomTitle extends HTMLElement {
//...
}

在類的構造函數中,我們將使用 Shadow DOM 來關聯自訂的 CSS、JavaScript 和 HTML 到我們的新標籤。

這樣,我們在 HTML 中看到的只是我們的標籤,但這將封裝很多功能。

我們首先初始化構造函數:

1
2
3
4
5
6
class CustomTitle extends HTMLElement {
constructor() {
super()
//...
}
}

然後,在其中調用 HTMLElement 的 attachShadow() 方法,傳遞一個 mode 為 ‘open’ 的物件。該屬性設置了 shadow DOM 的封裝模式。如果是 ‘open’,我們可以訪問元素的 shadowRoot 屬性。如果是 ‘closed’,我們就不能訪問。

這是如何完成的:

1
2
3
4
5
6
7
class CustomTitle extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
//...
}
}

你可能會在一些範例中看到一種語法 const shadowRoot = this.attachShadow(/* … */),但除非將 mode 設置為 ‘closed’,否則你可以避免使用這種方式,因為你可以通過調用 this.shadowRoot 來始終引用該物件。

現在,我們將使用它來設置其 innerHTML:

1
2
3
4
5
6
7
8
9
class CustomTitle extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
this.shadowRoot.innerHTML = `
<h1>My Custom Title!</h1>
`
}
}

你可以添加任意多的標籤,你並不受 innerHTML 屬性內只能有一個標籤的限制。

現在,我們將這個新定義的元素添加到 window.customElements 中:

1
window.customElements.define('custom-title', CustomTitle)

並且我們可以在頁面中使用 自訂元素!

注意:你不能使用自結標籤(換句話說,標籤 <custom-title /> 不被標準支持)。

注意標籤名稱中的破折號 -,這是我們必須在自訂元素中使用的。這是用來區分內建標籤和自訂標籤的方法。

現在這個元素已經在頁面中,我們可以像其他標籤一樣用 CSS 和 JavaScript 定位它!

為元素提供自訂 CSS

在構造函數中,除了定義內容的 HTML 標籤之外,你可以添加一個 style 標籤,並在其中為自訂元素添加 CSS:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CustomTitle extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
this.shadowRoot.innerHTML = `
<style>
h1 {
font-size: 7rem;
color: #000;
font-family: Helvetica;
text-align: center;
}
</style>
<h1>My Custom Title!</h1>
`
}
}

以下是我們創建的範例自訂元素,在 CodePen 上:

參見 Web Components, My Custom Title 由 Flavio Copes (@flaviocopes)
around CodePen

較簡短的語法

我們除了先定義類,再呼叫 window.customElements.define(),我們還可以使用以下簡寫語法來 內聯 定義類:

1
2
3
4
5
window.customElements.define('custom-title', class extends HTMLElement {
constructor() {
...
}
})

添加 JavaScript

就像我們對 CSS 所做的那樣,我們也可以嵌入 JavaScript。

不過,我們不能直接將 JavaScript 添加到模板標籤中,就像我們對 CSS 所做的那樣。

在自訂元素的構造函數中,我在這裡定義了一個點擊事件監聽器:

1
2
3
4
5
6
7
8
9
10
11
12
class CustomTitle extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
this.shadowRoot.innerHTML = `
<h1>My Custom Title!</h1>
`
this.addEventListener('click', e => {
alert('clicked!')
})
}
}

替代方案:使用模板

除了在 JavaScript 字符串中定義 HTML 和 CSS,你還可以在 HTML 中使用 template 標籤並分配一個 id:

1
2
3
4
5
6
7
8
9
10
11
12
13
<template id="custom-title-template">
<style>
h1 {
font-size: 7rem;
color: #000;
font-family: Helvetica;
text-align: center;
}
</style>
<h1>My Custom Title!</h1>
</template>

<custom-title></custom-title>

然後,你可以在自訂元素的構造函數中引用它,並將其添加到 Shadow DOM 中:

1
2
3
4
5
6
7
8
9
10
class CustomTitle extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
const tmpl = document.querySelector('#custom-title-template')
this.shadowRoot.appendChild(tmpl.content.cloneNode(true))
}
}

window.customElements.define('custom-title', CustomTitle)

CodePen 上的範例:

參見 Web Components, My Custom Title with template 由 Flavio Copes (@flaviocopes)
around CodePen

生命周期方法

除了構造函數之外,自訂元素類還可以定義特殊方法,這些方法在元素生命周期的特定時間執行:

  • connectedCallback 當元素被插入到 DOM 中時
  • disconnectedCallback 當元素從 DOM 中被移除時
  • attributeChangedCallback 當已觀察的屬性更改或新增或刪除時
  • adoptedCallback 當元素已移動到新的文檔
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CustomTitle extends HTMLElement {
constructor() {
...
}
connectedCallback() {
...
}
disconnectedCallback() {
...
}
attributeChangedCallback(attrName, oldVal, newVal) {
...
}
}

attributeChangedCallback() 接收 3 個參數:

  • 屬性名稱
  • 屬性的舊值
  • 屬性的新值

我上面提到的它聽取觀察的屬性。什麼是觀察的屬性?我們必須在由觀察的屬性組成的陣列中定義它們,而這個陣列是由靜態方法 observedAttributes 返回的:

1
2
3
4
5
6
7
8
9
10
11
12
13
class CustomTitle extends HTMLElement {
constructor() {
...
}

static get observedAttributes() {
return ['disabled']
}

attributeChangedCallback(attrName, oldVal, newVal) {
...
}
}

我定義了觀察 disabled 屬性。現在當它改變時,例如在 JavaScript 中設置 disabled 為 true:

1
document.querySelector('custom-title').disabled = true

attributeChangedCallback() 調用,並傳遞參數 'disabled',false,true

注意:在不明原因下,JavaScript 也可以調用 attributeChangedCallback() 應該只由瀏覽器自動調用。

定義自訂屬性

你可以通過為自訂元素添加 getter 和 setter 來定義自訂屬性:

1
2
3
4
5
6
7
8
9
10
11
12
13
class CustomTitle extends HTMLElement {
static get observedAttributes() {
return ['mycoolattribute']
}

get mycoolattribute() {
return this.getAttribute('mycoolattribute')
}

set mycoolattribute(value) {
this.setAttribute('mycoolattribute', value)
}
}

這是定義布爾屬性(如果存在的話就是 “true”)的方式,就像 HTML 元素的 disabled 屬性一樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CustomTitle extends HTMLElement {
static get observedAttributes() {
return ['booleanattribute']
}

get booleanattribute() {
return this.hasAttribute('booleanattribute')
}

set booleanattribute(value) {
if (value) {
this.setAttribute('booleanattribute', '')
} else {
this.removeAttribute('booleanattribute')
}
}
}

如何為尚未定義的自訂元素設置樣式

JavaScript 可能需要一些時間才能啟動,因此頁面在元素添加到頁面之前可能沒有定義自訂元素,從而導致醜陋的頁面布局重置。

為解決這個問題,在可用時,添加一個 :not(:defined) 的 CSS 虛擬類,設置元素的高度並淡入:

1
2
3
4
5
6
custom-title:not(:defined) {
display: block;
height: 400px;
opacity: 0;
transition: opacity 0.5s ease-in-out;
}

我們是否可以在所有瀏覽器中使用它們?

Firefox、Safari 和 Chrome 目前的版本支持它們。IE 永遠不會支持它們,而 Edge 在撰寫時正在開發支援。

你可以使用此 polyfill 在較舊的瀏覽器上添加更好的支援。tags: Web Components, Custom Elements