【原生組件】一文帶你入門 Web Components

前言

本文將介紹一些 Web Components 的相關知識,最後經過一個組件封裝的案例講解帶你入門 Web Components。javascript

什麼是 Web Components

MDN:Web Components 是一套不一樣的技術,容許您建立可重用的定製元素(它們的功能封裝在您的代碼以外)而且在您的web應用中使用它們。html

Web Components 不是單一的某個規範,而是由3組不一樣的技術標準組成的組件模型,它旨在提高組件封裝和代碼複用能力。java

  • Custom Elements - 自定義元素
  • Shadow DOM - 影子 DOM
  • HTML Template - HTML 模版

下面我們經過一個簡單的例子來逐步瞭解 Web Components 的知識以及使用。web

一個簡單的例子

建立模版

<body>
  <!-- 建立模版 -->
  <template id="my-template">
    <p>hello~<slot name="user"></slot></p>
  </template>
</body>
複製代碼

這能夠看做是咱們的組件內容,由template包裹,此時該標籤及其內部元素是不可見的,還須要咱們用 JavaScript 去操做並將其添加到 DOM 裏才能看得見。數組

能夠看到,除了template標籤以外還有slot標籤,對於用慣了框架的咱們應該對它不陌生了,插槽有助於提高咱們在開發中的靈活度,用法跟咱們平時的使用差很少,這裏就很少贅述了。markdown

定義組件的類對象

<body>
  <!-- 模版 -->
  <template id="my-template">
    <p>hello~<slot name="user"></slot></p>
  </template>
  
  <script> // 定義類對象 class HelloComponent extends HTMLElement { // 構造函數 constructor() { // 必須先調用 super 方法 super(); // 獲取<template> const template = document.getElementById('my-template'); // 建立影子 DOM 並將 template 添加到其子節點下 const _shadowRoot = this.attachShadow({ mode: 'open' }); const content = template.content.cloneNode(true) _shadowRoot.appendChild(content); } } // 自定義標籤 window.customElements.define('hello-component', HelloComponent); </script>
</body>
複製代碼

類 HelloComponent 繼承自 HTMLElement,咱們在構造函數中去掛載 DOM 元素。app

獲取<template>節點之後,克隆了它的全部子元素,這是由於可能有多個自定義元素的實例,這個模板還要留給其餘實例使用,因此不能直接移動它的子元素。框架

shadow DOM

上面咱們用 attachShadow 方法建立了一個 shadow DOM,影子 DOM,如其名同樣,它能夠將一個隱藏的、獨立的 DOM 附加到一個元素上ide

參數 mode 有兩個可選值:open 和 closed。函數

open 表示能夠經過頁面內的 JavaScript 方法來獲取 shadow DOM。

let shadowroot = element.shadowRoot;
複製代碼

相反地,closed 表示不能夠從外部獲取 shadow DOM,此時 element.shadowRoot 將返回null。

shadow DOM 其實離咱們很近,HTML一些內置的標籤就包含了 shadow DOM,如inputvideo等。

在開發者工具中,打開設置面板,勾選 Show user agent shadow DOM。

image.png

查看 input 元素。

image.png

能夠看到,input 多了一些咱們平時沒注意到的東西。如今你應該知道 input 的內容和 placeholder 從哪裏來了,一樣的,video 默認的那些播放按鈕也都是藏在這裏面的。

自定義標籤

window.customElements.define('hello-component', HelloComponent)
複製代碼

window.customElements.define 方法的第一個參數是自定義標籤的名稱,第二個參數是用於定義元素行爲的類對象。

注意:自定義標籤的名稱不能是單個單詞,且必須有短橫線

使用

<body>
  <!-- 模版 -->
  <template id="my-template">
    <p>hello~<slot name="user"></slot></p>
  </template>
  <script> // 定義類對象 class HelloComponent extends HTMLElement { // 構造函數 constructor() { // 必須先調用 super 方法 super(); // 獲取<template> const template = document.getElementById('my-template'); // 建立影子 DOM 並將 template 添加到其子節點下 const _shadowRoot = this.attachShadow({ mode: 'open' }); const content = template.content.cloneNode(true) _shadowRoot.appendChild(content); } } // 自定義標籤 window.customElements.define('hello-component', HelloComponent); </script>
  <!-- 使用 -->
  <hello-component>
    <span slot="user">張三</span>
  </hello-component>
</body>
複製代碼

生命週期

除了構造函數以外,custom elements 還有4個生命週期函數:

  • connectedCallback:當 custom element首次被插入文檔DOM時,被調用。
  • disconnectedCallback:當 custom element從文檔DOM中刪除時,被調用。
  • adoptedCallback:當 custom element被移動到新的文檔時,被調用。
  • attributeChangedCallback: 當 custom element增長、刪除、修改自身屬性時,被調用。
class HelloComponent extends HTMLElement {
  // 構造函數
  constructor() {
    super();
  }
  
  connectedCallback() {
    console.log('當自定義元素第一次被鏈接到文檔DOM時被調用')
  }

  disconnectedCallback() {
    console.log('當自定義元素與文檔DOM斷開鏈接時被調用')
  }

  adoptedCallback() {
    console.log('當自定義元素被移動到新文檔時被調用')
  }

  attributeChangedCallback() {
    console.log('當自定義元素的一個屬性被增長、移除或更改時被調用')
  }
}
複製代碼

實戰:封裝一個商品卡片組件

image.png

咱們指望的使用方式:

<body>
  <!-- 引入 -->
  <script type="module"> import './goods-card.js' </script>
  <!-- 使用 -->
  <goods-card img="https://img1.baidu.com/it/u=2613325730,275475287&fm=224&fmt=auto&gp=0.jpg" goodsName="跑鞋" ></goods-card>
</body>
複製代碼

組件內容實現

// goods-card.js
class GoodsCard extends HTMLElement {
  constructor() {
    super();
    const template = document.createElement('template')
    template.innerHTML = ` <style> .goods-card-container { width: 200px; border: 1px solid #ddd; } .goods-img { width: 100%; height: 200px; } .goods-name { padding: 10px 4px; margin: 0; text-align: center; } .add-cart-btn { width: 100%; } </style> <div class="goods-card-container"> <img class="goods-img" /> <p class="goods-name"></p> <button class="add-cart-btn">加入購物車</button> </div> `;
    const _shadowRoot = this.attachShadow({ mode: 'open' })
    const content = template.content.cloneNode(true)
    _shadowRoot.appendChild(content)
  }
}

window.customElements.define('goods-card', GoodsCard)
複製代碼

如今咱們已經成功把 DOM 渲染出來了,只不過如今商品圖片和商品名稱仍是空的,接下來咱們須要去獲取父組件傳遞過來的值並將其展現在視圖上。

class GoodsCard extends HTMLElement {
  constructor() {
    super();
    // ...省略部分代碼
    // 獲取並更新視圖
    const _goodsNameDom = _shadowRoot.querySelector('.goods-name')
    const _goodsImgDom = _shadowRoot.querySelector('.goods-img')
    _goodsNameDom.innerHTML = this.name
    _goodsImgDom.src = this.img
  }
  
  get name() {
    return this.getAttribute('name')
  }

  get img() {
    return this.getAttribute('img')
  }
}
複製代碼

到這裏咱們就把一個商品卡片渲染出來了。

響應式視圖更新

<goods-card img="http://zs-oa.oss-cn-shenzhen.aliyuncs.com/zsoa/goods/v1v2/1021161140018/spu1/349334579590860800.jpg" name="跑鞋" ></goods-card>
<script type="module"> import './goods-card.js' </script>
<script> setTimeout(() => { document.querySelector('goods-card').setAttribute('name', '籃球鞋') }, 2000); </script>
複製代碼

這裏咱們會發現一個問題,若是組件的傳值發生了變化,此時視圖上是不會更新的,商品名稱仍然顯示跑鞋而不是籃球鞋

這時候咱們須要用到前面講到的生命週期,用attributeChangedCallback來解決這個問題,對代碼進行改造。

class GoodsCard extends HTMLElement {
  constructor() {
    super();
    // ...省略部分代碼
    const _shadowRoot = this.attachShadow({ mode: 'open' })
    const content = template.content.cloneNode(true)
    _shadowRoot.appendChild(content)
    this._goodsNameDom = _shadowRoot.querySelector('.goods-name')
    this._goodsImgDom = _shadowRoot.querySelector('.goods-img')
  }

  attributeChangedCallback(key, oldVal, newVal) {
    console.log(key, oldVal, newVal)
    this.render()
  }

  static get observedAttributes() {
    return ['img', 'name']
  }
  
  get name() {
    return this.getAttribute('name')
  }

  get img() {
    return this.getAttribute('img')
  }

  render() {
    this._goodsNameDom.innerHTML = this.name
    this._goodsImgDom.src = this.img
  }
}
複製代碼

經過attributeChangedCallback,咱們能夠在屬性發生改變時執行 render 方法從而讓視圖獲得更新。

這裏還有一個陌生的函數observedAttributes,該函數是與attributeChangedCallback配套使用的,若是沒寫observedAttributes或屬性值發生改變的參數名稱沒有在observedAttributes函數返回的數組裏,attributeChangedCallback是不會觸發。

static get observedAttributes() {
  return ['img']
}

// 不會執行,由於商品名稱 name 沒有在 observedAttributes 返回的數組裏
attributeChangedCallback(key, oldVal, newVal) {
  console.log(key, oldVal, newVal)
  this.render()
}
複製代碼

事件交互

最後咱們來給按鈕添加一個點擊事件,這是一個很必要的需求,方便組件調用者去處理本身的邏輯。

constructor() {
  // ...省略部分代碼
  this._button = _shadowRoot.querySelector('.add-cart-btn')
  this._button.addEventListener('click', () => {
    this.dispatchEvent(new CustomEvent('onButton', { detail: 'button' }))
  })
}
複製代碼
<body>
  <script> document.querySelector('goods-card').addEventListener('onButton', (e) => { console.log('添加購物車', e.detail) // button }) </script>
</body>
複製代碼

完整代碼

<body>
  <!-- 使用 -->
  <goods-card img="https://img1.baidu.com/it/u=2613325730,275475287&fm=224&fmt=auto&gp=0.jpg" name="跑鞋" ></goods-card>

  <!-- 引入 -->
  <script type="module"> import './goods-card.js' </script>
  <script> document.querySelector('goods-card').addEventListener('onButton', (e) => { console.log('按鈕事件', e.detail) }) </script>
</body>
複製代碼
// goods-card.js
class GoodsCard extends HTMLElement {
  constructor() {
    super();
    const template = document.createElement('template')
    template.innerHTML = ` <style> .goods-card-container { width: 200px; border: 1px solid #ddd; } .goods-img { width: 100%; height: 200px; } .goods-name { padding: 10px 4px; margin: 0; text-align: center; } .add-cart-btn { width: 100%; } </style> <div class="goods-card-container"> <img class="goods-img" /> <p class="goods-name"></p> <button class="add-cart-btn">加入購物車</button> </div> `;
    const _shadowRoot = this.attachShadow({ mode: 'open' })
    const content = template.content.cloneNode(true)
    _shadowRoot.appendChild(content)
    this._goodsNameDom = _shadowRoot.querySelector('.goods-name')
    this._goodsImgDom = _shadowRoot.querySelector('.goods-img')
    this._button = _shadowRoot.querySelector('.add-cart-btn')
    this._button.addEventListener('click', () => {
      this.dispatchEvent(new CustomEvent('onButton', { detail: 'button' }))
    })
  }

  attributeChangedCallback(key, oldVal, newVal) {
    console.log(key, oldVal, newVal)
    this.render()
  }

  static get observedAttributes() {
    return ['img', 'name']
  }
  
  get name() {
    return this.getAttribute('name')
  }

  get img() {
    return this.getAttribute('img')
  }

  render() {
    this._goodsNameDom.innerHTML = this.name
    this._goodsImgDom.src = this.img
  }
}

window.customElements.define('goods-card', GoodsCard);
複製代碼

最後

以上,經過一個組件的封裝,相信你已經對Web Components有必定的認識和了解了,固然,本文只是帶你入門,講解一些基礎的開發實踐知識,並不是Web Components的所有,Web Components各個部分還有不少值得深刻的地方,感興趣的同窗可自行深刻了解一下。

感謝

本次分享到這裏就結束了,感謝你的閱讀,若是對你有什麼幫助的話,歡迎點贊支持一下❤️。

若是有什麼錯誤或不足,歡迎評論區指正、交流❤️。

參考資料:

阮一峯 - Web Components 入門實例教程

MDN - Web Componnents

相關文章
相關標籤/搜索