使用純粹的JS構建 Web Component
原文鏈接:https://ayushgp.github.io/html-web-components-using-vanilla-js
譯者:阿里雲 - 也樹
Web Component 出現有一陣子了。 Google 費了很大力氣去推動它更廣泛的應用,但是除 Opera 和 Chrome 以外的多數主流瀏覽器對它的支持仍然不夠理想。
但是通過 polyfill,你可以從現在開始構建你自己的 Web Component,你可以在這裡找到相關支持:https://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。這些標準如下:
- 定製元素(Custom Elements): web 開發者可以通過定製元素創建新的 HTML 標籤、增強已有的 HTML 標籤或是二次開發其它開發者已經完成的組件。這個 API 是 Web Component 的基石。
- HTML 模板(HTML Templates): HTML 模板定義了新的元素,描述一個基於 DOM 標準用於客戶端模板的途徑。模板允許你聲明標記片段,它們可以被解析為 HTML。這些片段在頁面開始載入時不會被用到,之後運行時會被實例化。
- Shadow DOM: Shadow DOM 被設計為構建基於組件的應用的一個工具。它可以解決 web 開發的一些常見問題,比如允許你把組件的 DOM 和作用域隔離開,並且簡化 CSS 等等。
- 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 中會發生什麼。注意這裡 constructor
和 connectedCallback
方法的區別。
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
方法。下面來編寫 render
和 toggleCard
方法。
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!
推薦閱讀: