Web Components 小欖

RecoReco發表於2018-12-02

Web Components技術可以把一組相關的HTML、JS程式碼和CSS風格打包成為一個自包含的元件,只要使用大家熟悉的標籤即可引入此元件。Web Components技術包括:

  1. Custom Element
  2. Shadow DOM
  3. Template
  4. HTML Import

四個分離而又互相關的四個構造塊。其中核心的即使是Custom Element、Shadow DOM,順便會講到而Template是一個支援技術。 HTML Import曾經被Chrome加入但是隨後和V0一起被廢棄。這裡也不會討論它。

Custom Element 定製元素。

定製元素可以在原生元素外建立定製元素。定製元素是Web元件的一個基本構成塊。可以在一個js檔案內包含Custom Element需要的全部要素,包括HTML模板、CSS Style和ES6類。並使用一個HTML檔案,引用此js檔案從而可以使用定製元素。

假設我們建立Spin Button,定製元素標籤為:

<spin-button value=“100” step="10" min="50" max="150"></spin-button>
複製程式碼

我們首先實現此定製元素,但是為了簡單起見,晚一點才看它的屬性。此定製元素內部有一個加號按鈕,一個減號按鈕,一個span顯示當前值。那麼只需要把這個HTML模板組織、風格和程式碼組合在一個檔案內:

var template = `
	<button inc>+</button><span>1</span><button dec>-</button>
	<style>
		span{color:red;}
		*{font-size:2rem;}
	</style>
`
class SpinButton extends HTMLElement{
	connectedCallback(){
		this.innerHTML = template
		var b1 = this.querySelector('[inc]')
		var b2 = this.querySelector('[dec]')
		var s = this.querySelector('span')
		var i = 1
		b1.onclick = function(){
			s.innerHTML = i++
		}
		b2.onclick = function(){
			s.innerHTML = i--
		}
	}
}
customElements.define('spin-button',SpinButton)
複製程式碼

並且建立一個index.html檔案載入此檔案,即可使用新的定製元素spin-button了:

<script src="./spin.js"></script>
<spin-button></spin-button>
複製程式碼

你可以看到執行在瀏覽器內的介面上的兩個按鈕和一個span。建立一個定製元素有幾個要點:

  1. 新的JS定製類需要繼承於類HTMLElement
  2. 回撥connectedCallback提供一個生命週期事件,當定製元素成功掛接到DOM後,會呼叫此回撥,可以在此回撥程式碼內加入自己的定製內容
  3. 程式碼中的this,指向了此定製元素本身,因此可以通過this.innerHTML設定本定製元素的內部DOM

這樣,我們建立了一個獨特的定製元素,這個元素不在原生的瀏覽器標籤內。

定製元素就是這樣建立了,並且對於使用者來說,只要通過熟悉的元素標籤,即可引用一組帶有定製風格、操作和介面的元件了。

但是此時的定製元素有一個問題,就是它內部定義的風格,不僅僅會影響內部的元素,也會洩露到外部導致文件也被影響,從而引發我們不希望的邊際效應。比如在index.html內如果在檔案尾部加入這樣的文字:

<span>black</span>
複製程式碼

你會發現black文字不是預設的顏色,而是紅色,這樣紅色來自於定製元素內部的風格定義程式碼。如果希望隔離元件內的風格定義,那麼可以使用Shaddow DOM技術。此主題會在下一部分內介紹。

Shadow DOM

Web建站使用元件技術有比較長的歷史了,這個技術一直以來都有一個挑戰,就是如何讓一個頁面可以使用第三方控制元件,但是不會被此元件使用的CSS風格所影響。解決方案是CSS可以區域性化。想要元件內部的風格不會影響到外部,辦法就是使用Shadow DOM。Shadow DOM建立了一個隔離區,在這個隔離區內的DOM是獨立的,這意味著:

  1. 內部DOM Tree不會被外部文件訪問到
  2. 也不會被外部的風格設定影響
  3. 內部的風格也不會影響到外部文件

我們拿前一個案例程式碼做實驗,看看如果使用這個技術特性。

使用Shadow DOM的關鍵,是首先建立一個Shadow Node,整個元件內部的HTML片段都插入到此節點內,而不是直接使用元件的innerHTML。我們可以在元件物件的構造器內執行此程式碼:

class SpinButton extends HTMLElement{
	constructor(){
		super()
		var shadow = this.attachShadow({mode:'open'})
		var t = document.createElement('template')
		t.innerHTML = template
		shadow.appendChild(t.content.cloneNode(true))
	}
}
複製程式碼

執行後,你會發現span的風格不再影響元件之外的標籤。看起來還是很簡單的,只要把你本來需要構造的HTML內部DOM插入到shadow節點內即可。

定製元素的屬性

元素的屬性被稱為Attribute,JS物件內的屬性被稱為Property。程式碼慣例上每一個Attribute都會有JS物件的一個Property對應。為了方便,我們希望新增的Attribute可以和JS內的Property同步。就是說,如果有人通過HTML DOM API修改了Attribute,那麼我希望對於的JS屬性會被同步修改;反之亦然,有人修改了Property,那麼這個修改可以會同步修改到對應的Attribute。

我們以spin-button的value屬性為例。定義一個普通的Property的方法是通過get/set關鍵字,比如定義value:

