Web Components

組件化是前端工程化重要的一環(huán),UI 和 交互(或邏輯)的復(fù)用極大的提高開(kāi)發(fā)效率以及減少代碼冗余。

目前開(kāi)源的組件庫(kù)都是特定于框架的,比如:基于 Vue 的 Element UI,基于 React 的 Ant Design 等。也就是說(shuō)這些組件庫(kù)無(wú)法做到跨框架使用。

Web Component 是一組 web 原生 API ,可以創(chuàng)建可復(fù)用的自定義元素 (custom elements)。組件完全是原生 JavaScript API 開(kāi)發(fā)的,可以跨框架使用。此外,web components 并不是一個(gè)單一的規(guī)范,而是三個(gè)獨(dú)立的 web 技術(shù)的集合。

  1. Custom elements(自定義元素)
  2. Shadow DOM (影子元素)
  3. HTML template (HTML 模版)

基本實(shí)現(xiàn)思路:

  1. 創(chuàng)建類(lèi)或者函數(shù)來(lái)實(shí)現(xiàn)組件的邏輯、交互功能
  2. 創(chuàng)建 Shadow DOM 并附加在自定義元素上,往 Shadow DOM 中添加要展示的元素
  3. 通過(guò) customElements.define 方法注冊(cè)
  4. 在頁(yè)面中使用注冊(cè)的自定義元素

Custom elements

customElements存在于 window 全局對(duì)象上,對(duì)自定義元素提供支持,包含四個(gè) API:define、get、whenDefined、 upgrade

customElements.define

自定義元素通過(guò) customElements.define 方法注冊(cè),包含三個(gè)參數(shù):

  1. 自定義元素名,格式:短線連接的字符串
  2. 自定義元素構(gòu)造器
  3. 可選的,含有 extends 屬性的對(duì)象。指定所創(chuàng)建的元素繼承自哪個(gè)內(nèi)置元素,可以繼承任何內(nèi)置元素

可以創(chuàng)建兩種類(lèi)型的自定義元素:

1. 自主定制元素

獨(dú)立元素,不會(huì)從內(nèi)置 HTML 元素繼承

class WebCTitle extends HTMLElement {
  constructor() {
    super()

    this.attachShadow({ mode: 'open' })
    this.style()
    this.render()
  }

  style() {
    const style = document.createElement('style')

    style.textContent = `

        .title {
            color: red;
            font-size: 20px;
        }
    `
    this.shadowRoot.appendChild(style)
  }

  render() {
    const template = document.createElement('template')

    template.innerHTML = `
        <div class="title">Hello!</div>
    `
    this.shadowRoot.appendChild(template.content.cloneNode(true))
  }
}

customElements.define('webc-title', WebCTitle)

Usage

<webc-title></webc-title>

2. 自定義內(nèi)置元素

元素繼承并擴(kuò)展自?xún)?nèi)置 HTML 元素

class WebCTitle extends HTMLParagraphElement {
  // 邏輯同上
}

customElements.define('webc-title', WebCTitle, {
  extends: 'p'
})

Usage

<p is="webc-title"></p>
自定義元素標(biāo)準(zhǔn)需要滿(mǎn)足的條件:
  1. 必須用短線連接
  2. 元素名必須小寫(xiě)
  3. 不要多次注冊(cè)
  4. 元素不能自閉合

customElements.get

獲取自定義元素的類(lèi),一般用于擴(kuò)展第三方組件庫(kù)。比如:

const WebCProp = customElements.get('webc-prop')

customElements.define('webc-props-plus', class extends WebCProp{
   constructor() {
    super()

    this.click = () => {}
  }
})

whenDefined

一般是將 <script> 標(biāo)簽放在頁(yè)面底部,為的是讓瀏覽器渲染引擎先解析 DOM,然后再解析 JavaScript。

當(dāng)渲染引擎讀取到自定義元素的時(shí)候,并不知道它是什么元素(此時(shí)注冊(cè)腳本還沒(méi)執(zhí)行),一般來(lái)說(shuō)當(dāng)渲染引擎碰到一個(gè)不認(rèn)識(shí)的元素的時(shí)候,會(huì)認(rèn)為這是一個(gè)無(wú)效的元素。但是,為了原生元素區(qū)分,自定義元素的命名規(guī)則必須包含短線,所有當(dāng)渲染引擎解析一個(gè)帶有短線的非原生元素時(shí),會(huì)認(rèn)為是一個(gè)未定義的自定義元素,不會(huì)當(dāng)作一個(gè)無(wú)效元素。當(dāng)執(zhí)行到注冊(cè)自定義元素的代碼時(shí),就會(huì)將之前未定義的元素標(biāo)記為定義的元素。

定義的元素對(duì)應(yīng)的偽類(lèi)選擇器就是 :defined,未定義的元素對(duì)應(yīng)的偽類(lèi)選擇器就是 :not(:defined)

通過(guò)這個(gè)偽類(lèi)選擇器,可以在定義元素之前的空白時(shí)間內(nèi),設(shè)置自定義元素的加載樣式。

whenDefine 是元素定義后觸發(fā)的回調(diào),通常用于異步注冊(cè)組件的時(shí)候。

<style>
  /* :not() 匹配不符合條件的元素 */
  :not(:defined) {
    display: block
    width: 200px;
    height: 200px;
    color: red;
    border: 1px solid pink;
  }
  :defined {
    color: blue;
  }
</style>
<when-define>loading...</when-define>
<script src="./whenDefine.js"></script>

class WhenDefine extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({ mode: 'open' })
  }

  render() {
    const template = document.createElement('template')

    template.innerHTML = `<div>hello</div>`
    this.shadowRoot.appendChild(template.content.cloneNode(true))
  }

  connectedCallback() {
    this.render()
  }
}

setTimeout(() => {
  customElements.define('when-define', WhenDefine)
}, 3000)

customElements.whenDefined('when-define').then(() => {
  console.log('注冊(cè)完成')
})

upgrade

upgrade 升級(jí),如果在定義元素之前先通過(guò) js 創(chuàng)建了元素,則元素實(shí)例并不是自定義元素類(lèi)的實(shí)例,則通過(guò) upgrade 將其升級(jí):

 // 創(chuàng)建
  const webTitle = document.createElement('webc-title')

  // 定義并注冊(cè)
  class WebTitle extends HTMLElement {}
  customElements.define('web-title', WebTitle)

  webTitle instanceof WebTitle // false

  // 升級(jí)元素
  customElements.upgrade(webTitle)

  webTitle instanceof WebTitle // true

生命周期

構(gòu)造函數(shù)中可以指定自定義元素的生命周期,將會(huì)在不同階段調(diào)用。具體包括四個(gè):

  • connectedCallback:當(dāng) custom element 首次被插入文檔 DOM 時(shí),被調(diào)用。

  • disconnectedCallback:當(dāng) custom element 從文檔 DOM 中刪除時(shí),被調(diào)用。

  • adoptedCallback:當(dāng) custom element 被移動(dòng)到新的文檔時(shí),被調(diào)用。

  • attributeChangedCallback: 當(dāng) custom element 增加、刪除、修改自身屬性時(shí),被調(diào)用。

function updateStyle(ele) {
  const shadow = ele.shadowRoot

  shadow.querySelector('style').textContent = `
        .title {
            color: #fff;
            font-size: 24px;
            text-align: center;
        }
        .dv {
            width: ${ele.getAttribute('w')}px;
            height: ${ele.getAttribute('h')}px;
            background-color: ${ele.getAttribute('c')};
        }
    `
}

class WebCTitle extends HTMLElement {
  static get observedAttributes() {
    return ['w', 'h', 'c']
  }

  constructor() {
    super()

    this.attachShadow({ mode: 'open' })
    this.style()
    this.render()
  }

  style() {
    const style = document.createElement('style')

    this.shadowRoot.appendChild(style)
  }

  render() {
    const template = document.createElement('template')

    template.innerHTML = `
        <div class="title">
            <div class="dv">Hello!</div>
        </div>
    `
    this.shadowRoot.appendChild(template.content.cloneNode(true))
  }

  connectedCallback() {
    updateStyle(this)
  }

  disconnectedCallback() {
    console.log('從文檔移除了')
  }

  adoptedCallback() {}
  attributeChangedCallback(name, oldValue, newValue) {
    console.log(name, oldValue, newValue)
    updateStyle(this)
  }
}

當(dāng)元素屬性變化后,要觸發(fā)attributeChangedCallback()回調(diào)函數(shù),必須通過(guò) 構(gòu)造器的靜態(tài)屬性 observedAttributes() 的 get 函數(shù)來(lái)實(shí)現(xiàn),函數(shù)返回一個(gè)數(shù)組,包含需要監(jiān)聽(tīng)的屬性名稱(chēng):

static get observedAttributes() {
  return ['w', 'h', 'c']
}

slot

slot屬性返回已插入元素所在的 Shadow DOM slot 的名稱(chēng)。

 connectedCallback() {
    console.log(this.querySelector('p').slot) // desc
  }

Shadow DOM

Shadow DOM 是很重要的 API,可以將結(jié)構(gòu)、樣式和行為隱藏起來(lái),獨(dú)立的將 DOM 附加到一個(gè)元素上與頁(yè)面上的其他代碼隔離。在微前端 qiankun 框架中就是通過(guò) shadow DOM 來(lái)實(shí)現(xiàn)不同容器樣式隔離的,還有 video 標(biāo)簽的按鈕和控制器。

Shadow DOM 術(shù)語(yǔ):

  • Shadow host:一個(gè)常規(guī) DOM 節(jié)點(diǎn),Shadow DOM 會(huì)被附加到這個(gè)節(jié)點(diǎn)上

  • Shadow tree:Shadow DOM 內(nèi)部的 DOM 樹(shù)

  • Shadow boundary:Shadow DOM 結(jié)束的地方,也是常規(guī) DOM 開(kāi)始的地方

  • Shadow root: Shadow tree 的根節(jié)點(diǎn)

image.png
image.png

用法

Element.attachShadow

Element.attachShadow() 方法將一個(gè) shadow root 附加到一個(gè)元素上。該方法接受一個(gè)對(duì)象參數(shù),對(duì)象的屬性為 mode,值為 open 或 closed。

  1. open:可以通過(guò) js API Element.shadowRoot 屬性獲取到 Shadow DOM
  2. closed:無(wú)法通過(guò) js 獲取 shadowRoot
image.png

將 shadow DOM 附加到元素后,就可以對(duì) shadow DOM 進(jìn)行常規(guī)的 DOM 操作了。

注意:Shadow DOM 內(nèi)部的樣式不會(huì)影響到外部,可以做到樣式隔離。

shadowRoot

shadowRoot 就是通過(guò) elements.attachShadow() 附加在元素上的 shadow DOM 的根結(jié)點(diǎn)。它是 ShadowRoot 的實(shí)例,繼承關(guān)系是 ShadowRoot --> DocumentFragment --> Node。所以具有 DOM 的常規(guī)屬性和方法。專(zhuān)屬屬性包括:

shadowRoot.host

附加的宿主 DOM 元素

shadowRoot.innerHTML

shadowRoot 內(nèi)部的 DOM 樹(shù)

shadowRoot.mode

只讀,值為 open OR closed

Templates & Slots

Templates

當(dāng)某段模版重復(fù)使用時(shí),可以通過(guò) template 模版去定義,生成一個(gè)文檔片段,并且不會(huì)顯示在頁(yè)面中。可以通過(guò) js 獲取,然后添加到 DOM 中。

<template id="tpl">
  <div class="title">
    <div class="dv">hello</div>
  </div>
</template>
const template = document.querySelector('#tpl')

console.dir(template.content)

修改 render 方法

render() {
    const template = document.querySelector('#tpl')

    this.shadowRoot.appendChild(template.content.cloneNode(true))
  }
image.png

template.content 為 DocumentFragment 的實(shí)例,該構(gòu)造器繼承自 Node。可以通過(guò) cloneNode 方法拷貝文檔片段,添加到 shadow DOM 中。這樣的話其實(shí)可以把 style 也定義在 template 中。

<template id="tpl">
  <style>
    .title {
      color: #fff;
      font-size: 24px;
      text-align: center;
    }
    .dv {
      width: 100px;
      height: 100px;
      background-color: red;
    }
  </style>

  <div class="title">
    <div class="dv">hello</div>
  </div>
</template>
class WebCTitle extends HTMLElement {
  constructor() {
    super()

    this.attachShadow({ mode: 'open' })
    this.render()
  }

  render() {
    const template = document.querySelector('#tpl')

    this.shadowRoot.appendChild(template.content.cloneNode(true))
  }
}

Slots

template 適用于固定 html 片段,但是在某些場(chǎng)景下元素不靈活。web component 支持 slots,類(lèi)似于 Vue 中的 slot。并且支持具名插槽。

<template id="tpl">
  <div class="title">
    <div class="dv">
      <slot>默認(rèn)標(biāo)題</slot>
      <slot name="desc">默認(rèn)描述</slot>
    </div>
  </div>
</template>

<webc-title>
  <span>標(biāo)題</span>
  <p slot="desc">描述信息</p>
</webc-title>
image.png

slotchange

如果 添加/刪除 了插槽元素,瀏覽器將監(jiān)視插槽并更新渲染。另外,由于不復(fù)制 light DOM 節(jié)點(diǎn),而是僅在插槽中進(jìn)行渲染,所以?xún)?nèi)部的變化是立即可見(jiàn)的。因此無(wú)需執(zhí)行任何操作即可更新渲染。但如果組件想知道插槽的更改,那么可以用 slotchange 事件。

slotchange事件會(huì)在插槽第一次填充時(shí)觸發(fā),并且在插槽元素的 添加/刪除/替換 操作(而不是其子元素)時(shí)觸發(fā),插槽是event.target

class WebCList extends HTMLElement {
  constructor() {
    super()

    this.attachShadow({ mode: 'open' })
    this.render()
  }

  render() {
    const template = document.createElement('template')

    template.innerHTML = `
        <div>
            <h1><slot name="title">默認(rèn)標(biāo)題</slot></h1>
            <ul>
                <slot name="item"></slot>
            </ul>
        </div>
    `
    this.shadowRoot.appendChild(template.content.cloneNode(true))
  }

  connectedCallback() {
    // 默認(rèn)執(zhí)行一次

    this.shadowRoot.firstElementChild
      .querySelector('ul')
      .addEventListener('slotchange', e => {
        // e.target 為 slot

        // console.dir(e.target)
        // console.dir(e.target.name)

        // slot.assignedNodes({flatten: true/false}) – 分配給插槽的 DOM 節(jié)點(diǎn)。默認(rèn)情況下,flatten 選項(xiàng)為 false。如果顯式地設(shè)置為 true,則它將更深入地查看扁平化 DOM ,如果嵌套了組件,則返回嵌套的插槽,如果未分配節(jié)點(diǎn),則返回備用內(nèi)容。

        // console.dir(e.target.assignedNodes())

        // 分配給插槽的 DOM 元素(與上面相同,但僅元素節(jié)點(diǎn))

        console.log(e.target.assignedElements())
        console.log(e.target.assignedElements()[1].assignedSlot) // 返回節(jié)點(diǎn)的插槽
      })
  }
}

customElements.define('webc-list', WebCList)
<webc-list id="list">
  <h3 slot="title">TODO list</h3>
  <li slot="item">mike</li>
  <li slot="item">rose</li>
  <li slot="item">jack</li>
</webc-list>

2 秒后動(dòng)態(tài)增加 li 元素

const list = document.querySelector('#list')

setTimeout(() => {
  const li = document.createElement('li')

  li.setAttribute('slot', 'item')
  li.textContent = 'mark'
  list.appendChild(li)
}, 2000)

Shadow DOM 樣式

shadow DOM 可以包含 <style><link rel="stylesheet" href=""> 標(biāo)簽。在使用 link 時(shí),樣式表是 HTTP 緩存的,因此不會(huì)為使用同一模板的多個(gè)組件重新下載樣式表。

:host

:host 選擇器可以選擇 shadow 宿主(包含 shadow 樹(shù)的元素)

例如,我們正在創(chuàng)建 <webc-dialog> 元素,并且想讓它居中。那么需要對(duì) <webc-dialog> 元素本身設(shè)置樣式。這就需要用 :host 設(shè)置

<template id="tpl">
  <style>
    /* 這些樣式將從內(nèi)部應(yīng)用到 custom-dialog 元素上 */
    :host {
      position: fixed;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      display: inline-block;
      border: 1px solid red;
      padding: 10px;
    }
  </style>
  <slot></slot>
</template>

<script>
  const tpl = document.querySelector('#tpl')
  customElements.define(
    'webc-dialog',
    class extends HTMLElement {
      connectedCallback() {
        this.attachShadow({ mode: 'open' }).append(tpl.content.cloneNode(true))
      }
    }
  )
</script>

<webc-dialog> Hello! </webc-dialog>

:host(selector)

:host 相同,但僅在 shadow 宿主與 selector 匹配時(shí)才應(yīng)用樣式。

例如,當(dāng) <webc-dialog> 具有 centered 屬性時(shí)才將其居中:

<webc-dialog centered> Hello! </webc-dialog>

:host-context(selector)

:host 相同,但僅當(dāng)外部文檔中的 shadow 宿主或它的任何祖先節(jié)點(diǎn)與 selector 匹配時(shí)才應(yīng)用樣式。

例如,:host-context(.dark-theme) 只有在 <webc-dialog> 或者 <webc-dialog> 的任何祖先節(jié)點(diǎn)上有 dark-theme 類(lèi)時(shí)才匹配:

<body class="dark-theme">
  <!-- :host-context(.dark-theme) 只應(yīng)用于 .dark-theme 內(nèi)部的 webc-dialog-->
  <custom-dialog> hello </custom-dialog>
</body>

shadow 宿主

shadow 宿主( <webc-dialog> 本身)駐留在 light DOM 中,因此它受到文檔 CSS 規(guī)則的影響。

如果在局部的 :host 和文檔中都給一個(gè)屬性設(shè)置樣式,那么文檔樣式優(yōu)先。

<style>
  webc-dialog {
    padding: 0;
  }
</style>

這樣就可以在 :host 規(guī)則中設(shè)置組件的 “默認(rèn)” 樣式,然后在文檔中覆蓋它們。唯一的例外是當(dāng)局部屬性被標(biāo)記 !important 時(shí),對(duì)于這樣的屬性,局部樣式優(yōu)先。

Slot 內(nèi)容樣式

占槽元素來(lái)自 light DOM,所以使用文檔樣式。局部樣式不會(huì)影響占槽內(nèi)容。

在下面的例子中,按照文檔樣式,占槽的 <span> 是粗體,但是它不從局部樣式中獲取 background

<style>
  span {
    font-weight: 600;
  }
</style>

<webc-card>
  <div slot="username">
    <span>Jack</span>
  </div>
</webc-card>

<script>
  customElements.define(
    'webc-card',
    class extends HTMLElement {
      connectedCallback() {
        this.attachShadow({ mode: 'open' })
        this.shadowRoot.innerHTML = `
          <style>
              span {
                  background: red;
              }
          </style>
          Name: <slot name="username"></slot>
        `
      }
    }
  )
</script>

結(jié)果是粗體,但背景不是紅色。

如果想在組件中設(shè)置占槽元素的樣式,有兩種選擇。

  1. 可以對(duì) <slot> 本身進(jìn)行樣式化,并借助 CSS 繼承:
customElements.define(
  'webc-card',
  class extends HTMLElement {
    connectedCallback() {
      this.attachShadow({ mode: 'open' })
      this.shadowRoot.innerHTML = `
      <style>
          slot[name="username"] {
              font-weight: bold;
          }
      </style>
      Name: <slot name="username"></slot>
    `
    }
  }
)
  1. 使用

    ::slotted(selector)
    

    偽類(lèi)。它根據(jù)兩個(gè)條件來(lái)匹配元素:

    1. 這是一個(gè)占槽元素,來(lái)自于 light DOM。插槽名并不重要,任何占槽元素都可以,但只能是元素本身,而不是它的子元素 。
    2. 該元素與 selector 匹配。

在例子中,::slotted(div) 正好選擇了 <div slot="username"> ,但是沒(méi)有選擇它的子元素:

<webc-card>
  <div slot="username">
    <div>jack</div>
  </div>
</webc-card>

<script>
  customElements.define(
    'webc-card',
    class extends HTMLElement {
      connectedCallback() {
        this.attachShadow({ mode: 'open' })
        this.shadowRoot.innerHTML = `
          <style>
              ::slotted(div) {
                  border: 1px solid red;
              }
          </style>
          Name: <slot name="username"></slot>
        `
      }
    }
  )
</script>

CSS 變量

通過(guò)給自定義元素中定義 CSS 變量,在 shadow DOM 中的 style 中使用 CSS 變量,這種方式用在開(kāi)發(fā)三方組件庫(kù)時(shí),可以做到樣式自定義。

事件

冒泡

Shadow DOM 內(nèi)部觸發(fā)的事件大都可以向上冒泡。如果是一個(gè) slot 元素,并且事件發(fā)生在內(nèi)部某個(gè)地方,那么會(huì)冒泡到 <slot> 并繼續(xù)向上。

event.composedPath

使用 event.composedPath() 獲得原始事件目標(biāo)的完整路徑及所有 shadow 元素。比如:

customElements.define(
  'webc-dialog',
  class extends HTMLElement {
    connectedCallback() {
      this.attachShadow({ mode: 'open' }).append(tpl.content.cloneNode(true))

      this.shadowRoot.querySelector('slot').addEventListener('click', e => {
        console.log(e.composedPath())
      })
    }
  }
)
image.png

對(duì)于 span 上的點(diǎn)擊事件,調(diào)用 event.composedPath()會(huì)返回一個(gè)數(shù)組,[span, slot, div, document-fragment, webc-dialog, body, html, document, Window]

event.composed

大多數(shù)事件能成功冒泡到 shadow DOM 邊界。很少有事件不能冒泡到 shadow DOM 邊界。

由事件對(duì)象的 composed 屬性控制。如果 composedtrue,那么事件就能穿過(guò)邊界。否則它僅能在 shadow DOM 內(nèi)部捕獲。

自定義事件

組件中事件的處理以及向組件外部暴露事件通過(guò)自定義事件來(lái)處理。

當(dāng) dispatch 自定義事件需要設(shè)置 bubbles 和 composed 為 true 使的事件冒泡。

比如 shadow 內(nèi)部觸發(fā)了某個(gè)事件,需要暴露出 API 以供用戶(hù)使用,則通過(guò) dispath 事件,并可以傳遞參數(shù)。

CustomEvent

構(gòu)造器,參數(shù)一是自定義事件類(lèi)型,參數(shù)二對(duì)象,detail 為傳遞的參數(shù)。

customElements.define(
  'webc-dialog',

  class extends HTMLElement {
    connectedCallback() {
      this.attachShadow({ mode: 'open' }).append(tpl.content.cloneNode(true))

      this.shadowRoot.querySelector('slot').addEventListener('click', e => {
        this.dispatchEvent(
          new CustomEvent('tab-click', {
            bubbles: true,
            composed: true,
            detail: 123
          })
        )
      })
    }
  }
)

Usage

<webc-dialog id="dialog"> <span> Hello! </span> </webc-dialog>

<script>
  document.querySelector('#dialog').addEventListener('tab-click', e => {
    // ...
  })
</script>

注意

大部分內(nèi)建事件的 composed 是 true,有些內(nèi)建事件是 false。比如:

  • mouseentermouseleave
  • loadunloadaborterror
  • select
  • slotchange

處理數(shù)據(jù)的方式

Attributes

Attributes 是和HTML相關(guān)的概念,是我們定義HTML元素(即HTML標(biāo)記)特征的方式,同樣適用于 web components。

對(duì)于一下元素的, width、height、src、alt是attr。

