自定義元素探祕及構建可複用組件最佳實踐

原文請查閱 這裏,略有刪減,本文采用 知識共享署名 4.0 國際許可協議共享,BY Troland

這是 JavaScript 工做原理第十九章。javascript

概述

前述文章中,咱們介紹了 Shadow DOM 接口和一些其它概念,而這些都是網頁組件的組成部分。網頁組件背後的思想即經過建立顆粒化,模塊化和可複用的元素來擴展 HTML 內置功能。這是一個已經被全部主流瀏覽器兼容的相對嶄新的 W3C 標準且能夠被用在生產環境之中,雖然不兼容的瀏覽器須要使用墊片庫(將在隨後的章節中進行討論)。html

正如開發者所知,瀏覽器爲構建網站和網頁程序提供了一些重要的開發工具。咱們所說的 HTML,CSS 和 JavaScript 即開發者使用 HTML 來構建結構,CSS 進行樣式化而後使用 JavaScript 來讓頁面動起來。然而,在網頁組件出現以前,把 JavaScript 腳本和 HTML 結構組合起來並不是易事。html5

本文將闡述網頁組件的基石-自定義元素。總之,開發者可使用自定義元素接口來建立包含 JavaScript 邏輯和樣式的自定義元素(正如名稱的字面意思)。許多開發者會把自定義元素和 shadow DOM 混爲一談。可是,他們是徹底不一樣的概念且它們互補而不是能夠相互替代的。java

一些框架(好比 Angular,React) 試圖經過引進其自有概念來解決一樣的問題。開發者能夠把自定義元素和 Angular 的指令或者 React 組件進行對比。然而,自定義元素是瀏覽器原生的且只須要原生 JavaScript,HTML 和 CSS。固然了,這並不意味着它能夠取代一個典型的 JavaScript 框架。現代框架不只僅爲開發者提供模仿自定義元素行爲的能力。所以,能夠同時使用框架和自定義元素。git

接口

在深刻了解以前,讓咱們先大概快速瀏覽一下接口的內容。全局 customElements 對象爲開發者提供了一些方法:github

  • define(tagName, constructor, options) -建立一個新的自定義元素。

    包含三個參數:自定義元素的可用標籤名稱,自定義元素類定義及選項參數對象。目前僅支持一個選項參數:extends 指定想要擴展的 HTML 內置元素名稱的字符串。用來建立定製化內置元素。web

  • get(tagName) -若元素已經定義則返回自定義元素的構造函數不然返回 undefined。只有一個參數:自定義元素的可用標籤名稱。
  • whenDefined(tagName)-返回一個 promise 對象,當定義自定義元素即解析。若元素已定義則當即進行解析。若自定義元素標籤名稱不可用則摒棄 promise。只有一個參數:自定義元素的可用標籤名稱。

如何建立自定義元素

建立自定義元素實際上就是小菜一碟。開發者只須要作兩件事:建立擴展 HTMLElement 類元素的類定義,而後以合適的名稱註冊元素。promise

class MyCustomElement extends HTMLElement {
  constructor() {
    super();
    // …
  }

  // …
}

customElements.define('my-custom-element', MyCustomElement);

或者如你所願,可使用匿名類以防止弄亂當前做用域瀏覽器

customElements.define('my-custom-element', class extends HTMLElement {
  constructor() {
    super();
    // …
  }

  // …
});

從以上例子可見,使用 customElements.define(...) 方法註冊自定義元素。session

自定義元素所解決的問題

實際上,問題是啥?嵌套 DIV 是問題之一。嵌套 Div 是啥?在現代網頁程序中這是一個很是常見的現象,開發者會使用多個嵌套塊狀元素(div 互相嵌套之類)。

<div class="top-container">
  <div class="middle-container">
    <div class="inside-container">
      <div class="inside-inside-container">
        <div class="are-we-really-doing-this">
          <div class="mariana-trench">
            …
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

由於瀏覽器能夠在頁面上正常進行渲染,因此使用了這樣的嵌套結構。可是,這會使得 HTML 不具可讀性且難以維護。

所以,例如假設有以下組件:

那麼傳統 HTML 結構相似以下:

<div class="primary-toolbar toolbar">
  <div class="toolbar">
    <div class="toolbar-button">
      <div class="toolbar-button-outer-box">
        <div class="toolbar-button-inner-box">
          <div class="icon">
            <div class="icon-undo">&nbsp;</div>
          </div>
        </div>
      </div>
    </div>
    <div class="toolbar-button">
      <div class="toolbar-button-outer-box">
        <div class="toolbar-button-inner-box">
          <div class="icon">
            <div class="icon-redo">&nbsp;</div>
          </div>
        </div>
      </div>
    </div>
    <div class="toolbar-button">
      <div class="toolbar-button-outer-box">
        <div class="toolbar-button-inner-box">
          <div class="icon">
            <div class="icon-print">&nbsp;</div>
          </div>
        </div>
      </div>
    </div>
    <div class="toolbar-toggle-button toolbar-button">
      <div class="toolbar-button-outer-box">
        <div class="toolbar-button-inner-box">
          <div class="icon">
            <div class="icon-paint-format">&nbsp;</div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

但想象下若是可使用相似以下代碼:

<primary-toolbar>
  <toolbar-group>
    <toolbar-button class="icon-undo"></toolbar-button>
    <toolbar-button class="icon-redo"></toolbar-button>
    <toolbar-button class="icon-print"></toolbar-button>
    <toolbar-toggle-button class="icon-paint-format"></toolbar-toggle-button>
  </toolbar-group>
</primary-toolbar>

要我說,第二個示例清爽多了。第二個示例更具可維護性,可讀性且對於瀏覽器和開發者更加合理。更加簡潔。

另外一個問題便可複用性。做爲開發者,不只僅要書寫可運行的代碼還得寫出可維護代碼。書寫可維護代碼即可以輕易地複用代碼片斷而不是重複地複製粘貼。

我將會給出一個簡單的示例而你就會明白。假設有以下元素:

<div class="my-custom-element">
  <input type="text" class="email" />
  <button class="submit"></button>
</div>

若須要在其它地方使用這段代碼,開發者須要再次書寫相同的 HTML 結構。如今,想象 一下須要稍微修改一下這些元素。開發者須要找出每一個代碼須要修改的地方,而後一遍遍地作出一樣的修改。太噁心了。。。

若使用以下碼豈不會更好?

<my-custom-element></my-custom-element>

現代網頁程序不只僅只有靜態 HTML。開發者須要作交互。這就須要 JavaScript。通常來講,開發者須要作的即建立一些元素而後在上面監聽事件以響應用戶輸入。點擊,拖拽或者懸浮事件等等。

var myDiv = document.querySelector('.my-custom-element');

myDiv.addEventListener('click', () => {
  myDiv.innerHTML = '<b> I have been clicked </b>';
});
<div class="my-custom-element">
  I have not been clicked yet.
</div>

使用自定義元素接口能夠把全部的邏輯封裝進元素自身。如下代碼能夠實現和上面代碼同樣的功能:

class MyCustomElement extends HTMLElement {
  constructor() {
    super();

    var self = this;

    self.addEventListener('click', () => {
      self.innerHTML = '<b> I have been clicked </b>';
    });
  }
}

customElements.define('my-custom-element', MyCustomElement);
<my-custom-element>
  I have not been clicked yet
</my-custom-element>

咋一看上去,自定義元素技術須要書寫更多的 JavaScript 代碼。可是在實際程序中,建立不需複用的單一組件的狀況是不多見的。一個典型的現代網頁程序的重要特徵即大多數元素都是動態建立的。那麼,開發者就須要分別處理使用 JavaScript 動態添加元素或者使用 HTML 結構中預約義內容。那麼可使用自定義元素來實現這些功能。

總之,自定義元素讓開發者的代碼更易理解和維護,並分割爲小型,可複用及可封裝的模塊。

要求

在建立自定義元素以前,開發者須要遵照以下特殊規則:

  • 名稱必須包含一個破折號 - 。這樣 HTML 解析器就能夠把自定義元素和內置元素區分開來。這樣能夠保證不會和內置元素出現命名衝突的問題(不論是如今或者未來當添加其它元素的時候)。好比,<my-custom-element> 是正確的而 myCustomElement<my_custom_element> 則否則。
  • 不容許重複註冊標籤名稱。重複註冊標籤名稱會致使瀏覽器拋出 DOMException 錯誤。不能夠覆蓋已註冊自定義元素。
  • 自定義元素不能夠自關閉。HTML 解析器只容許一小撮內置元素能夠自關閉(好比 <img><link><br>)。

功能

那麼究竟自定義元素能夠實現哪些功能?答案是不少。

最好用的功能之一即元素的類定義能夠引用 DOM 元素自身。這意味着開發者能夠直接使用 this 來直接監聽事件,訪問 DOM 屬性,訪問 DOM 元素子節點等等。

class MyCustomElement extends HTMLElement {
  // ...

  constructor() {
    super();

    this.addEventListener('mouseover', () => {
      console.log('I have been hovered');
    });
  }

  // ...
}

固然,這樣開發者就可使用新內容來覆蓋元素的子節點。但通常不推薦這樣作,由於這可能會致使意外的行爲。做爲自定義元素的使用者,由於不是使用者開發的,當元素裏面的標記被其它內容所取代,用戶會以爲很奇怪。

在元素生命週期的特定階段,開發者能夠在一些生命週期鉤子中執行代碼。

constructor

每當建立或者更新元素會觸發構造函數(隨後再詳細講解下)。通常狀況會在該階段初始化狀態,監聽事件,建立 shadow DOM 等等。須要記住的是必須老是在構造函數中調用 super()

connectedCallback

每當在 DOM 中添加元素的時候會調用 connectedCallback 方法。能夠用來(推薦)延遲執行某些代碼直到元素徹底渲染於頁面上時候調用(好比獲取一個資源)。

disconnectedCallback

connectedCallback 相反,當元素被從 DOM 刪除時調用 disconnectedCallback 方法。通常用於釋放資源的時候調用。須要注意的是若用戶關閉選項卡不會調用 disconnectedCallback 方法。所以,首先開發者須要注意初始化代碼。

attributeChangedCallback

每當添加,刪除,更新或者替換元素的某一屬性的時候調用。當解析器建立的時候也會調用。可是,請注意只有在 observedAttributes 屬性白名單中的屬性纔會觸發。

addoptedCallback

當使用 document.adoptNode(...) 來把元素移動到另外一個文檔的時候會觸發 addoptedCallback方法。

請注意以上全部的回調都是同步。例如,當把元素添加進 DOM 的時候只會觸發鏈接回調。

屬性反射

內置 HTML 元素提供了一個很是方便的功能:屬性反射。這意味着直接修改某些屬性值會直接反射到 DOM 的屬性中。例如 id 屬性:

myDiv.id = 'new-id';

將會更新 DOM 爲

<div id="new-id"> ... </div>

反之亦然。這是很是有用的由於這樣就使得開發者能夠聲明式書寫元素。

自定義元素自身沒有該功能,可是有辦法能夠實現。爲了在自定義元素中實現該相同的功能,開發者須要定義屬性的 getters 和 setters 方法。

class MyCustomElement extends HTMLElement {
  // ...

  get myProperty() {
    return this.hasAttribute('my-property');
  }

  set myProperty(newValue) {
    if (newValue) {
      this.setAttribute('my-property', newValue);
    } else {
      this.removeAttribute('my-property');
    }
  }

  // ...
}

擴展元素

開發者不只僅可使用自定義元素接口建立新的 HTML 元素還能夠用來擴展示有的 HTML 元素。並且該接口在內置元素和其它自定義元素中工做得很好。僅僅只須要擴展元素的類定義便可。

class MyAwesomeButton extends MyButton {
  // ...
}

customElements.define('my-awesome-button', MyAwesomeButton);

或者當擴展內置元素時,開發者須要爲 customElements.define(...) 函數添加第三個 extends 的參數,參數值爲須要擴展的元素標籤名稱。因爲許多內置元素共享相同的 DOM 接口,extends 參數會告訴瀏覽器須要擴展的目標元素。若沒有指定須要擴展的元素,瀏覽器將不會知道須要擴展的功能類別 。

class MyButton extends HTMLButtonElement {
  // ...
}

customElements.define('my-button', MyButton, {extends: 'button'});

一個可擴展原生元素也被稱爲可定製化內置元素。

開發者須要記住的經驗法則即老是擴展存在的 HTML 元素。而後,一點點往裏添加功能。這樣就能夠保留元素以前的功能(屬性,函數)。

請注意如今只有 Chrome 67+ 才支持定製化內置元素。之後,其它瀏覽器也會實現,可是 Safari 徹底沒有實現該功能。

更新元素

如上所述,可使用 customElements.define(...) 方法註冊自定義元素。但這並不意味着,開發者必須首先註冊元素。能夠推遲在以後某個時間註冊自定義元素。甚至能夠在往 DOM 中添加元素後再註冊元素也是能夠的。這一過程稱爲更新元素。開發者可使用 customElements.whenDefined(...) 方法獲取元素的定義時間。開發者傳入元素標籤名,返回一個 promise 對象,而後當元素註冊的時候解析。

customElements.whenDefined('my-custom-element').then(_ => {
  console.log('My custom element is defined');
});

例如,開發者也許想要延遲執行代碼直到定義元素內全部子元素。若內嵌自定義元素,這將會很是有用。

有時候,父元素有可能會依賴於其子元素的實現。在這種狀況下,開發者須要確保子元素在其父元素以前定義。

Shadow DOM

如前所述,須要把自定義元素和 shadow DOM 一塊兒使用。前者用來把 JavaScript 邏輯封裝進元素然後者用來爲一小段 DOM 建立一個不爲外部影響的隔絕環境。建議查看以前專門介紹 shadow DOM 的文章以便更好地理解 shadow DOM 概念。

只需調用 this.attachShadow 就能夠在自定義元素內使用 shadow DOM

class MyCustomElement extends HTMLElement {
  // ...

  constructor() {
    super();

    let shadowRoot = this.attachShadow({mode: 'open'});
    let elementContent = document.createElement('div');
    shadowRoot.appendChild(elementContent);
  }

  // ...
});

模板

咱們在以前的文章中簡單介紹了下模板,須要單獨一篇文章來專門介紹模板。這裏,咱們將會給出一個簡單的示例來介紹如何在自定義元素中使用模板。

經過聲明一個 DOM 片斷來使用 <template>,該標籤內容只會被解析而不會在頁面上渲染。

<template id="my-custom-element-template">
  <div class="my-custom-element">
    <input type="text" class="email" />
    <button class="submit"></button>
  </div>
</template>
let myCustomElementTemplate = document.querySelector('#my-custom-element-template');

class MyCustomElement extends HTMLElement {
  // ...

  constructor() {
    super();

    let shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.appendChild(myCustomElementTemplate.content.cloneNode(true));
  }

  // ...
});

那麼如今,咱們在自定義元素裏面使用了 shadow DOM 和 模板,建立了一個元素,該元素做用域和其它元素隔絕且把 HTML 結構和 JavaScript 邏輯完美地隔離開來。

樣式化

那麼,咱們講解了 HTML 和 JavaScript,如今還剩下 CSS。顯然,須要樣式化元素。開發者能夠在 shadow DOM 中添加樣式可是用戶如何從外部樣式化元素呢?答案很簡單-只須要和通常的內置元素同樣寫樣式便可。

my-custom-element {
  border-radius: 5px;
  width: 30%;
  height: 50%;
  // ...
}

請注意外部定義的樣式比元素內部定義的樣式優先級高,外部樣式會覆蓋掉元素內定義的樣式。

開發者須要明白有時候頁面渲染,而後會在某些時刻會發現無樣式內容閃爍(FOUC)。開發者能夠經過爲未定義組件定義樣式及當元素已定義的時候使用一些動畫過渡效果。使用 :defined 選擇器來達成這一效果。

my-button:not(:defined) {
  height: 20px;
  width: 50px;
  opacity: 0;
}

未知元素對比未定義自定義元素

HTML 規範很是靈活且容許開發者任意聲明標籤。若不被瀏覽器解析則會解析爲 HTMLUnknownElement

var element = document.createElement('thisElementIsUnknown');

if (element instanceof HTMLUnknownElement) {
  console.log('The selected element is unknown');
}

可是這並不適用於自定義元素。還記得討論定義自定義元素時候的特殊命名規則嗎?緣由是由於當瀏覽器發現一個自定義元素的名稱有效的時候,瀏覽器會把它解析爲 HTMLElement ,而後瀏覽器會把它看做一個未定義的自定義元素。

var element = document.createElement('this-element-is-undefined');

if (element instanceof HTMLElement) {
  console.log('The selected element is undefined but not unknown');
}

在視覺上, HTMLElement 和 HTMLUnknownElement 可能沒啥不一樣,可是須要注意其它地方。解析器會區別對待這兩種元素。具備有效自定義名稱的元素會被看做擁有自定義元素實現。在定義實現細節以前該自定義元素會被當作一個空 div 元素。而一個未定義元素沒有實現任何內置元素的任何方法或屬性。

瀏覽器兼容

custom elements 初版是在 Chrome 36+ 中引入的。被稱爲自定義元素接口 v0,雖然如今仍然可用,可是已經被棄用並被認爲是糟糕的實現。若想要學習 v0 版,能夠閱讀這篇文章。從 Chrome 54 和 Safari 10.1(雖然只有部分支持) 開始支持自定義元素接口 v1,微軟 Edge 還處於其原型設計階段而 Mozilla 從 v50 開始支持,但默認不支持須要顯式啓用。目前只有 webkit 瀏覽器徹底支持。然而,如上所述,可使用墊片庫兼容到包括 IE11 在內的全部瀏覽器。

檢測可用性

經過檢查 window 對象中的 customElements 屬性是否可用來檢查瀏覽器是否支持自定義元素。

const supportsCustomElements = 'customElements' in window;

if (supportsCustomElements) {
  // 可使用自定義元素接口
}

不然須要使用墊片庫:

function loadScript(src) {
  return new Promise(function(resolve, reject) {
    const script = document.createElement('script');

    script.src = src;
    script.onload = resolve;
    script.onerror = reject;

    document.head.appendChild(script);
  });
}

// Lazy load the polyfill if necessary.
if (supportsCustomElements) {
  // 瀏覽器原生支持自定義元素
} else {
  loadScript('path/to/custom-elements.min.js').then(_ => {
    // 加載自定義元素墊片
  });
}

總之,網頁組件標準中的自定義元素爲開發者提供了以下功能:

  • 把 JavaScript 和 CSS 樣式整合入 HTML 元素
  • 容許開發者擴展已有的 HTML 元素(內置和其它自定義元素)
  • 不須要其它庫或者框架的支持。只須要原生 JavaScript,HTML 和 CSS 還有可選的墊片庫來支持舊瀏覽器。
  • 能夠和其它網頁組件功能無縫銜接(shadow DOM,模板,插槽等)。
  • 和瀏覽器開發者工具緊密集成在一塊兒。
  • 使用已知的可訪問功能

總之,自定義元素和開發者已經使用過的組件技術並無什麼大的不一樣。它只讓開發網頁程序過程更加便攜的另外一種方式。那麼,它讓更快地構建很是複雜的程序成爲可能。

參考資料:

相關文章
相關標籤/搜索