介紹

Angular Components、ReactJS Components、VueJS Components 通通都有,就是沒有屬於 VanillaJS 的 Components,所以今後只好自己創造 (大誤)。

其實早在 2011 年, Web Components 的概念就已經被提出,除了希望可以建立重複使用的 Web UI 元件外,也帶入了封裝的概念,如透過 Shadow DOM 的引入以封裝 Custom Element 的 CSS 樣式,避免互相污染,另外透過 Custom Element 中自定義的行為來達到功能上的封裝與重用性。

上面提到 Shadow DOM 、 Custom Elements,另外還有 HTML Template 為 Web Components 主要的三個核心內容,以下我們就分別介紹這三項核心內容。

Custom elements

Custom elements 透過原生 JavaScript APIs 來自定義客製化 HTML 標籤,包含它的 attributes、properties與 events 等,方便依照網站設計的客製化需求來使用。

1
2
3
4
5
6
class MyCustomElement extends HTMLElement {
  ...
}

// 定義 Custom element
window.customElements.define("custom-element", MyCustomElement);

上面的範例示範了如何透過 JavaScript 來宣告一個 Custom Element,並定義一個 custom-element 的 HTML 標籤,定義完成後就可以透過下列的範例來實用這個 Custom Element。

1
2
3
4
5
6
<html>
<body>
  <!-- 使用客製化的 Custom Element -->
  <custom-element></custom-element>
</body>
</html>

Shadow DOM

在講 Shadow DOM 之前我們先介紹一下什麼是 DOM tree, HTML 是一個結構化的標籤語言,所以每一個標籤在 DOM tree 上可以看作一個節點 (Node)。

DOM tree DOM Tree 範例 (圖片來源: 維基百科)

而 Shadow DOM 則是允許創建隱藏的 DOM trees 並附加在任意一個的 DOM 節點上。

Shadow DOM Shadow DOM 渲染 (圖片來源: MDN)

由於 Shadow DOM 是屬於隱藏的 DOM trees,所以可以起到封裝的效果,您可以在 Shadow DOM 中加上 <style></style> 元件,並在其中撰寫關於這個元件的 CSS 樣式,而這裡面的 CSS 樣式並不會去影響到原生 DOM trees 中的節點,這屬於樣式的封裝。

另一個封裝的效果是關於 id 與 class,在 HTML 中擁有同樣 id 的 element 只能同時存在一個,對於 UI 元件設計人員來說這非常不方便,而不同的 elements 雖然可以使用相同的 class 屬性,但命名衝突時還是可能造成下列的問題:

  1. CSS 選擇器問題: 在使用 JavaScript 取用特定 element 時使用 CSS selector 可能造成元件選擇錯誤,如:
    1
    2
    3
    4
    5
    6
    7
    
    <script>
     // 使用 class 取用元件時,可能因命名衝突選到錯誤的元件
     const sectionElement = document.querySelector('.section');
    </script>
    
    <div class="section">Another element</div>
    <div class="section">Target element</div>
    
  2. 樣式套用問題: 不同的 element 因為設計錯誤而使用了相同的 id 或 class 也可能造成樣式套用錯誤,如下範例,.right 在第一個 div 元件中代表著文字應該靠右對齊,而在第二個 div 元件中代表著靠右浮動,但由於使用了相同的 class 名稱造成命名衝突,所以導致樣式套用的問題。
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    <style>
     /* style for element A */
     .right {
       text-align: right;
     }
     /* style for element B */
     .right {
       float: right;
     }
    </style>
    
    <div class="right">Element A</div>
    <div class="right">Element B</div>
    

Shadow DOM 能有效封裝頁面元素的 id 和 class,防止不同元件之間的命名衝突。

舉例來說,元件 A 定義了一個class為 .header 的元素,元件 B 也定義了一個同名 class。

使用Shadow DOM後,這兩個 class 不會互相影響:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<html>
<body>
<custom-element id="element-a">
  #shadow-root (open)
  ┆  <style>
  ┆    .header {
  ┆      /*
  ┆        元件A專用的樣式,
  ┆        不會影響 host dom 以及元件B
  ┆       */
  ┆      background-color: yellow;
  ┆    }
  ┆  </style>
  ┆  <div class="header">元件A</div>
</custom-element>

<custom-element id="element-b">
  #shadow-root (open)
  ┆ <style>
  ┆   .header {
  ┆     /*
  ┆       元件B專用的樣式,
  ┆       不會影響 host dom 以及元件A
  ┆      */
  ┆     background-color: red;
  ┆   }
  ┆ </style>
  ┆ <div class="header">元件B</div>
</custom-element>

</body>
</html>

HTML templates

HTML templates 提供了 <template> 以及 <slot> 兩個標籤提供一個方式來注入 HTML 到 Custom Elements 中。

template 標籤

使用 <template> 標籤,我們可以定義一個在元件實體中會重覆出現的 html 結構,例如在一個輸入信用卡號的 Custom Element 中,會重複出現四個卡號的 input 元件、一個後三碼 input 元件,還有兩個有效期限的輸入欄位:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<html>
<body>
  <template id="card-input-template">
    <style>
      .row {
        display: flex;
        flex-direction: row;
        gap: 8px;
      }
    </style>
    <div class="wrapper">
      <div class="row">
        <label>卡號:</label>
        <input type="text" maxlength="4" />
        <input type="text" maxlength="4" />
        <input type="text" maxlength="4" />
        <input type="text" maxlength="4" />
      </div>
      <div class="row">
        <label>後三碼:</label>
        <input type="text" maxlength="3" />
      </div>
      <div class="row">
        <label>有效期限:</label>
        <input type="text" maxlength="2" />月 /
        <input type="text" maxlength="2" />年
      </div>
    </div>
  </template>
  <script>
  (function() {
    'use strict';

    window.customElements.define('card-input', class extends HTMLElement {
      constructor() {
        super();
        let shadowRoot = this.attachShadow({mode: 'open'});
      }

      connectedCallback() {
        // 使用 template 來建立 DOM tree
        const t = document.querySelector('#card-input-template');
        const instance = t.content.cloneNode(true);
        shadowRoot.appendChild(instance);
      }
    });
  }) ();
  </script>

  <!-- 使用 card-input 元件 -->
  <label>請輸入您的第一張信用卡:</label>
  <card-input></card-input>

  <!-- Custom element 使用定義好的 template,使得程式碼變得簡潔 -->
  <label>請輸入您的第二張信用卡:</label>
  <card-input></card-input>

</body>
</html>

slot 標籤

<slot> 標籤則方便我們使用在需要動態注入 html 元件或 text node 的情況,例如我們實作了一個紅色邊框元件 <red-frame>,可以在任意 html 元件上都加上紅色框框,範例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
  <style>
    #frame {
      border: 4px solid red;
    }
  </style>
  <div id="frame">
    <slot></slot> <!-- 在 template 中使用 slot 來接收外部注入的內容 -->
  </div>
</template>

<script>
  (function() {
    'use strict';
    window.customElements.define('red-frame', class extends HTMLElement {
      // ...省略
    });
  }) ();
</script>

<red-frame>
  <slot>
    <!-- img 元件會注入到 red-frame 中並套用紅色邊框 -->
    <img src="a.jpg" />
  </slot>
</red-frame>

複習

今天我們提到了關於 Web Components 的介紹,並提到了關於 Web Components 的三個核心功能: Shadow DOM 、 Custom Elements,另外還有 HTML Template。

除了可以封裝我們辛苦寫好的 UI 元件方便後續可以重複使用外,也介紹了使用 Shadow DOM 可以避免不同元間之間樣式污染還有因為命名衝突所帶來的問題。

最後介紹了透過 template 來實作 html 樣版設計,以及使用 slot 來動態注入 html 元件到 custom element 中,增加 custom element 的靈活度,後續我們也會再透過實際的一個設計案例來帶大家從無到有實作一個 Web Components,希望透過 Web Components 的封裝,可以讓大家在前端設計與開發上都更輕鬆 😊