<img src="./xx.png" alt="hello" width="200" height="200" />

當(dāng)渲染引擎解析 HTML 代碼以創(chuàng)建 DOM 對(duì)象時(shí),它會(huì)識(shí)別標(biāo)準(zhǔn)屬性并從中創(chuàng)建 DOM 屬性。但這只限于標(biāo)準(zhǔn)屬性中,而不是自定義屬性。此外,并非所有元素的標(biāo)準(zhǔn)屬性都相同。例如,id是所有元素通用的標(biāo)準(zhǔn)屬性,而alt屬性只是img上的。

對(duì)于不自動(dòng)映射的自定義屬性,采用一下方法進(jìn)行操作:

element.hasAttributes(): 元素是否至少具有一個(gè)屬性
element.hasAttribute(name): 元素是否具有某個(gè)屬性
element.setAttribute(name, value):給元素設(shè)置屬性
element.getAttribute(name):返回名為name屬性的值,如果不存在則返回 null
element.getAttributeNames():返回元素屬性組成的數(shù)組
element.toggleAttribute(name):切換給定元素的某個(gè)布爾值屬性的狀態(tài)(如果屬性不存在則添加屬性,屬性存在則移除屬性)

<webc-list title="hello"></custom-list>

<script>
class WebcList extends HTMLElement {

  constructor() {
    super();
    console.log(this.getAttribute("title"));
  }
}

customElements.define("webc-list", WebcList);
</script>

構(gòu)造器方法內(nèi)部的 this 即自定義元素的實(shí)例。

使用類(lèi)的靜態(tài) getter 方法 observedAttributes 列出要監(jiān)聽(tīng)的屬性,當(dāng)屬性變化后會(huì)觸發(fā)生命周期attributeChangedCallback 函數(shù)。

優(yōu)點(diǎn):直觀、簡(jiǎn)單

Properties

Properties是與 JavaScript 相關(guān)的概念。它們是 DOM Node 接口的一部分。

prop可以是任何類(lèi)型的值,并且屬性名區(qū)分大小寫(xiě)。

prop 反映射 到attr

對(duì)于標(biāo)準(zhǔn)attr,attr 會(huì)和 prop 建立映射關(guān)系。 prop 是 HTML attr的JavaScript的表示。就意味著當(dāng)一個(gè)prop被修改時(shí),attr也會(huì)改變,反之亦然。

對(duì)于自定義元素,需要在定義自定義元素時(shí)明確地執(zhí)行此操作。

通過(guò)為我們希望將其值映射到其同名attr的prop定義 getter 和 setter 方法來(lái)做到這一點(diǎn)。

class {
  set color(value){
    this.setAttribute('color', value)
  }

  get color(){
    this.getAttribute('color')
  }
}

image.png

假設(shè)我們有一個(gè)自定義元素,其類(lèi)有一個(gè)名為的屬性color,我們希望反映它的同名屬性。給出這種情況,代碼如下:

 <img src="aa.jpg"/>
setTimeout(() => {
  document.querySelector('img').src="bb.jpg"
},1500)

以上示例,img的src會(huì)在1.5s后改變。

瀏覽器引擎會(huì)為標(biāo)準(zhǔn)的 HTML attr 創(chuàng)建 prop,但不會(huì)為自定義元素創(chuàng)建。

使用 Attributes 適用于傳遞字符串。否則無(wú)論傳遞什么類(lèi)型的數(shù)據(jù)都會(huì)被轉(zhuǎn)成字符串,比如數(shù)字、函數(shù)、對(duì)象、數(shù)組等。

傳遞對(duì)象需要通過(guò) JSON.stringify 序列化成字符串,在組件中通過(guò) JSON.parse 反序列化成對(duì)象。這種方式很不合理,當(dāng)對(duì)象很大時(shí),不僅繁瑣而且 DOM 結(jié)構(gòu)冗余。

既然 DOM 也是對(duì)象,那么是不是可以給其任意的添加刪除屬性?當(dāng)然可以。

const dialog = document.querySelector('#dialog')

