Web Components實現類Element UI中的Card卡片

引言

Web Components 是一個瀏覽器原生支持的組件化方案,允許你創建新的自定義、可封裝、可重用的HTML 標記。不用加載任何外部模塊,直接就可以在瀏覽器中跑。本文就簡單介紹一下:使用 Web Components 實現一個類 Element UI 中的 Card 卡片組件。

隨著前端工程化生態日益成熟,出現瞭很多優秀的框架,如:VueReactAngular等等,極大的提高瞭日常開發效率。

其中組件化開發發揮瞭至關重要的作用,但是這些組件化開發都需要依賴第三方框架,編譯打包之後才能在瀏覽器正常使用。

而原生組件 Web Components ,相比與第三方框架使用起來更簡單直接,符合直覺,不用加載任何外部模塊,代碼量小。

Web Components 核心組成

  • 自定義元素(custom element),使用 window.customElements.define API註冊
  • Shadow DOM隔離,影藏標記結構、樣式和行為
  • 可以在<template>中定義標記結構、樣式,多次重用。利用 slot 插槽、命名插槽,可以傳入定制化的結構UI,使用上類似 Vue 中的 slot 插槽

1. Custom Elements

自定義的 HTML 標簽,稱為自定義元素(custom element)。根據規范,自定義元素的名稱必須包含連詞線-,用與區別原生的 HTML 元素。所以,<com-card>不能寫成<comcard>

<div id="custom-card" class="com-card">
  <div class="com-card-head">
    <slot name="head"></slot>
  </div>
  <div class="com-card-body">
    <slot></slot>
    <div class="link-wrap">
      <a class="link" href="" title=" rel="external nofollow"  rel="external nofollow" "></a>
    </div>
  </div>
</div>
<script>
  class ComCard extends HTMLElement {
    constructor() {
      super()
      var tplEle = document.getElementById('custom-card')
      this.append(tplEle)
    }
  }
  window.customElements.define('com-card', ComCard)
</script>

這樣就註冊瞭瀏覽器可識別渲染的一個自定義元素標簽。

2. Shadow DOM

Shadow DOM 是對DOM的一個封裝。可以將標記結構、樣式和行為隱藏起來,並與頁面上的其他代碼相隔離,保證不同的部分不會混在一起,可使代碼更加幹凈、整潔。

使用自定義元素的 this.attachShadow() 方法可以開啟 Shadow DOM

class ComCard extends HTMLElement {
  constructor() {
    super()
    var shadow = this.attachShadow({mode: 'closed'})  // open
    var tplEle = document.getElementById('custom-card')
    shadow.appendChild(tplEle)
  }
}
window.customElements.define('com-card', ComCard); 

其中參數{ mode: 'closed' },表示 Shadow DOM 是封閉的,不允許外部訪問。

3. templates 和 slots

因為組件的樣式應該與代碼封裝在一起,隻對自定義元素生效,不影響外部的全局樣式。所以,可以把樣式寫在<template>裡面,這樣作為自定義元素結構的基礎可以被多次重用。

<template id="custom-card-template">
  <style>
    .com-card {
    }
  </style>
  <div class="com-card">
  </div>
</template>
<script>
  class ComCard extends HTMLElement {
    constructor() {
      super();
      var shadow = this.attachShadow({mode: 'closed'})  // open
      var tplEle = document.getElementById('custom-card-template')
      var content = tplEle.content.cloneNode(true)
      shadow.appendChild(content)
    }
  }
  window.customElements.define('com-card', ComCard);
</script>

完整代碼

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Web Component</title>
  <style>
    * {
        box-sizing: border-box;
    }
    body {
        font-size: 14px;
    }
    .box {
        padding: 5px 0 30px;
    }
    .box .caption {
        display: none;
    }
    .box h1 {
        text-align: center;
    }
    .box li {
        color: #666;
        font-size: 14px;
        line-height: 1.8;
        margin-top: 15px;
    }
    .img {
        display: block;
        width: 80%;
        margin: 0 !important;
    }
    .card-head {
        display: flex;
        justify-content: space-between;
        align-items: center;
    }
    .card-title {
        color: #333;
        font-size: 16px;
    }
    .card-head-btn {
        color: #409eff;
        cursor: pointer;
        text-decoration: none !important;
    }
    .card-head-btn:hover {
        text-decoration: none;
    }
  </style>
</head>
<body>
<div class="box">
  <h1>Web Component</h1>
  <com-card data-show-head="0" data-url="https://tiven.cn" data-title="天問博客">
    <div slot="head" class="card-head">
      <div class="card-title">卡片名稱</div>
      <a class="card-head-btn">操作按鈕</a>
    </div>
    <img class="img" src="https://tiven.cn/static/img/kpl-sunwukong-a3Lt-ed2NG9r4NFDm_9DA.jpg" alt="天問">
  </com-card>
  

  

  <com-card data-show-head="1" data-url="https://tiven.cn/p/de241e23/" data-title="Vite+Vue3+Vant快速構建項目">
    <div slot="head" class="card-head">
      <div class="card-title">卡片名稱</div>
      <a class="card-head-btn" onclick="hello()">操作按鈕</a>
    </div>
    <img class="img" src="https://tiven.cn/static/img/kpl-xuance-JqX71qH7aTflHV_gqvhIc.jpg" alt="天問">
    <ol>
      <li>君不見黃河之水天上來,奔流到海不復回。</li>
      <li>君不見高堂明鏡悲白發,朝如青絲暮成雪。</li>
      <li>天生我材必有用,千金散盡還復來。</li>
    </ol>
  </com-card>