get value(){}
set value(newValue){}
複製程式碼

隨後就可以使用object.value訪問此屬性值,或者通過object.value = newValue為屬性設定新值。可以在兩個函式內通過程式碼設定和Attribute同步:

get value(){
	return this.getAttribute('value') || 1
}
set value(v){
	this.setAttribute('value',v)
}
複製程式碼

這樣程式碼內通過對屬性value的訪問,最後都會導致對Attribute的訪問。如果有程式碼對Attribute訪問,如何修改Attribute的同時同步更新Property呢。這就需要利用HTMLElement提供的生命週期方法了:

static get observedAttributes() {
  return ['value'];
}
attributeChangedCallback(name, oldValue, newValue) {
  switch (name) {
    case 'value':
      
      break;
  }
}
複製程式碼

方法observedAttributes聽過返回值宣告需要觀察的屬性,這樣就可以在指定屬性清單發生更新時通過另一個生命週期方法attributeChangedCallback,通知程式碼變化的情況。做響應的同步處理。整合後的程式碼如下:

var template = `
	<button inc>+</button><span>1</span><button dec>-</button>
	<style>
		span{color:red;}
		*{font-size:2rem;}
	</style>
`
class SpinButton extends HTMLElement{
	constructor(){
		super()
		var shadow = this.attachShadow({mode:'open'})
		var t = document.createElement('template')
		t.innerHTML = template
		shadow.appendChild(t.content.cloneNode(true))
		var b1 = shadow.querySelector('[inc]')
		var b2 = shadow.querySelector('[dec]')
		this.s = shadow.querySelector('span')
		var i = 1
		var that = this
		b1.onclick = function(){
			that.s.innerHTML = ++that.value 
		}
		b2.onclick = function(){
			that.s.innerHTML = -- that.value 
		}
	}
	static get observedAttributes() {
	  return ['value'];
	}
	attributeChangedCallback(name, oldValue, newValue) {
	  switch (name) {
	    case 'value':
	      this.s.innerHTML = newValue
	      break;
	  }
	}
	get value(){
		return this.getAttribute('value') || 1
	}
	set value(v){
		this.setAttribute('value',v)
	}
}
customElements.define('spin-button',SpinButton)
複製程式碼

插槽

元件給使用者使用的時候,一般會執行使用者傳遞特定的引數,以便讓元件更加符合自己的需求。

傳遞引數有幾種方法,一種是通過元素的屬性傳遞引數,一般的簡單值比如數字、日期和字串就可以此方式傳遞。另外就是允許傳遞HTML片段,這樣可以傳遞更加複雜的內容。這個方式使用的技術是有標準的,在Web Component標準內,被稱為是slot插槽,也就是大家常常說到得內容分發技術。

我們將會以Hello World為案例,講述傳參的方法。假設一個標籤<greeting-hello>,屬性傳參允許指定hello的物件,像是這樣:

<greeting-hello who="world">
<greeting-hello who="Reco">
複製程式碼

Slot插槽傳參可以傳遞複雜的HTML片段,像是這樣:

<greeting-hello>
	<b slot="who">Reco</b>
</greeting-hello>
複製程式碼

通過對任何一個元素標記屬性slot,即可指定需要插入的HTML片段和它的名字(這裡的片段名字叫做who),然後可以在定製元素內通過屬性傳遞引數已經談過了,這裡僅僅針對插槽傳遞`<slot>引用此片段:

<slot name="who"></slot>
複製程式碼

有了插槽技術,就無需自己編寫程式碼,方便的引入本來在使用元件的頁面內的HTML片段。具體做法隨後描述。

和建立一個普通的定製元素並沒什麼區別,還是一樣的如此:

<script type="module">
var template = `<h3>Hello,<slot name='who'/></h3>`
class GreetingHello extends HTMLElement{
	constructor(){
		super()
		var shadow = this.attachShadow({mode:'open'})
		var t = document.createElement('template')
		t.innerHTML = template
		shadow.appendChild(t.content.cloneNode(true))
	}
}
customElements.define('greeting-hello',GreetingHello)
</script>
<greeting-hello><i slot="who">Reco</i></greeting-hello>
複製程式碼

分發之後的效果等於是這樣的:

,<h3>Hello,<i>Reco</i></h3>
複製程式碼

你會發現,在我們自己的程式碼中,沒有任何處理slot標籤的任何程式碼。Web Components內部已經為我們實現了自動的內容分發。讓傳遞HTML片段到元件內變成非常方便的事情。

狀態

Web Components的關鍵構成技術包括Custom Element和Shadow DOM,最早在Chrome實現,第一個版本被稱為V0但是其他瀏覽器沒有跟進,因此逐步被廢棄。本文討論的是V1版本。Firefox也已經實現了V1版本。 可以在網站Whatcaniuse查詢當前支援狀態。

ref

  1. Posts of wb alligator.io/web-compone…
  2. Custom Elements v1: Reusable Web Components developers.google.com/web/fundame… *3. web-components-examples github.com/mdn/web-com…
  3. Firefox 63 – Tricks and Treats! hacks.mozilla.org/2018/10/fir…
  4. HTML Web Component using Plain JavaScript www.codementor.io/ayushgupta/… 6. Doing something with Web Components medium.com/@dalaidunc/…

相關文章