dialog.list = [1, 2, 3]
console.log(dialog.list)

這種方式不僅可以傳遞對(duì)象,甚至可以傳遞任何類(lèi)型的數(shù)據(jù)。

優(yōu)點(diǎn):可以處理任何數(shù)據(jù)類(lèi)型。更適合處理復(fù)雜數(shù)據(jù)類(lèi)型

自定義事件

就是以上提到的自定義事件

event bus

這種方式不再是監(jiān)聽(tīng)自定義元素的事件,而是定義了一個(gè)全局的事件總線,這樣就可以在任何地方使用。類(lèi)似于 Node 中的 EventEmitter 模塊,不同的是讓 DOM 管理事件。

class EventBus {
  constructor() {
    this._bus = document.createElement('div')
  }

  regisger(event, callback) {
    this._bus.addEventListener(event, callback)
  }

  remove(event, callback) {
    this._bus.removeEventListener(event, callback)
  }

  fire(event, detail = {}) {
    this._bus.dispatchEvent(new CustomEvent(event, detail))
  }
}
const bus = new EventBus()

bus.regisger('clicked', () => {
  console.log(123)
})

bus.fire('clicked', { value: 23 })

優(yōu)勢(shì):適合組件之間的通信

總結(jié)

web components 的諸多好處:

封裝

封裝是 web components最重要的特性和好處。封裝確保我們的代碼與組件所屬頁(yè)面中已經(jīng)存在的任何框架或功能的任何其他元素隔離,從而避免沖突和不可控的行為。

可重用性

封裝和 ESM 實(shí)現(xiàn)了可重用性。使得開(kāi)發(fā)者能夠編寫(xiě)可在許多站點(diǎn)和平臺(tái)上使用的可重用組件。

靈活性

可以通過(guò)多種方式自定義 Web 組件。例如,使用attributes/properties自定義行為,使用slot自定義內(nèi)容,使用 CSS 變量自定義樣式。提供了很大的靈活性,使得一個(gè)原始組件可以有很多不同的形態(tài)。

表現(xiàn)

web components 為以前只能使用第三方庫(kù)提供的某些功能提供了標(biāo)準(zhǔn)規(guī)范。這樣可以免除外部依賴(lài)。同時(shí)意味著可以減少我們的代碼和捆綁包的復(fù)雜性和大小,從而縮短加載頁(yè)面時(shí)間。

兼容性

現(xiàn)代瀏覽器都有很好的兼容。
對(duì)于舊版瀏覽器中不可用的功能,可以使用 polyfill,例如 https://www.webcomponents.org

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,238評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,430評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,134評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,893評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,653評(píng)論 6 408
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,136評(píng)論 1 323
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,212評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,372評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,888評(píng)論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,738評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,939評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,482評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,179評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,588評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,829評(píng)論 1 283
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,610評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,916評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容

  • Web組件由四部分組成 Template Shadow DOM (Chrome Opera支持) Custom E...
    KeKeMars閱讀 4,374評(píng)論 3 2
  • 第一章 1、使用瀏覽器去訪問(wèn)的程序,叫網(wǎng)頁(yè) 2、web代碼存放在服務(wù)器 代碼分為兩種:① 運(yùn)行在瀏覽器端:前端代...
    fastwe閱讀 3,427評(píng)論 0 2
  • 前言 不知不覺(jué),2019年即將接近尾聲,現(xiàn)有前端三大框架也各自建立著自己的生態(tài)、自己的使用群體。從angular1...
    Kaku_fe閱讀 2,797評(píng)論 0 19
  • 前端開(kāi)發(fā)面試題 <a name='preface'>前言</a> 只看問(wèn)題點(diǎn)這里 看全部問(wèn)題和答案點(diǎn)這里 本文由我...
    自you是敏感詞閱讀 780評(píng)論 0 3
  • 前言 在初涉前端之時(shí),我就一直在好奇一個(gè)問(wèn)題,為什么像: …… 等等這些標(biāo)簽,看起來(lái)似乎很簡(jiǎn)單,可為什么可以展現(xiàn)出...
    隱逸王閱讀 434評(píng)論 0 0