</div>
<template id="custom-card-template">
  <style>
    .com-card {
        min-width: 200px;
        min-height: 100px;
        border-radius: 4px;
        border: 1px solid #ebeef5;
        background-color: #fff;
        overflow: hidden;
        color: #303133;
        transition: .3s;
        box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
    }
    .com-card-head {
        padding: 10px 20px;
        border-bottom: 1px solid #ebeef5;
        box-sizing: border-box;
    }
    .com-card-body {
        padding: 20px;
    }
    .link-wrap {
        text-align: left;
        padding-top: 20px;
    }
    .link {
        display: inline-block;
        height: 42px;
        line-height: 43px;
        padding: 0 30px;
        text-align: center;
        cursor: pointer;
        color: #fff;
        background-color: #409eff;
        border-color: #409eff;
        -webkit-appearance: none;
        box-sizing: border-box;
        outline: none;
        transition: .1s;
        font-weight: 500;
        -moz-user-select: none;
        -webkit-user-select: none;
        -ms-user-select: none;
        font-size: 14px;
        border-radius: 4px;
        text-decoration: none !important;
    }
  </style>
  <div class="com-card">
    <div class="com-card-head">
      <slot name="head"></slot>
    </div>
    <div class="com-card-body">
      <slot></slot>
      <div class="link-wrap">
        <a class="link" href="" title=" rel="external nofollow"  rel="external nofollow" "></a>
      </div>
    </div>
  </div>
</template>
<script>
  class ComCard extends HTMLElement {
    constructor() {
      super();
      var shadow = this.attachShadow({mode: 'closed'})  // open
      var tplEle = document.getElementById('custom-card-template')
      var content = tplEle.content.cloneNode(true)
      var attrList = Array.from(this.attributes);
      var props = attrList.reduce((prev, item)=>{
        prev[item.name] = item.value
        return prev
      }, {})
      if (props['data-show-head']!=='1') {
        var head = content.querySelector('.com-card-head')
        head.remove()
      }
      var urlEle = content.querySelector('.link')
      if (props['data-url'] && props['data-title']) {
        urlEle.href = props['data-url']
        urlEle.title = props['data-title']
        urlEle.innerText = props['data-title']
      } else {
        urlEle.remove()
      }
      shadow.appendChild(content)
    }
    connectedCallback(){
      //在這裡發送數據請求(Ajax)
      console.log('connectedCallback')
    }
    //被從文檔DOM中刪除時調用
    disconnectedCallback(){
      console.log('disconnectedCallback')
    }
    //被移動到新的文檔時調用
    adoptedCallback(){
      console.log('adoptedCallback')
    }
    //當增加、刪除、修改自身的屬性時被調用
    attributeChangedCallback(){
      console.log('attributeChangedCallback')
    }
  }
  window.customElements.define('com-card', ComCard);
  function hello() {
    alert('Hello,Web Component')
  }
</script>
</body>
</html>

最終效果如上圖所示

Web Components vs Vue Components

Vue Component Web Component
data 實例屬性
props attributes
watch observedAttributes、attributeChangedCallback
computed getters
methods class methods
mounted connectedCallback
destroyed disconnectedCallback
style scoped template中的style
template template

Web Components 生命周期回調函數

connectedCallback:當 custom element首次被插入文檔DOM時,被調用。

disconnectedCallback:當 custom element從文檔DOM中刪除時,被調用。

adoptedCallback:當 custom element被移動到新的文檔時,被調用。

attributeChangedCallback: 當 custom element增加、刪除、修改自身屬性時,被調用。

優點 and 缺點

優點:

  • 瀏覽器原生支持,不需要引入額外的第三方庫
  • 語義化
  • 復用性,移植性高
  • 不同團隊不同項目可以共用組件

缺點:

  • 需要操作DOM
  • 目前瀏覽器兼容性、性能方面不夠友好
  • 和外部css交互比較難

基於web components的框架

LitElement 是一個快速、輕量級的 Web UI 框架。使用 lit-html 來渲染元素。

Polymer 是一款實用、基於事件驅動、封裝性和交互性強的 Web UI 框架。

Omi 是基於 Web 組件的跨框架跨平臺框架 。移動端 & 桌面 & 小程序。

以上就是Web Components實現類Element UI中的Card卡片的詳細內容,更多關於Web Components實現Element UI的資料請關註WalkonNet其它相關文章!

推薦閱讀: