WebComponent魔法堂:深究Custom Element 之 標(biāo)準(zhǔn)構(gòu)建

前言

?通過《WebComponent魔法堂:深究Custom Element 之 面向痛點(diǎn)編程》,我們明白到其實(shí)Custom Element并不是什么新東西,我們甚至可以在IE5.5上定義自己的alert元素。但這種簡單粗暴的自定義元素并不是我們需要的,我們需要的是具有以下特點(diǎn)的自定義元素:

  1. 自定義元素可通過原有的方式實(shí)例化(<custom-element></custom-element>,new CustomElement()document.createElement('CUSTOM-ELEMENT'))
  2. 可通過原有的方法操作自定義元素實(shí)例(如document.body.appendChild,可被CSS樣式所修飾等)
  3. 能監(jiān)聽元素的生命周期
    ?而Google為首提出的H5 Custom Element讓我們可以在原有標(biāo)準(zhǔn)元素的基礎(chǔ)上向?yàn)g覽器注入各種抽象層次更高的自定義元素,并且在元素CRUD操作上與原生API無縫對(duì)接,編程體驗(yàn)更平滑。下面我們一起來通過H5 Custom Element來重新定義alert元素吧!

命名這件“小事”

?在正式擼代碼前我想讓各位最頭痛的事應(yīng)該就是如何命名元素了,下面3個(gè)因素將影響我們的命名:

  1. 命名沖突。自定義組件如同各種第三方類庫一樣存在命名沖突的問題,那么很自然地會(huì)想到引入命名空間來解決,但由于組件的名稱并不涉及組件資源加載的問題,因此我們這里簡化一下——為元素命名添加前綴即可,譬如采用很JAVA的com-cnblogs-fsjohnhuang-alert。
  2. 語義化。語義化我們理解就是元素名稱達(dá)到望文生義的境界,譬如x-alert一看上去就是知道x是前綴而已跟元素的功能無關(guān),alert才是元素的功能。
  3. 足夠的吊:)高大上的名稱總讓人賞心悅目,就像我們項(xiàng)目組之前開玩笑說要把預(yù)警系統(tǒng)改名為"超級(jí)無敵全球定位來料品質(zhì)不間斷跟蹤預(yù)警綜合平臺(tái)",呵呵!
    ?除了上述3點(diǎn)外,H5規(guī)范中還有這條規(guī)定:自定義元素必須至少包含一個(gè)連字符,即最簡形式也要這樣a-b。而不帶連字符的名稱均留作瀏覽器原生元素使用。換個(gè)說法就是名稱帶連字符的元素被識(shí)別為有效的自定義元素,而不帶連字符的元素要么被識(shí)別為原生元素,要么被識(shí)別為無效元素。
const compose = (...fns) => {
  const lastFn = fns.pop()
  fns = fns.reverse()
  return a => fns.reduce((p, fn) => fn(p), lastFn(a))
}
const info = msg => console.log(msg)
const type = o => Object.prototype.toString.call(o)
const printType = compose(info, type)

const newElem = tag => document.createElement(tag)

// 創(chuàng)建有效的自定義元素
const xAlert = newElem('x-alert')
infoType(xAlert) // [object HTMLElement]

// 創(chuàng)建無效的自定義元素
const alert = newElem('alert')
infoType(alert) // [object HTMLUnknownElement]

// 創(chuàng)建有效的原生元素
const div = newElem('div')
infoType(div) // [object HTMLDivElement]

?那如果我偏要用alert來自定義元素呢?瀏覽器自當(dāng)會(huì)說一句“悟空,你又調(diào)皮了”

?現(xiàn)在我們已經(jīng)通過命名規(guī)范來有效區(qū)分自定義元素和原生元素,并且通過前綴解決了命名沖突問題。嘿稍等,添加前綴真的是解決命名沖突的好方法嗎?這其實(shí)跟通過添加前綴解決id沖突一樣,假如有兩個(gè)元素發(fā)生命名沖突時(shí),我們就再把前綴加長直至不再?zèng)_突為止,那就有可能出現(xiàn)很JAVA的com-cnblogs-fsjohnhuang-alert的命名,噪音明顯有點(diǎn)多,直接降低語義化的程度,重點(diǎn)還有每次引用該元素時(shí)都要敲這么多字符,打字的累看的也累。這一切的根源就是有且僅有一個(gè)Scope——Global Scope,因此像解決命名沖突的附加信息則無法通過上下文來隱式的提供,直接導(dǎo)致需要通過前綴的方式來硬加上去。
?前綴的方式我算是認(rèn)了,但能不能少打?qū)懽帜??像命名空間那樣
木有命名沖突時(shí)

#!usr/bin/env python
# -*- coding: utf-8 -*-
from django.http import HttpResponse

def index(request):
  return HttpResponse('Hello World!')

存在命名沖突時(shí)

#!usr/bin/env python
# -*- coding: utf-8 -*-
import django.db.models
import peewee

type(django.db.models.CharField)
type(peewee.CharField)

前綴也能有選擇的省略就好了!

把玩Custome Element v0

?對(duì)元素命名吐嘈一地后,是時(shí)候把玩API了。

從頭到腳定義新元素

/** x-alert元素定義 **/
const xAlertProto = Object.create(HTMLElement.prototype, {
  /* 元素生命周期的事件 */
  // 實(shí)例化時(shí)觸發(fā)
  createdCallback: {
    value: function(){
      console.log('invoked createCallback!')

      const raw = this.innerHTML
      this.innerHTML = `<div class="alert alert-warning alert-dismissible fade in">
                          <button type="button" class="close" aria-label="Close">
                            <span aria-hidden="true">&times;</span>
                          </button>
                          <div class="content">${raw}</div>
                        </div>`
      this.querySelector('button.close').addEventListener('click', _ => this.close())
    }
  },
  // 元素添加到DOM樹時(shí)觸發(fā)
  attachedCallback: {
    value: function(){
      console.log('invoked attachedCallback!')
    }
  },
  // 元素DOM樹上移除時(shí)觸發(fā)
  detachedCallback: {
    value: function(){
      console.log('invoked detachedCallback!')
    }
  },
  // 元素的attribute發(fā)生變化時(shí)觸發(fā)
  attributeChangedCallback: {
    value: function(attrName, oldVal, newVal){
      console.log(`attributeChangedCallback-change ${attrName} from ${oldVal} to ${newVal}`)
    }
  },
  /* 定義元素的公有方法和屬性 */
  // 重寫textContent屬性
  textContent: {
    get: function(){ return this.querySelector('.content').textContent },
    set: function(val){ this.querySelector('.content').textContent = val }
  },
  close: {
    value: function(){ this.style.display = 'none' }
  },
  show: {
    value: function(){ this.style.display = 'block' }
  }
}) 
// 向?yàn)g覽器注冊自定義元素
const XAlert = document.registerElement('x-alert', { prototype: xAlertProto })

/** 操作 **/
// 實(shí)例化
const xAlert1 = new XAlert() // invoked createCallback!
const xAlert2 = document.createElement('x-alert') // invoked createCallback!
// 添加到DOM樹
document.body.appendChild(xAlert1) // invoked attachedCallback!
// 從DOM樹中移除
xAlert1.remove() // invoked detachedCallback!
// 僅作為DIV的子元素,而不是DOM樹成員不會(huì)觸發(fā)attachedCallback和detachedCallback函數(shù)
const d = document.createElement('div')
d.appendChild(xAlert1)
xAlert1.remove()
// 訪問元素實(shí)例方法和屬性
xAlert1.textContent = 1
console.log(xAlert1.textContent) // 1
xAlert1.close()
// 修改元素實(shí)例特性
xAlert1.setAttribute('d', 1) // attributeChangedCallback-change d from null to 1
xAlert1.removeAttribute('d') // attributeChangedCallback-change d from 1 to null 
// setAttributeNode和removeAttributeNode方法也會(huì)觸發(fā)attributeChangedCallback

?上面通過定義x-alert元素展現(xiàn)了Custom Element的所有API,其實(shí)就是繼承HTMLElement接口,然后選擇性地實(shí)現(xiàn)4個(gè)生命周期回調(diào)方法,而在createdCallback中書寫自定義元素內(nèi)容展開的邏輯。另外可以定義元素公開屬性和方法。最后通過document.registerElement方法告知瀏覽器我們定義了全新的元素,你要好好對(duì)它哦!
?那現(xiàn)在的問題在于假如<x-alert></x-alert>這個(gè)HTML Markup出現(xiàn)在document.registerElement調(diào)用之前,那會(huì)出現(xiàn)什么情況呢?這時(shí)的x-alert元素處于unresolved狀態(tài),并且可以通過CSS Selector :unresolved來捕獲,當(dāng)執(zhí)行document.registerElement后,x-alert元素則處于resolved狀態(tài)。于是可針對(duì)兩種狀態(tài)作樣式調(diào)整,告知用戶處于unresolved狀態(tài)的元素暫不可用,敬請期待。

<style>
  x-alert{
    display: block;
  }
  x-alert:unresolved{
    content: 'LOADING...';
  }
</style>

漸進(jìn)增強(qiáng)原生元素

?有時(shí)候我們只是想在現(xiàn)有元素的基礎(chǔ)上作些功能增強(qiáng),倘若又要從頭做起那也太折騰了,幸好Custom Element規(guī)范早已為我們想好了。下面我們來對(duì)input元素作增強(qiáng)

const xInputProto = Object.create(HTMLInputElement.prototype, {
  createdCallback: {
    value: function(){ this.value = 'x-input' }
  },
  isEmail: {
    value: function(){
      const val = this.value
      return /[0-9a-zA-Z]+@\S+\.\S+/.test(val)
    }
  }
})
document.registerElement('x-input', {
  prototype: xInputProto,
  extends: 'input'
})

// 操作
const xInput1 = document.createElement('input', 'x-input') // <input is="x-input">
console.log(xInput1.value) // x-input
console.log(xInput1.isEmail()) // false

Custom Element v1 —— 換個(gè)裝而已啦

?Custom Element API現(xiàn)在已經(jīng)升級(jí)到v1版本了,其實(shí)就是提供一個(gè)專門的window.customElements作為入口來統(tǒng)一管理和操作自定義元素,并且以對(duì)ES6 class更友善的方式定義元素,其中的步驟和概念并沒有什么變化。下面我們采用Custom Element v1的API重寫上面兩個(gè)示例

  1. 從頭定義
class XAlert extends HTMLElement{
  // 相當(dāng)于v0中的createdCallback,但要注意的是v0中的createdCallback僅元素處于resolved狀態(tài)時(shí)才觸發(fā),而v1中的constructor就是即使元素處于undefined狀態(tài)也會(huì)觸發(fā),因此盡量將操作延遲到connectedCallback里執(zhí)行
  constructor(){
    super() // 必須調(diào)用父類的構(gòu)造函數(shù)

    const raw = this.innerHTML
    this.innerHTML = `<div class="alert alert-warning alert-dismissible fade in">
                        <button type="button" class="close" aria-label="Close">
                          <span aria-hidden="true">&times;</span>
                        </button>
                        <div class="content">${raw}</div>
                      </div>`
    this.querySelector('button.close').addEventListener('click', _ => this.close())
  }
  // 相當(dāng)于v0中的attachedCallback
  connectedCallback(){
    console.log('invoked connectedCallback!')
  }
  // 相當(dāng)于v0中的detachedCallback
  disconnectedCallback(){
    console.log('invoked disconnectedCallback!')
  }
  // 相當(dāng)于v0中的attributeChangedCallback,但新增一個(gè)可選的observedAttributes屬性來約束所監(jiān)聽的屬性數(shù)目
  attributeChangedCallback(attrName, oldVal, newVal){
    console.log(`attributeChangedCallback-change ${attrName} from ${oldVal} to ${newVal}`)
  }
  // 缺省時(shí)表示attributeChangedCallback將監(jiān)聽所有屬性變化,若返回?cái)?shù)組則僅監(jiān)聽數(shù)組中的屬性變化
  static get observedAttributes(){ return ['disabled'] }
  // 新增事件回調(diào),就是通過document.adoptNode方法修改元素ownerDocument屬性時(shí)觸發(fā)
  adoptedCallback(){
    console.log('invoked adoptedCallback!')
  }
  get textContent(){
    return this.querySelector('.content').textContent
  }
  set textContent(val){
    this.querySelector('.content').textContent = val
  }
  close(){
    this.style.display = 'none'
  }
  show(){
    this.style.display = 'block'
  }
}
customElements.define('x-alert', XAlert)
  1. 漸進(jìn)增強(qiáng)
class XInput extends HTMLInputElement{
  constructor(){
    super()

    this.value = 'x-input'
  }
  isEmail(){
    const val = this.value
    return /[0-9a-zA-Z]+@\S+\.\S+/.test(val)
  }
}
customElements.define('x-input', XInput, {extends: 'input'})

// 實(shí)例化方式
document.createElement('input', {is: 'x-input'})
new XInput()
<input is="x-input">

?除此之外之前的unresolved狀態(tài)改成defined和undefined狀態(tài),CSS對(duì)應(yīng)的選擇器為:defined:not(:defined)
?還有就是新增一個(gè)customeElements.whenDefined({String} tagName):Promise方法,讓我們能監(jiān)聽自定義元素從undefined轉(zhuǎn)換為defined的事件。

<share-buttons>
  <social-button type="twitter"><a href="...">Twitter</a></social-button>
  <social-button type="fb"><a href="...">Facebook</a></social-button>
  <social-button type="plus"><a href="...">G+</a></social-button>
</share-buttons>

// Fetch all the children of <share-buttons> that are not defined yet.
let undefinedButtons = buttons.querySelectorAll(':not(:defined)');

let promises = [...undefinedButtons].map(socialButton => {
  return customElements.whenDefined(socialButton.localName);
));

// Wait for all the social-buttons to be upgraded.
Promise.all(promises).then(() => {
  // All social-button children are ready.
});

從頭定義一個(gè)剛好可用的元素不容易啊!

?到這里我想大家已經(jīng)對(duì)Custom Element API有所認(rèn)識(shí)了,下面我們嘗試自定義一個(gè)完整的元素吧。不過再實(shí)操前,我們先看看一個(gè)剛好可用的元素應(yīng)該注意哪些細(xì)節(jié)。

明確各階段適合的操作

1.constructor
?用于初始化元素的狀態(tài)和設(shè)置事件監(jiān)聽,或者創(chuàng)建Shadow Dom。
2.connectedCallback
?資源獲取和元素渲染等操作適合在這里執(zhí)行,但該方法可被調(diào)用多次,因此對(duì)于只執(zhí)行一次的操作要自帶檢測方案。
3.disconnectedCallback
?適合作資源清理等工作(如移除事件監(jiān)聽)

更細(xì)的細(xì)節(jié)

1.constructor中的細(xì)節(jié)
1.1. 第一句必須調(diào)用super()保證父類實(shí)例創(chuàng)建
1.2. return語句要么沒有,要么就只能是returnreturn this
1.3. 不能調(diào)用document.writedocument.open方法
1.4. 不要訪問元素的特性(attribute)和子元素,因?yàn)樵乜赡芴幱趗ndefined狀態(tài)并沒有特性和子元素可訪問
1.5. 不要設(shè)置元素的特性和子元素,因?yàn)榧词乖靥幱赿efined狀態(tài),通過document.createElementnew方式創(chuàng)建元素實(shí)例時(shí),本應(yīng)該是沒有特性和子元素的
2.打造focusable元素 by tabindex特性
?默認(rèn)情況下自定義元素是無法獲取焦點(diǎn)的,因此需要顯式添加tabindex特性來讓其focusable。另外還要注意的是若元素disabledtrue時(shí),必須移除tabindex讓元素unfocusable。
3.ARIA特性
?通過ARIA特性讓其他閱讀器等其他訪問工具可以識(shí)別我們的自定義元素。
4.事件類型轉(zhuǎn)換
?通過addEventListener捕獲事件,然后通過dispathEvent發(fā)起事件來對(duì)事件類型進(jìn)行轉(zhuǎn)換,從而觸發(fā)更符合元素特征的事件類型。

下面我們來擼個(gè)x-btn

class XBtn extends HTMLElement{
  static get observedAttributes(){ return ['disabled'] }
  constructor(){
    super()

    this.addEventListener('keydown', e => {
      if (!~[13, 32].indexOf(e.keyCode)) return  

      this.dispatchEvent(new MouseEvent('click', {
        cancelable: true,
        bubbles: true
      }))
    })

    this.addEventListener('click', e => {
      if (this.disabled){
        e.stopPropagation()
        e.preventDefault()
      }
    })
  }
  connectedCallback(){
    this.setAttribute('tabindex', 0)
    this.setAttribute('role', 'button')
  }
  get disabled(){
    return this.hasAttribute('disabled')
  }
  set disabled(val){
    if (val){
      this.setAttribute('disabled','')
    }
    else{
      this.removeAttribute('disabled')
    }
  }
  attributeChangedCallback(attrName, oldVal, newVal){
    this.setAttribute('aria-disabled', !!this.disabled)
    if (this.disabled){
      this.removeAttribute('tabindex')
    }
    else{
      this.setAttribute('tabindex', '0')
    }
  }
}
customElements.define('x-btn', XBtn)

如何開始使用Custom Element v1?

?Chrome54默認(rèn)支持Custom Element v1,Chrome53則須要修改啟動(dòng)參數(shù)chrome --enable-blink-features=CustomElementsV1。其他瀏覽器可使用webcomponets.js這個(gè)polyfill。

題外話一番

?關(guān)于Custom Element我們就說到這里吧,不過我在此提一個(gè)有點(diǎn)怪但又確實(shí)應(yīng)該被注意到的細(xì)節(jié)問題,那就是自定義元素是不是一定要采用<x-alert></x-alert>來聲明呢?能否采用<x-alert/><x-alert>的方式呢?
?答案是不行的,由于自定義元素屬于Normal Element,因此必須采用<x-alert></x-alert>這種開始標(biāo)簽和閉合標(biāo)簽來聲明。那么什么是Normal Element呢?
其實(shí)元素分為以下5類:

  1. Void elements
    ?格式為<tag-name>,包含以下元素area,base,br,col,embed,hr,img,keygen,link,meta,param,source,track,wbr
  2. Raw text elements
    ?格式為<tag-name></tag-name>,包含以下元素script,style
  3. escapable raw text elements
    ?格式為<tag-name></tag-name>,包含以下元素textarea,title
  4. Foreign elements
    ?格式為<tag-name/>,MathML和SVG命名空間下的元素
  5. Normal elements
    ?格式為<tag-name></tag-name>,除上述4種元素外的其他元素。某些條件下可以省略結(jié)束標(biāo)簽,因?yàn)闉g覽器會(huì)自動(dòng)為我們補(bǔ)全,但結(jié)果往往會(huì)很吊軌,所以還是自己寫完整比較安全。

總結(jié)

?當(dāng)頭一回聽到Custom Element時(shí)我是那么的興奮不已,猶如找到根救命稻草似的。但如同其他新技術(shù)的出現(xiàn)一樣,利弊同行,如何判斷和擇優(yōu)利用是讓人頭痛的事情,也許前人的經(jīng)驗(yàn)?zāi)芙o我指明方向吧!下篇《WebComponent魔法堂:深究Custom Element 之 從過去看現(xiàn)在》,我們將穿越回18年前看看先驅(qū)HTML Component的黑歷史,然后再次審視WebComponent吧!
?尊重原創(chuàng),轉(zhuǎn)載請注明來自:http://www.cnblogs.com/fsjohnhuang/p/5938790.html _肥仔John

感謝

How to Create Custom HTML Elements
A vocabulary and associated APIs for HTML and XHTML
Custom Elements v1
custom-elements-customized-builtin-example

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,564評(píng)論 19 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,046評(píng)論 25 709
  • 在聽這首歌,突然聽歌詞里有 "china bule "這個(gè)詞,以前都沒注意過。 作為中國人,聽到China就敏感。...
    WoodSage閱讀 651評(píng)論 0 0
  • 爸爸 爸爸 已經(jīng)好久未這樣稱呼您啦 只喊:老爸 老爸 忽然 好想近在咫尺的爸爸 在每個(gè)清晨黃昏里 在每頓早餐晚飯后...
    塵光閱讀 576評(píng)論 0 2
  • 又是一年圣誕節(jié),朋友圈也猶如一場沒有硝煙的戰(zhàn)爭:有人曬著其樂融融的美照,洋溢著幸福的笑臉。然而,還有一些人卻唱著反...
    韌青閱讀 461評(píng)論 2 0

友情鏈接更多精彩內(nèi)容