Web Components 入門例項教程

阮一峰發表於2019-08-06

元件是前端的發展方向,現在流行的 React 和 Vue 都是元件框架。

谷歌公司由於掌握了 Chrome 瀏覽器,一直在推動瀏覽器的原生元件,即 Web Components API。相比第三方框架,原生元件簡單直接,符合直覺,不用載入任何外部模組,程式碼量小。目前,它還在不斷髮展,但已經可用於生產環境。

Web Components API 內容很多,本文不是全面的教程,只是一個簡單演示,讓大家看一下怎麼用它開發元件。

一、自定義元素

下圖是一個使用者卡片。

本文演示如何把這個卡片,寫成 Web Components 元件,這裡是最後的完整程式碼

網頁只要插入下面的程式碼,就會顯示使用者卡片。


<user-card></user-card>

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

二、customElements.define()

自定義元素需要使用 JavaScript 定義一個類,所有<user-card>都會是這個類的例項。


class UserCard extends HTMLElement {
  constructor() {
    super();
  }
}

上面程式碼中,UserCard就是自定義元素的類。注意,這個類的父類是HTMLElement,因此繼承了 HTML 元素的特性。

接著,使用瀏覽器原生的customElements.define()方法,告訴瀏覽器<user-card>元素與這個類關聯。


window.customElements.define('user-card', UserCard);

三、自定義元素的內容

自定義元素<user-card>目前還是空的,下面在類裡面給出這個元素的內容。


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

    var image = document.createElement('img');
    image.src = 'https://semantic-ui.com/images/avatar2/large/kristy.png';
    image.classList.add('image');

    var container = document.createElement('div');
    container.classList.add('container');

    var name = document.createElement('p');
    name.classList.add('name');
    name.innerText = 'User Name';

    var email = document.createElement('p');
    email.classList.add('email');
    email.innerText = 'yourmail@some-email.com';

    var button = document.createElement('button');
    button.classList.add('button');
    button.innerText = 'Follow';

    container.append(name, email, button);
    this.append(image, container);
  }
}

上面程式碼最後一行,this.append()this表示自定義元素例項。

完成這一步以後,自定義元素內部的 DOM 結構就已經生成了。

四、<template>標籤

使用 JavaScript 寫上一節的 DOM 結構很麻煩,Web Components API 提供了<template>標籤,可以在它裡面使用 HTML 定義 DOM。


<template id="userCardTemplate">
  <img src="https://semantic-ui.com/images/avatar2/large/kristy.png" class="image">
  <div class="container">
    <p class="name">User Name</p>
    <p class="email">yourmail@some-email.com</p>
    <button class="button">Follow</button>
  </div>
</template>

然後,改寫一下自定義元素的類,為自定義元素載入<template>


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

    var templateElem = document.getElementById('userCardTemplate');
    var content = templateElem.content.cloneNode(true);
    this.appendChild(content);
  }
}  

上面程式碼中,獲取<template>節點以後,克隆了它的所有子元素,這是因為可能有多個自定義元素的例項,這個模板還要留給其他例項使用,所以不能直接移動它的子元素。

到這一步為止,完整的程式碼如下。


<body>
  <user-card></user-card>
  <template>...</template>

  <script>
    class UserCard extends HTMLElement {
      constructor() {
        super();

        var templateElem = document.getElementById('userCardTemplate');
        var content = templateElem.content.cloneNode(true);
        this.appendChild(content);
      }
    }
    window.customElements.define('user-card', UserCard);    
  </script>
</body>

五、新增樣式

自定義元素還沒有樣式,可以給它指定全域性樣式,比如下面這樣。


user-card {
  /* ... */
}

但是,元件的樣式應該與程式碼封裝在一起,只對自定義元素生效,不影響外部的全域性樣式。所以,可以把樣式寫在<template>裡面。


<template id="userCardTemplate">
  <style>
   :host {
     display: flex;
     align-items: center;
     width: 450px;
     height: 180px;
     background-color: #d4d4d4;
     border: 1px solid #d5d5d5;
     box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
     border-radius: 3px;
     overflow: hidden;
     padding: 10px;
     box-sizing: border-box;
     font-family: 'Poppins', sans-serif;
   }
   .image {
     flex: 0 0 auto;
     width: 160px;
     height: 160px;
     vertical-align: middle;
     border-radius: 5px;
   }
   .container {
     box-sizing: border-box;
     padding: 20px;
     height: 160px;
   }
   .container > .name {
     font-size: 20px;
     font-weight: 600;
     line-height: 1;
     margin: 0;
     margin-bottom: 5px;
   }
   .container > .email {
     font-size: 12px;
     opacity: 0.75;
     line-height: 1;
     margin: 0;
     margin-bottom: 15px;
   }
   .container > .button {
     padding: 10px 25px;
     font-size: 12px;
     border-radius: 5px;
     text-transform: uppercase;
   }
  </style>

  <img src="https://semantic-ui.com/images/avatar2/large/kristy.png" class="image">
  <div class="container">
    <p class="name">User Name</p>
    <p class="email">yourmail@some-email.com</p>
    <button class="button">Follow</button>
  </div>
</template>

上面程式碼中,<template>樣式裡面的:host偽類,指代自定義元素本身。

六、自定義元素的引數

<user-card>內容現在是在<template>裡面設定的,為了方便使用,把它改成引數。


<user-card
  image="https://semantic-ui.com/images/avatar2/large/kristy.png"
  name="User Name"
  email="yourmail@some-email.com"
></user-card>

<template>程式碼也相應改造。


<template id="userCardTemplate">
  <style>...</style>

  <img class="image">
  <div class="container">
    <p class="name"></p>
    <p class="email"></p>
    <button class="button">Follow John</button>
  </div>
</template>

最後,改一下類的程式碼,把引數加到自定義元素裡面。


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

    var templateElem = document.getElementById('userCardTemplate');
    var content = templateElem.content.cloneNode(true);
    content.querySelector('img').setAttribute('src', this.getAttribute('image'));
    content.querySelector('.container>.name').innerText = this.getAttribute('name');
    content.querySelector('.container>.email').innerText = this.getAttribute('email');
    this.appendChild(content);
  }
}
window.customElements.define('user-card', UserCard);    

七、Shadow DOM

我們不希望使用者能夠看到<user-card>的內部程式碼,Web Component 允許內部程式碼隱藏起來,這叫做 Shadow DOM,即這部分 DOM 預設是隱藏的,開發者工具裡面看不到。

自定義元素的this.attachShadow()方法開啟 Shadow DOM,詳見下面的程式碼。


class UserCard extends HTMLElement {
  constructor() {
    super();
    var shadow = this.attachShadow( { mode: 'closed' } );

    var templateElem = document.getElementById('userCardTemplate');
    var content = templateElem.content.cloneNode(true);
    content.querySelector('img').setAttribute('src', this.getAttribute('image'));
    content.querySelector('.container>.name').innerText = this.getAttribute('name');
    content.querySelector('.container>.email').innerText = this.getAttribute('email');

    shadow.appendChild(content);
  }
}
window.customElements.define('user-card', UserCard);

上面程式碼中,this.attachShadow()方法的引數{ mode: 'closed' },表示 Shadow DOM 是封閉的,不允許外部訪問。

至此,這個 Web Component 元件就完成了,完整程式碼可以訪問這裡。可以看到,整個過程還是很簡單的,不像第三方框架那樣有複雜的 API。

八、元件的擴充套件

在前面的基礎上,可以對元件進行擴充套件。

(1)與使用者互動

使用者卡片是一個靜態元件,如果要與使用者互動,也很簡單,就是在類裡面監聽各種事件。


this.$button = shadow.querySelector('button');
this.$button.addEventListener('click', () => {
  // do something
});

(2)元件的封裝

上面的例子中,<template>與網頁程式碼放在一起,其實可以用指令碼把<template>注入網頁。這樣的話,JavaScript 指令碼跟<template>就能封裝成一個 JS 檔案,成為獨立的元件檔案。網頁只要載入這個指令碼,就能使用<user-card>元件。

這裡就不展開了,更多 Web Components 的高階用法,可以接著學習下面兩篇文章。

九、參考連結

(完)

相關文章