Web Components 上手指南

如今的前端開發基本離不開 React、Vue 這兩個框架的支撐,而這兩個框架下面又衍生出了許多的自定義組件庫:css

這些組件庫的出現,讓咱們能夠直接使用已經封裝好的組件,並且在開源社區的幫助下,出現了不少的模板項目( vue-element-adminAnt Design Pro ),能讓咱們快速的開始一個項目。html

雖然 React、Vue 爲咱們的組件開發提供了便利,可是這二者在組件的開發思路上,一個是自創的 JSX 語法,一個是特有的單文件模板的語法,二者的目標都是想提供一種組件的封裝方法。畢竟都有其原創的東西在裏面,和咱們剛開始接觸的 Web 基礎的 HTML、CSS、JS 的方式仍是有些出入的。今天介紹的就是,經過 HTML、CSS、JS 的方式來實現自定義的組件,也是目前瀏覽器原生提供的方案:Web Components。前端

什麼是 Web Components?

Web Components 自己不是一個單獨的規範,而是由一組DOM API 和 HTML 規範所組成,用於建立可複用的自定義名字的 HTML 標籤,而且能夠直接在你的 Web 應用中使用。vue

代碼的複用一直都是咱們追求的目標,在 JS 中可複用的代碼咱們能夠封裝成一個函數,可是對於複雜的HTML(包括相關的樣式及交互邏輯),咱們一直都沒有比較好的辦法來進行復用。要麼藉助後端的模板引擎,要麼藉助已有框架對 DOM API 的二次封裝,而 Web Components 的出現就是爲了補足瀏覽器在這方面的能力。git

如何使用 Web Components?

Web Components 中包含的幾個規範,都已在 W3C 和 HTML 標準中進行了規範化,主要由三部分組成:github

  • Custom elements(自定義元素):一組 JavaScript API,用來建立自定義的 HTML標籤,並容許標籤建立或銷燬時進行一些操做;
  • Shadow DOM(影子DOM):一組 JavaScript API,用於將建立的 DOM Tree 插入到現有的元素中,且 DOM Tree 不能被外部修改,不用擔憂元素被其餘地方影響;
  • HTML templates(HTML模板):經過 <template><slot> 直接在 HTML 文件中編寫模板,而後經過 DOM API 獲取。

Custom elements(自定義元素)

瀏覽器提供了一個方法: customElements.define() , 來進行自定義標籤的定義。該方法接受三個參數:後端

  • 自定義元素的名稱,一個 DOMString 標準的字符串,爲了防止自定義元素的衝突,必須是一個帶短橫線鏈接的名稱(e.g. custom-tag)。
  • 定義自定義元素的一些行爲,相似於 React、Vue 中的生命週期。
  • 擴展參數(可選),該參數類型爲一個對象,且須要包含 extends 屬性,用於指定建立的元素繼承自哪個內置元素(e.g. { extends: 'p' })。

下面經過一些例子,演示其用法,完整代碼放到了 JS Bin 上。瀏覽器

建立一個新的 HTML 標籤

先看看如何建立一個全新的自定義元素。app

class HelloUser extends HTMLElement {
  constructor() {
    // 必須調用 super 方法
    super();

    // 建立一個 div 標籤
    const $box = document.createElement("p");
    let userName = "User Name";
    if (this.hasAttribute("name")) {
      // 若是存在 name 屬性,讀取 name 屬性的值
      userName = this.getAttribute("name");
    }
    // 設置 div 標籤的文本內容
    $box.innerText = `Hello ${userName}`;

    // 建立一個 shadow 節點,建立的其餘元素應附着在該節點上
    const shadow = this.attachShadow({ mode: "open" });
    shadow.appendChild($box);
  }
}

// 定義一個名爲 <hello-user /> 的元素
customElements.define("hello-user", HelloUser);
<hello-user name="Shenfq"></hello-user>

這時候頁面上就會生成一個 <p> 標籤,其文本內容爲:Hello Shenfq。這種形式的自定義元素被稱爲: Autonomous custom elements,是一個獨立的元素,能夠在 HTML 中直接使用。框架

擴展已有的 HTML 標籤

咱們除了能夠定義一個全新的 HTML 標籤,還能夠對已有的 HTML 標籤進行擴展,例如,咱們須要封裝一個與 <ul> 標籤能力相似的組件,就可使用以下方式:

class SkillList extends HTMLUListElement {
  constructor() {
    // 必須調用 super 方法
    super();

    if (
      this.hasAttribute("skills") &&
      this.getAttribute("skills").includes(',')
    ) {
      // 讀取 skills 屬性的值
      const skills = this.getAttribute("skills").split(',');
      skills.forEach(skill => {
        const item = document.createElement("li");
        item.innerText = skill;
        this.appendChild(item);
      })
    }
  }
}

// 對 <ul> 標籤進行擴展
customElements.define("skill-list", SkillList, { extends: "ul" });
<ul is="skill-list" skills="js,css,html"></ul>

對已有的標籤進行擴展,須要用到 customElements.define 方法的第三個參數,且第二參數的類,也須要繼承須要擴展標籤的對應的類。使用的時候,只須要在標籤加上 is 屬性,屬性值爲第一個參數定義的名稱。

生命週期

自定義元素的生命週期比較簡單,一共只提供了四個回調方法:

  • connectedCallback:當自定義元素被插入到頁面的 DOM 文檔時調用。
  • disconnectedCallback:當自定義元素從 DOM 文檔中被刪除時調用。
  • adoptedCallback:當自定義元素被移動時調用。
  • attributeChangedCallback: 當自定義元素增長、刪除、修改自身屬性時調用。

下面演示一下使用方法:

class HelloUser extends HTMLElement {
  constructor() {
    // 必須調用 super 方法
    super();

    // 建立一個 div 標籤
    const $box = document.createElement("p");
    let userName = "User Name";
    if (this.hasAttribute("name")) {
      // 若是存在 name 屬性,讀取 name 屬性的值
      userName = this.getAttribute("name");
    }
    // 設置 div 標籤的文本內容
    $box.innerText = `Hello ${userName}`;

    // 建立一個 shadow 節點,建立的其餘元素應附着在該節點上
    const shadow = this.attachShadow({ mode: "open" });
    shadow.appendChild($box);
  }
  connectedCallback() {
    console.log('建立元素')
    // 5s 後移動元素到 iframe
    setTimeout(() => {
      const iframe = document.getElementsByTagName("iframe")[0]
      iframe.contentWindow.document.adoptNode(this)
    }, 5e3)
  }
  disconnectedCallback() {
    console.log('刪除元素')
  }
  adoptedCallback() {
    console.log('移動元素')
  }
}
<!-- 頁面插入一個 iframe,將自定義元素移入其中 -->
<iframe width="0" height="0"></iframe>
<hello-user name="Shenfq"></hello-user>

在元素被建立後,等待 5s,而後將自定義元素移動到 iframe 文檔中,這時候能看到控制檯會同時出現 刪除元素移動元素 的 log。

Console

Shadow DOM(影子DOM)

在前面介紹自定義元素的時候,已經用到了 Shadow DOM。 Shadow DOM 的做用是讓內部的元素與外部隔離,讓自定義元素的結構、樣式、行爲不受到外部的影響。

咱們能夠看到前面定義的 <hello-user> 標籤,在控制檯的 Elements 內,會顯示一個 shadow-root ,代表內部是一個 Shadow DOM。

Shadow DOM

其實 Web Components 沒有提出以前,瀏覽器內部就有使用 Shadow DOM 進行一些內部元素的封裝,例如 <video> 標籤。咱們須要如今控制檯的配置中,打開 Show user agent ashdow DOM 開關。

設置

而後在控制檯的 Elements 內,就能看到 <video> 標籤內其實也有一個 shadow-root

video 標籤

建立 Shadow DOM

咱們能夠在任意一個節點內部建立一個 Shadow DOM,在獲取元素實例後,調用 Element.attachShadow() 方法,就能將一個新的 shadow-root 附加到該元素上。

該方法接受一個對象,且只有一個 mode 屬性,值爲 openclosed,表示 Shadow DOM 內的節點是否能被外部獲取。

<div id="root"></div>
<script>
  // 獲取頁面的
  const $root = document.getElementById('root');
  const $p = document.createElement('p');
  $p.innerText = '建立一個 shadow 節點';
  const shadow = $root.attachShadow({mode: 'open'});
  shadow.appendChild($p);
</script>

Shadow DOM

mode 的差別

前面提到了 mode 值爲 openclosed,主要差別就是是否可使用 Element.shadowRoot 獲取到 shadow-root 進行一些操做。

<div id="root"></div>
<script>
  // 獲取頁面的
  const $root = document.getElementById('root');
  const $p = document.createElement('p');
  $p.innerText = '建立一個 shadow 節點';
  const shadow = $root.attachShadow({mode: 'open'});
  shadow.appendChild($p);
  console.log('is open', $div.shadowRoot);
</script>

open mode

<div id="root"></div>
<script>
  // 獲取頁面的
  const $root = document.getElementById('root');
  const $p = document.createElement('p');
  $p.innerText = '建立一個 shadow 節點';
  const shadow = $root.attachShadow({mode: 'closed'});
  shadow.appendChild($p);
  console.log('is closed', $div.shadowRoot);
</script>

closed mode

HTML templates(HTML模板)

前面的案例中,有個很明顯的缺陷,那就是操做 DOM 仍是得使用 DOM API,相比起 Vue 得模板和 React 的 JSX 效率明顯更低,爲了解決這個問題,在 HTML 規範中引入了 <tempate><slot> 標籤。

使用模板

模板簡單來講就是一個普通的 HTML 標籤,能夠理解成一個 div,只是這個元素內的因此內容不會展現到界面上。

<template id="helloUserTpl">
  <p class="name">Name</p>
  <a target="blank" class="blog">##</a>
</template>

在 JS 中,咱們能夠直接經過 DOM API 獲取到該模板的實例,獲取到實例後,通常不能直接對模板內的元素進行修改,要調用 tpl.content.cloneNode 進行一次拷貝,由於頁面上的模板並非一次性的,可能其餘的組件也要引用。

// 經過 ID 獲取標籤
const tplElem = document.getElementById('helloUserTpl');
const content = tplElem.content.cloneNode(true);

咱們在獲取到拷貝的模板後,就能對模板進行一些操做,而後再插入到 Shadow DOM 中。

<hello-user name="Shenfq" blog="http://blog.shenfq.com" />

<script>
  class HelloUser extends HTMLElement {
    constructor() {
      // 必須調用 super 方法
      super();

      // 經過 ID 獲取標籤
      const tplElem = document.getElementById('helloUserTpl');
      const content = tplElem.content.cloneNode(true);

      if (this.hasAttribute('name')) {
        const $name = content.querySelector('.name');
        $name.innerText = this.getAttribute('name');
      }
      if (this.hasAttribute('blog')) {
        const $blog = content.querySelector('.blog');
        $blog.innerText = this.getAttribute('blog');
        $blog.setAttribute('href', this.getAttribute('blog'));
      }
      // 建立一個 shadow 節點,建立的其餘元素應附着在該節點上
      const shadow = this.attachShadow({ mode: "closed" });
      shadow.appendChild(content);
    }
  }

  // 定義一個名爲 <hello-user /> 的元素
  customElements.define("hello-user", HelloUser);
</script>

添加樣式

<template> 標籤中能夠直接插入 <style> 標籤在,模板內部定義樣式。

<template id="helloUserTpl">
  <style>
    :host {
      display: flex;
      flex-direction: column;
      width: 200px;
      padding: 20px;
      background-color: #D4D4D4;
      border-radius: 3px;
    }

    .name {
      font-size: 20px;
      font-weight: 600;
      line-height: 1;
      margin: 0;
      margin-bottom: 5px;
    }

    .email {
      font-size: 12px;
      line-height: 1;
      margin: 0;
      margin-bottom: 15px;
    }
  </style>
  <p class="name">User Name</p>
  <a target="blank" class="blog">##</a>
</template>

其中 :host 僞類用來定義 shadow-root的樣式,也就是包裹這個模板的標籤的樣式。

佔位元素

佔位元素就是在模板中的某個位置先佔據一個位置,而後在元素插入到界面上的時候,在指定這個位置應該顯示什麼。

<template id="helloUserTpl">
  <p class="name">User Name</p>
  <a target="blank" class="blog">##</a>
  <!--佔位符-->
  <slot name="desc"></slot> 
</template>

<hello-user name="Shenfq" blog="http://blog.shenfq.com">
  <p slot="desc">歡迎關注公衆號:更了不得的前端</p>
</hello-user>

這裏用的用法與 Vue 的 slot 用法一致,不作過多的介紹。

總結

到這裏 Web Components 的基本用法就介紹得差很少了,相比於其餘的支持組件化方案的框架,使用 Web Components 有以下的優勢:

  • 瀏覽器原生支持,不須要引入額外的第三方庫;
  • 真正的內部私有化的 CSS,不會產生樣式的衝突;
  • 無需通過編譯操做,便可實現的組件化方案,且與外部 DOM 隔離;

Web Components 的主要缺點就是標準可能還不太穩定,例如文章中沒有提到的模板的模塊化方案,就已經被廢除,如今尚未正式的方案引入模板文件。並且原生的 API 雖然能用,可是就是很差用,要否則也不會出現 jQuery 這樣的庫來操做 DOM。好在如今也有不少基於 Web Components 實現的框架,後面還會開篇文章專門講一講使用 Web Components 的框架 lit-htmllit-element

好啦,今天的文章就到這裏了,但願你們能有所收穫。

image

相關文章
相關標籤/搜索