WebComponent魔法堂:深究Custom Element 之 面向痛點編程

前言

 最近加入到新項目組負責前端技術預研和選型,一直偏向於以Polymer爲表明的WebComponent技術線,因而查閱各種資料想說服老大向這方面靠,最後獲得的結果是:"資料99%是英語無所謂,最重要是UI/UX上符合要求,技術的事你說了算。",因而我只好乖乖地去學UI/UX設計的事,木有設計師撐腰的前端是苦逼的:(嘈吐一地後,仍是擠點時間總結一下WebComponent的內容吧,爲之後做培訓材料做點準備。html

浮在水面上的痛

組件噪音太多了!

 在使用Bootstrap的Modal組件時,咱們難免要Ctrl+c而後Ctrl+v下面一堆代碼前端

<div class="modal fade" tabindex="-1" role="dialog">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
        <h4 class="modal-title">Modal title</h4>
      </div>
      <div class="modal-body">
        <p>One fine body&hellip;</p>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
        <button type="button" class="btn btn-primary">Save changes</button>
      </div>
    </div><!-- /.modal-content -->
  </div><!-- /.modal-dialog -->
</div><!-- /.modal -->

modal
 一個不留神誤刪了一個結束標籤,或拼錯了某個class或屬性那就悲催了,此時一個語法高亮、提供語法檢查的編輯器是如此重要啊!可是我其實只想配置個Modal而已。
 因爲元素信息由標籤標識符,元素特性樹層級結構組成,因此排除噪音後提取的核心配置信息應該以下(YAML語法描述):html5

dialog:
  modal: true
  children:  
    header: 
      title: Modal title
      closable: true
    body:
      children:
        p:
          textContent: One fine body&hellip;
    footer
      children:
        button: 
          type: close
          textContent: Close
        button: 
          type: submit 
          textContent: Save changes

轉換成HTML就是node

<dialog modal>
  <dialog-header title="Modal title" closable></dialog-header>
  <dialog-body>
    <p>One fine body&hellip;</p>
  </dialog-body>
  <dialog-footer>
    <dialog-btn type="close">Close</dialog-btn>
    <dialog-btn type="submit">Save changes</dialog-btn>
  </dialog-footer>
</dialog>

而像Alert甚至能夠極致到這樣web

<alert>是否是很簡單啊?</alert>

惋惜瀏覽器木有提供<alert></alert>,那怎麼辦呢?瀏覽器

手打牛丸模式1

既然瀏覽器木有提供,那咱們本身手寫一個吧!app

<script>
'use strict'
class Alert{
  constructor(el = document.createElement('ALERT')){
    this.el = el
    const raw = el.innerHTML
    el.dataset.resolved = ''
    el.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>
                      ${raw}
                    </div>`
    el.querySelector('button.close').addEventListener('click', _ => this.close())
  }
  close(){
    this.el.style.display = 'none'
  }
  show(){
    this.el.style.display = 'block'
  }
}

function registerElement(tagName, ctorFactory){
  [...document.querySelectorAll(`${tagName}:not([data-resolved])`)].forEach(ctorFactory)
}
function registerElements(ctorFactories){
  for(let k in ctorFactories){
    registerElement(k, ctorFactories[k])
  }
}

清爽一下!編輯器

<alert>舒爽多了!</alert>
<script>
registerElements({alert: el => new Alert(el)})
</script>

覆盤找問題

 雖然表面上實現了需求,但存在2個明顯的缺陷this

  1. 不完整的元素實例化方式
    原生元素有2種實例化方式google

  2. 聲明式

<!-- 由瀏覽器自動完成 元素實例化 和 添加到DOM樹 兩個步驟 -->
<input type="text">
  1. 命令式

// 元素實例化
const input = new HTMLInputElement() // 或者 document.createElement('INPUT')
input.type = 'text'
// 添加到DOM樹
document.querySelector('#mount-node').appendChild(input)

 因爲聲明式注重What to do,而命令式注重How to do,而且咱們操做的是DOM,因此採用聲明式的HTML標籤比命令式的JavaScript會來得簡潔平滑。但當咱們須要動態實例化元素時,命令式則是最佳的選擇。因而咱們勉強能夠這樣

// 元素實例化
const myAlert = new Alert()
// 添加到DOM樹
document.querySelector('#mount-node').appendChild(myAlert.el)
/*
因爲Alert沒法正常實現HTMLElement和Node接口,所以沒法實現
document.querySelector('#mount-node').appendChild(myAlert)
myAlert和myAlert.el的差異在於前者的myAlert是元素自己,然後者則是元素句柄,其實沒有明確哪一種更好,只是原生方法都是支持操做元素自己,一下來個不一致的句柄不蒙纔怪了
*/

 即便你能忍受上述的代碼,那經過innerHTML實現半聲明式的動態元素實例化,那又怎麼玩呢?是再手動調用一下registerElement('alert', el => new Alert(el))嗎?
 更別想經過document.createElement來建立自定義元素了。

  1. 有生命無週期
     元素的生命從實例化那刻開始,而後經歷如添加到DOM樹、從DOM樹移除等階段,而想要更全面有效地管理元素的話,那麼捕獲各階段並完成相應的處理則是惟一有效的途徑了。

生命週期很重要

 當定義一個新元素時,有3件事件是必須考慮的:

  1. 元素自閉合: 元素自身信息的自包含,而且不受外部上下文環境的影響;

  2. 元素的生命週期: 經過監控元素的生命週期,從而實現不一樣階段完成不一樣任務的目錄;

  3. 元素間的數據交換: 採用property in, event out的方式與外部上下文環境通訊,從而與其餘元素進行通訊。
     元素自閉合貌似無望了,下面咱們試試監聽元素的生命週期吧!

手打牛丸模式2

 經過constructor咱們能監聽元素的建立階段,但後續的各個階段呢?可幸的是能夠經過MutationObserver監聽document.body來實現:)
最終獲得的以下版本:

'use strict'
class Alert{
  constructor(el = document.createElement('ALERT')){
    this.el = el
    this.el.fireConnected = () => { this.connectedCallback && this.connectedCallback() }
    this.el.fireDisconnected = () => { this.disconnectedCallback && this.disconnectedCallback() }
    this.el.fireAttributeChanged = (attrName, oldVal, newVal) => { this.attributeChangedCallback && this.attributeChangedCallback(attrName, oldVal, newVal) } 

    const raw = el.innerHTML
    el.dataset.resolved = ''
    el.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>
                      ${raw}
                    </div>`
    el.querySelector('button.close').addEventListener('click', _ => this.close())
  }
  close(){
    this.el.style.display = 'none'
  }
  show(){
    this.el.style.display = 'block'
  }
  connectedCallback(){
    console.log('connectedCallback')
  }
  disconnectedCallback(){
    console.log('disconnectedCallback')
  }
  attributeChangedCallback(attrName, oldVal, newVal){
    console.log('attributeChangedCallback')
  }
}

function registerElement(tagName, ctorFactory){
  [...document.querySelectorAll(`${tagName}:not([data-resolved])`)].forEach(ctorFactory)
}
function registerElements(ctorFactories){
  for(let k in ctorFactories){
    registerElement(k, ctorFactories[k])
  }
}

const observer = new MutationObserver(records => {
  records.forEach(record => {
    if (record.addedNodes.length && record.target.hasAttribute('data-resolved')){
      // connected
      record.target.fireConnected()
    }
    else if (record.removedNodes.length){
      // disconnected
      const node = [...record.removedNodes].find(node => node.hasAttribute('data-resolved'))
      node && node.fireDisconnected()
    }
    else if ('attributes' === record.type && record.target.hasAttribute('data-resolved')){
      // attribute changed
      record.target.fireAttributeChanged(record.attributeName, record.oldValue, record.target.getAttribute(record.attributeName))
    }
  })
})
observer.observe(document.body, {attributes: true, childList: true, subtree: true})

registerElement('alert', el => new Alert(el))

總結

 千辛萬苦擼了個基本不可用的自定義元素模式,但經過代碼咱們進一步瞭解到對於自定義元素咱們須要如下基本特性:

  1. 自定義元素可經過原有的方式實例化(<custom-element></custom-element>,new CustomElement()document.createElement('CUSTOM-ELEMENT'))

  2. 可經過原有的方法操做自定義元素實例(如document.body.appendChild等)

  3. 能監聽元素的生命週期
    下一篇《WebComponent魔法堂:深究Custom Element 之 標準構建》中,咱們將一同探究H5標準中Custom Element API,並利用它來實現知足上述特性的自定義元素:)

 尊重原創,轉載請註明來自: http://www.cnblogs.com/fsjohn... ^_^肥仔John

感謝

Custom ELement
Custom ELement v1
MutationObserver

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息