使用純粹的JS構建 Web Component

Icarus發表於2017-12-15

原文連結:https://ayushgp.github.io/html-web-components-using-vanilla-js

譯者:阿里雲 - 也樹

Web Component 出現有一陣子了。 Google 費了很大力氣去推動它更廣泛的應用,但是除 Opera 和 Chrome 以外的多數主流瀏覽器對它的支援仍然不夠理想。

但是通過 polyfill,你可以從現在開始構建你自己的 Web Component,你可以在這裡找到相關支援:www.webcomponents.org/polyfills

在這篇文章中,我會演示如何建立帶有樣式,擁有互動功能並且在各自檔案中優雅組織的 HTML 標籤。

介紹

Web Component 是一系列 web 平臺的 API,它們可以允許你建立全新可定製、可重用並且封裝的 HTML 標籤,從而在普通網頁及 web 應用中使用。

定製的元件基於 Web Component 標準構建,可以在現在瀏覽器上使用,也可以和任意與 HTML 互動的 JavaScript 庫和框架配合使用。

用於支援 Web Component 的特性正逐漸加入 HTML 和 DOM 的規範,web 開發者使用封裝好樣式和定製行為的新元素來擴充 HTML 會變得輕而易舉。

它賦予了僅僅使用純粹的JS/HTML/CSS就可以建立可重用元件的能力。如果 HTML 不能滿足需求,我們可以建立一個可以滿足需求的 Web Component。

舉個例子,你的使用者資料和一個 ID 有關,你希望有一個可以填入使用者 ID 並且可以獲取相應資料的元件。HTML 可能是下面這個樣子:

<user-card user-id="1"></user-card>

複製程式碼

這是一個 Web Component 最基本的應用。下面的教程將會聚焦在如何構建這個使用者卡片元件。

Web Component 的四個核心概念

HTML 和 DOM 標準定義了四種新的標準來幫助定義 Web Component。這些標準如下:

  1. 定製元素(Custom Elements): web 開發者可以通過定製元素建立新的 HTML 標籤、增強已有的 HTML 標籤或是二次開發其它開發者已經完成的元件。這個 API 是 Web Component 的基石。

  2. HTML 模板(HTML Templates): HTML 模板定義了新的元素,描述一個基於 DOM 標準用於客戶端模板的途徑。模板允許你宣告標記片段,它們可以被解析為 HTML。這些片段在頁面開始載入時不會被用到,之後執行時會被例項化。

  3. Shadow DOM: Shadow DOM 被設計為構建基於元件的應用的一個工具。它可以解決 web 開發的一些常見問題,比如允許你把元件的 DOM 和作用域隔離開,並且簡化 CSS 等等。

  4. HTML 引用(HTML Imports): HTML 模板(HTML Templates)允許你建立新的模板,同樣的,HTML 引用(HTML imports)允許你從不同的檔案中引入這些模板。通過獨立的HTML檔案管理元件,可以幫助你更好的組織程式碼。

定義定製元素

我們首先需要宣告一個類,定義元素如何表現。這個類需要繼承 HTMLElement 類,但讓我們先繞過這部分,先來討論定製元素的生命週期方法。你可以使用下面的生命週期回撥函式:

  • connectedCallback — 每當元素插入 DOM 時被觸發。

  • disconnectedCallback — 每當元素從 DOM 中移除時被觸發。

  • attributeChangedCallback — 當元素上的屬性被新增、移除、更新或取代時被觸發。

UserCard 資料夾下建立 UserCard.js:

class UserCard extends HTMLElement {
  constructor() {
    super();
    this.addEventListener('click', e => {
      this.toggleCard();
    });
  }

  toggleCard() {
    console.log("Element was clicked!");
  }
}

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

複製程式碼

這個例子裡我們已經建立了一個定義了定製元素行為的類。customElements.define('user-card', UserCard); 函式呼叫告知 DOM 我們已經建立了一個新的定製元素叫 user-card,它的行為被 UserCard 類定義。現在可以在我們的 HTML 裡使用 user-card 元素了。

我們會用到 https://jsonplaceholder.typicode.com/ 的 API 來建立我們的使用者卡片。下面是資料的樣例:

{
  id: 1,
  name: "Leanne Graham",
  username: "Bret",
  email: "Sincere@april.biz",
  address: {
    street: "Kulas Light",
    suite: "Apt. 556",
    city: "Gwenborough",
    zipcode: "92998-3874",
    geo: {
      lat: "-37.3159",
      lng: "81.1496"
    }
  },
  phone: "1-770-736-8031 x56442",
  website: "hildegard.org"
}

複製程式碼

建立模板

現在,讓我們建立一個將在螢幕上渲染的模板。建立一個名為 UserCard.html 的新檔案,內容如下:

<template id="user-card-template">
  <div>
    <h2>
      <span></span> (
      <span></span>)
    </h2>
    <p>Website: <a></a></p>
    <div>
      <p></p>
    </div>
    <button class="card__details-btn">More Details</button>
  </div>
</template>
<script src="/UserCard/UserCard.js"></script>

複製程式碼

注意:我們在類名前加了一個 card__ 字首。在較早版本的瀏覽器中,我們不能使用 shadow DOM 來隔離元件 DOM。這樣當我們為元件編寫樣式時,可以避免意外的樣式覆蓋。

編寫樣式

我們建立好了卡片的模板,現在來用 CSS 裝飾它。建立一個 UserCard.css 檔案,內容如下:

.card__user-card-container {
  text-align: center;
  display: inline-block;
  border-radius: 5px;
  border: 1px solid grey;
  font-family: Helvetica;
  margin: 3px;
  width: 30%;
}

.card__user-card-container:hover {
  box-shadow: 3px 3px 3px;
}

.card__hidden-content {
  display: none;
}

.card__details-btn {
  background-color: #dedede;
  padding: 6px;
  margin-bottom: 8px;
}

複製程式碼

現在,在 UserCard.html 檔案的最前面引入這個 CSS 檔案:

<link rel="stylesheet" href="/UserCard/UserCard.css">

複製程式碼

樣式已經就緒,接下來可以繼續完善我們元件的功能。

connectedCallback

現在我們需要定義建立元素並且新增到 DOM 中會發生什麼。注意這裡 constructorconnectedCallback 方法的區別。

constructor 方法是元素被例項化時呼叫,而 connectedCallback 方法是每次元素插入 DOM 時被呼叫。connectedCallback 方法在執行初始化程式碼時是很有用的,比如獲取資料或渲染。

小貼士: 在 UserCard.js 的頂部,定義一個常量 currentDocument。它在被引入的 HTML 指令碼中是必要的,允許這些指令碼有途徑操作引入模板的 DOM。像下面這樣定義:

const currentDocument = document.currentScript.ownerDocument;

複製程式碼

接下來定義我們的 connectedCallback 方法:

// 元素插入 DOM 時呼叫
connectedCallback() {
  const shadowRoot = this.attachShadow({mode: 'open'});

  // 選取模板並且克隆它。最終將克隆後的節點新增到 shadowDOM 的根節點。
  // 當前文件需要被定義從而獲取引入 HTML 的 DOM 許可權。
  const template = currentDocument.querySelector('#user-card-template');
  const instance = template.content.cloneNode(true);
  shadowRoot.appendChild(instance);

  // 從元素中選取 user-id 屬性
  // 注意我們要像這樣指定卡片: 
  // <user-card user-id="1"></user-card>
  const userId = this.getAttribute('user-id');

  // 根據 user ID 獲取資料,並且使用返回的資料渲染
  fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
      .then((response) => response.text())
      .then((responseText) => {
          this.render(JSON.parse(responseText));
      })
      .catch((error) => {
          console.error(error);
      });
}

複製程式碼

渲染使用者資料

我們已經定義好了 connectedCallback 方法,並且把克隆好的模板繫結到了 shadow root 上。現在我們需要填充模板內容,然後在 fetch 方法獲取資料後觸發 render 方法。下面來編寫 rendertoggleCard 方法。

render(userData) {
  // 使用操作 DOM 的 API 來填充卡片的不同區域
  // 元件的所有元素都存在於 shadow dom 中,所以我們使用了 this.shadowRoot 這個屬性來獲取 DOM
  // DOM 只可以在這個子樹種被查詢到
  this.shadowRoot.querySelector('.card__full-name').innerHTML = userData.name;
  this.shadowRoot.querySelector('.card__user-name').innerHTML = userData.username;
  this.shadowRoot.querySelector('.card__website').innerHTML = userData.website;
  this.shadowRoot.querySelector('.card__address').innerHTML = `<h4>Address</h4>
    ${userData.address.suite}, <br />
    ${userData.address.street},<br />
    ${userData.address.city},<br />
    Zipcode: ${userData.address.zipcode}`
}

toggleCard() {
  let elem = this.shadowRoot.querySelector('.card__hidden-content');
  let btn = this.shadowRoot.querySelector('.card__details-btn');
  btn.innerHTML = elem.style.display == 'none' ? 'Less Details' : 'More Details';
  elem.style.display = elem.style.display == 'none' ? 'block' : 'none';
}

複製程式碼

既然元件已經完成,我們就可以把它用在任意專案中了。為了繼續教程,我們需要建立一個 index.html 檔案,然後寫入下面的程式碼:

<html>

<head>
  <title>Web Component</title>
</head>

<body>
  <user-card user-id="1"></user-card>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/1.0.14/webcomponents-hi.js"></script>
  <link rel="import" href="./UserCard/UserCard.html">
</body>

</html>

複製程式碼

因為並不是所有瀏覽器都支援 Web Component,我們需要引入 webcomponents.js 這個檔案。注意我們用到 HTML 引用語句來引入我們的元件。

為了執行這些程式碼,你需要建立一個靜態檔案伺服器。如果你不清楚如何建立,你可以使用像 static-server 或者 json-server 這樣的簡易靜態服務。教程裡,我們安裝 static-server:

$ npm install -g static-server

複製程式碼

接著在你的專案目錄下,使用下面的命令執行伺服器:

$ static-server

複製程式碼

開啟你的瀏覽器並訪問localhost:3000,你就可以看到我們剛剛建立的元件了。

小貼士和技巧

還有很多關於 Web Component 的東西沒有在這篇短文中寫到,我想簡單的陳述一些開發 Web Component 的小貼士和技巧。

元件的命名

  • 定製元素的名稱必須包含一個短橫線。所以 <my-tabs><my-amazing-website> 是合法的名稱, 而<foo><foo_bar> 不行。

  • 在 HTML 新增新標籤時需要確保向前相容,不能重複註冊同一個標籤。

  • 定製元素標籤不能是自閉合的,因為 HTML 只允許一部分元素可以自閉合。需要寫成像 <app-drawer></app-drawer> 這樣的閉合標籤形式。

擴充元件

建立元件時可以使用繼承的方式。舉個例子,如果想要為兩種不同的使用者建立一個 UserCard,你可以先建立一個基本的 UserCard 然後將它擴充為兩種特定的使用者卡片。想要了解更多元件繼承的知識,可以檢視Google web developers’ article

Lifecycle Callbacks生命週期回撥函式

我們建立了當元素加入 DOM 後自動觸發的 connectedCallback 方法。我們同樣有元素從 DOM 中移除時觸發的 disconnectedCallback 方法。 attributesChangedCallback(attribute, oldval, newval)方法會在我們改變定製元件的屬性時被觸發。

元件元素是類的例項

既然元件元素是類的例項,就可以在這些類中定義公用方法。這些公用方法可以用來允許其它定製元件/指令碼來和這些元件產生互動,而不是隻能改變這些元件的屬性。

定義私有方法

可以通過多種方式定義私有方法。我傾向於使用(立即執行函式),因為它們易寫和易理解。舉個例子,如果你建立的元件有非常複雜的內部功能,你可以像下面這樣做:

(function() {

  // 使用第一個self引數來定義私有函式
  // 當呼叫這些函式時,從類中傳遞引數
  function _privateFunc(self, otherArgs) { ... }

  // 現在函式只可以在你的類的作用域中可用
  class MyComponent extends HTMLElement {
    ...

    // 定義下面這樣的函式可以讓你有途徑和這個元素互動
    doSomething() {
      ...
      _privateFunc(this, args)
    }
    ...
  }

  customElements.define('my-component', MyComponent);
})()

複製程式碼

凍結類

為了防止新的屬性被新增,需要凍結你的類。這樣可以防止類的已有屬性被移除,或者已有屬性的可列舉、可配置或可寫屬性被改變,同樣也可以防止原型被修改。你可以使用下面的方法:

class MyComponent extends HTMLElement { ... }
const FrozenMyComponent = Object.freeze(MyComponent);
customElements.define('my-component', FrozenMyComponent);

複製程式碼

注意: 凍結類會阻止你在執行時新增補丁並且會讓你的程式碼難以除錯。

結論

這篇關於 Web Component 的教程作用非常有限。這可以部分歸咎於對 Web Component 的影響很大的 React。我希望這篇文章可以提供給你足夠的資訊來讓你嘗試不新增任何依賴來構建自己的定製元件。你可以在 定製元件 API 規範(Custom components API spec) 找到更多關於 Web Component 的資訊。

你可以在這裡閱讀第二部分的教程:使用純粹的JS構建 Web Component - Part 2!

相關文章