從 Web 元件到 Lit 元素

1. 簡介

上次更新時間:2021 年 8 月 10 日

網頁元件

網頁元件是一組網路平台 API,可讓您建立新的自訂、可重複使用及封裝的 HTML 標記,以便在網頁和網頁應用程式中使用。根據 Web Component 標準建立的自訂元件和小工具,也能在各種新式瀏覽器上運作,並搭配任何支援 HTML 的 JavaScript 程式庫或架構使用。

什麼是 Lit?

Lit 是一種簡單的程式庫,可用來建構快速、輕量、可搭配任何架構,或完全沒有架構的網路元件。您可以利用 Lit 建構共用元件、應用程式、設計系統等。

Lit 提供的 API 可簡化常見的網頁元件工作,例如管理屬性、屬性和轉譯。

課程內容

  • 什麼是網頁元件
  • Web 元件的概念
  • 如何建立網頁元件
  • 什麼是 lit-html 和 LitElement
  • 適用於網頁元件的 Lit 功能

建構項目

  • 基本喜歡 / 不喜歡 Web 元件
  • 喜歡 / 不喜歡的 Lit 型 Web 元件

軟硬體需求

  • 任何更新版本瀏覽器 (Chrome、Safari、Firefox、Chromium Edge)。網頁元件適用於所有新式瀏覽器和 polyfill,可能適用於 Microsoft Internet Explorer 11 和非 Chromium Edge Microsoft Edge。
  • 瞭解 HTML、CSS、JavaScript 和 Chrome 開發人員工具

2. 開始設定與探索遊樂場

存取程式碼

本程式碼研究室中會提供 Lit Playground 的連結,如下所示:

Playground 是一個程式碼沙箱,可完全在瀏覽器中執行。這個程式庫可以編譯及執行 TypeScript 和 JavaScript 檔案,也可以自動解析匯入節點模組。例如:

// before
import './my-file.js';
import 'lit';

// after
import './my-file.js';
import 'https://unpkg.com/lit?module';

您可以在 Lit Playground 中進行整個教學課程,使用這些查核點作為起點。如果您使用 VS Code,就可以透過這些檢查點下載任何步驟的起始程式碼,並使用這些檢查點檢查您的工作。

探索燈光效果的遊樂場 UI

檔案選取器分頁列標示為第 1 節、程式碼編輯部分 (第 2 節)、輸出預覽畫面 (第 3 節) 和預覽重新載入按鈕 (第 4 節)

Lit Playground UI 螢幕截圖醒目顯示您將在這個程式碼研究室中使用的部分。

  1. 檔案選取器。注意加號按鈕...
  2. 檔案編輯器。
  3. 程式碼預覽。
  4. 「重新載入」按鈕。
  5. 下載按鈕。

VS Code 設定 (進階)

使用這項 VS Code 設定的優點如下:

  • 範本類型檢查
  • 範本智慧功能和自動完成建議

如果您已安裝 NPM、VS Code (含 lit-plugin 外掛程式),並瞭解如何使用該環境,只要下載並按照下列步驟啟動這些專案即可:

  • 按下「下載」按鈕
  • 將 tar 檔案的內容解壓縮至目錄
  • 安裝可解析裸機模組指定碼的開發伺服器 (Lint 團隊建議採用 @web/dev-server)
  • 執行開發伺服器並開啟瀏覽器 (如果您使用的是 @web/dev-server,則可以使用 npx web-dev-server --node-resolve --watch --open)
    • 如果您要使用 package.json 範例,請使用 npm run serve

3. 定義自訂元素

自訂元素

網頁元件是 4 組原生網路 API 的集合。這些因素包括:

  • ES 模組
  • 自訂元素
  • 陰影 DOM
  • HTML 範本

您已使用 ES 模組規格,透過 <script type="module"> 建立 JavaScript 模組,並採用匯入和匯出內容並載入至網頁中。

定義自訂元素

自訂元素規格可讓使用者使用 JavaScript 定義自己的 HTML 元素。名稱必須包含連字號 (-),以便與原生瀏覽器元素有所區別。清除 index.js 檔案並定義自訂元素類別:

index.js

class RatingElement extends HTMLElement {}

customElements.define('rating-element', RatingElement);

定義自訂元素的方法是將擴充 HTMLElement 的類別與連字號標記名稱建立關聯。呼叫 customElements.define 會指示瀏覽器將 RatingElement 類別與 tagName ‘rating-element' 建立關聯。這表示文件中所有名為 <rating-element> 的元素都會與這個類別建立關聯。

<rating-element> 放入文件內文,並查看轉譯內容。

index.html

<body>
 <rating-element></rating-element>
</body>

現在,只要檢視輸出內容,即可發現未轉譯任何內容。這是正常情況,因為您尚未指定瀏覽器如何顯示 <rating-element>。您可以在 Chrome 開發人員工具中選取 <rating-element>,確認自訂元素定義是否成功元素選取器後,在控制台中呼叫:

$0.constructor

輸出內容應會如下所示:

class RatingElement extends HTMLElement {}

自訂元素生命週期

自訂元素附有一組生命週期掛鉤。這些因素包括:

  • constructor
  • connectedCallback
  • disconnectedCallback
  • attributeChangedCallback
  • adoptedCallback

系統會在元素初次建立時呼叫 constructor,例如呼叫 document.createElement(‘rating-element')new RatingElement()。建構函式很適合用來設定元素,但一般來說,在建構函式「啟動」元素的建構函式中進行 DOM 操控的做法錯誤,是不理想的做法效能因素

當自訂元素附加至 DOM 時,系統會呼叫 connectedCallback。這通常是初始進行 DOM 操作的情況。

從 DOM 移除自訂元素後,會呼叫 disconnectedCallback

使用者指定的屬性有任何變更時,系統就會呼叫 attributeChangedCallback(attrName, oldValue, newValue)

透過 adoptNode (例如在 HTMLTemplateElement 中) 從另一個 documentFragment 採用自訂元素至主要文件中時,系統會呼叫 adoptedCallback

算繪 DOM

現在,請返回自訂元素,並將某些 DOM 與該元素建立關聯。在元素附加至 DOM 時設定元素內容:

index.js

class RatingElement extends HTMLElement {
 constructor() {
   super();
   this.rating = 0;
 }
 connectedCallback() {
   this.innerHTML = `
     <style>
       rating-element {
         display: inline-flex;
         align-items: center;
       }
       rating-element button {
         background: transparent;
         border: none;
         cursor: pointer;
       }
     </style>
     <button class="thumb_down" >
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
     </button>
     <span class="rating">${this.rating}</span>
     <button class="thumb_up">
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
     </button>
   `;
 }
}

customElements.define('rating-element', RatingElement);

constructor 中,您將名為 rating 的執行個體屬性儲存在元素上。在 connectedCallback 中,您要將 DOM 子項新增至 <rating-element>,即可顯示目前評分,以及「喜歡」和「不喜歡」按鈕。

4. 陰影 DOM

為什麼要使用 Shadow DOM?

在上一步中,您會注意到插入樣式標記中的選取器,包括同時選取網頁上的任何評分元素和按鈕。這可能會導致樣式從元素外流,並選取您不想設定樣式的其他節點。此外,這個自訂元素外的其他樣式,可能會不小心設定自訂元素中節點的樣式。例如,嘗試在主要文件的標題中加入樣式標記:

index.html

<!DOCTYPE html>
<html>
 <head>
   <script src="./index.js" type="module"></script>
   <style>
     span {
       border: 1px solid red;
     }
   </style>
 </head>
 <body>
   <rating-element></rating-element>
 </body>
</html>

輸出內容應在評分的跨度周圍有紅色邊框方塊。這是一個小案例,但缺少 DOM 封裝可能會導致更複雜的應用程式發生問題。這時 Shadow DOM 就能派上用場

連接陰影根

將陰影根附加至元素,並在該根層級轉譯 DOM:

index.js

class RatingElement extends HTMLElement {
 constructor() {
   super();
   this.rating = 0;
 }
 connectedCallback() {
   const shadowRoot = this.attachShadow({mode: 'open'});

   shadowRoot.innerHTML = `
     <style>
       :host {
         display: inline-flex;
         align-items: center;
       }
       button {
         background: transparent;
         border: none;
         cursor: pointer;
       }
     </style>
     <button class="thumb_down" >
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
     </button>
     <span class="rating">${this.rating}</span>
     <button class="thumb_up">
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
     </button>
   `;
 }
}

customElements.define('rating-element', RatingElement);

重新整理頁面時,您會發現主要文件中的樣式無法再選取陰影根目錄中的節點。

這是如何辦到的?在 connectedCallback 中呼叫 this.attachShadow,後者會將陰影根附加至元素。open 模式表示陰影內容可供檢查,並且透過 this.shadowRoot 存取陰影根目錄。請同時查看 Chrome 檢查器中的 Web 元件:

Chrome 檢查器中的群樹。其中有 <rating-element>將 a#shadow-root (開啟) 設為其子項,以及之前由該 shadowroot 內部的 DOM。

現在,您應該會看到保存內容的可展開陰影根目錄。該陰影根層級中的所有項目都稱為 Shadow DOM。如果您在 Chrome 開發人員工具中選取評分元素,並呼叫 $0.children,會發現該元素不會傳回子項。這是因為 Shadow DOM 不算是同一個 DOM 樹狀結構與直接子項的一部分,而是「陰影樹」的一部分。

淺色 DOM

實驗:新增節點做為 <rating-element> 的直接子項:

index.html

<rating-element>
 <div>
   This is the light DOM!
 </div>
</rating-element>

重新整理頁面後,就會發現這個自訂元素的「Light DOM」內未顯示這個新的 DOM 節點。這是因為 Shadow DOM 會透過 <slot> 元素,控制 Light DOM 節點如何投影至陰影區塊的功能。

5. HTML 範本

使用範本的好處

使用不含清理作業的 innerHTML 和範本常值字串可能導致指令碼插入出現安全性問題。雖然過去的方法包含使用 DocumentFragment,但也有一些問題,例如範本定義時載入圖片、執行指令碼,以及造成重複使用的障礙。這就是 <template> 元素的來源。範本提供 inert DOM,這是複製節點的高效能方法,以及可重複使用的範本。

使用範本

接下來,將元件轉換到使用 HTML 範本:

index.html

<body>
 <template id="rating-element-template">
   <style>
     :host {
       display: inline-flex;
       align-items: center;
     }
     button {
       background: transparent;
       border: none;
       cursor: pointer;
     }
   </style>
   <button class="thumb_down" >
     <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
   </button>
   <span class="rating"></span>
   <button class="thumb_up">
     <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
   </button>
 </template>

 <rating-element>
   <div>
     This is the light DOM!
   </div>
 </rating-element>
</body>

在這裡,您已將 DOM 內容移至主要文件 DOM 中的範本標記。現在重構自訂元素定義:

index.js

class RatingElement extends HTMLElement {
 constructor() {
   super();
   this.rating = 0;
 }
 connectedCallback() {
   const shadowRoot = this.attachShadow({mode: 'open'});
   const templateContent = document.getElementById('rating-element-template').content;
   const clonedContent = templateContent.cloneNode(true);
   shadowRoot.appendChild(clonedContent);

   this.shadowRoot.querySelector('.rating').innerText = this.rating;
 }
}

customElements.define('rating-element', RatingElement);

如要使用這個範本元素,您必須查詢範本、取得其內容,然後使用 templateContent.cloneNode 複製這些節點,其中 true 引數會執行深層複製作業。接著,您要使用資料來初始化群數。

恭喜,您現已擁有 Web 元件!很抱歉,這個專區尚未執行任何動作,接下來請新增一些功能。

6. 新增功能

屬性繫結

如要在評分元素上設定評分,目前只能建構元素、在物件上設定 rating 屬性,然後將它放到網頁上。不過,這並不是原生 HTML 元素通常的運作方式。原生 HTML 元素通常會隨著屬性和屬性變更而更新。

加入下列程式碼,讓自訂元素在 rating 屬性變更時更新檢視畫面:

index.js

constructor() {
  super();
  this._rating = 0;
}

set rating(value) {
  this._rating = value;
  if (!this.shadowRoot) {
    return;
  }

  const ratingEl = this.shadowRoot.querySelector('.rating');
  if (ratingEl) {
    ratingEl.innerText = this._rating;
  }
}

get rating() {
  return this._rating;
}

您將針對評分屬性新增 setter 和 getter,然後更新評分元素的文字 (如有)。也就是說,如果為元素設定評分屬性,檢視畫面就會更新:直接在開發人員工具控制台中完成簡單的測試!

屬性繫結

現在,請在屬性變更時更新檢視畫面。這類似於設定 <input value="newValue"> 時更新其檢視畫面的輸入。幸好,Web 元件生命週期包含 attributeChangedCallback。如要更新評分,請加入以下幾行:

index.js

static get observedAttributes() {
 return ['rating'];
}

attributeChangedCallback(attributeName, oldValue, newValue) {
 if (attributeName === 'rating') {
   const newRating = Number(newValue);
   this.rating = newRating;
 }
}

為了讓 attributeChangedCallback 觸發,您必須為 RatingElement.observedAttributes which defines the attributes to be observed for changes 設定靜態 getter。接著,請在 DOM 中宣告評分。歡迎體驗:

index.html

<rating-element rating="5"></rating-element>

評分現在應透過宣告更新!

按鈕功能

現在,多半都缺少按鈕功能。這個元件的行為應讓使用者能夠提供上下投票,並向使用者提供視覺回饋。雖然您可以使用某些事件監聽器和反映屬性來實作這個項目,不過請先附加以下這行程式碼來更新樣式,提供視覺回饋:

index.html

<style>
...

 :host([vote=up]) .thumb_up {
   fill: green;
 }
  :host([vote=down]) .thumb_down {
   fill: red;
 }
</style>

在 Shadow DOM 中,:host 選取器是指 Shadow Root 所附加的節點或自訂元素。在此案例中,如果 vote 屬性為 "up",則「喜歡」按鈕會變為綠色,但如果 vote"down", then it will turn the thumb-down button red,現在,請透過類似實作 rating 的方式為 vote 建立反映屬性 / 屬性,藉此實作此邏輯。從屬性 setter 和 getter 開始:

index.js

constructor() {
  super();
  this._rating = 0;
  this._vote = null;
}

set vote(newValue) {
  const oldValue = this._vote;
  if (newValue === oldValue) {
    return;
  }
  if (newValue === 'up') {
    if (oldValue === 'down') {
      this.rating += 2;
    } else {
      this.rating += 1;
    }
  } else if (newValue === 'down') {
    if (oldValue === 'up') {
      this.rating -= 2;
    } else {
      this.rating -= 1;
    }
  }
  this._vote = newValue;
  this.setAttribute('vote', newValue);
}

get vote() {
  return this._vote;
}

您在 constructor 中使用 null 初始化 _vote 執行個體屬性,並在 setter 中檢查新值是否不同。如果需要,請按照實際情況調整評分,此外,最重要的是將 vote 屬性反映給含有 this.setAttribute 的主機。

接下來,請設定屬性繫結:

index.js

static get observedAttributes() {
  return ['rating', 'vote'];
}

attributeChangedCallback(attributeName, oldValue, newValue) {
  if (attributeName === 'rating') {
    const newRating = Number(newValue);

    this.rating = newRating;
  } else if (attributeName === 'vote') {
    this.vote = newValue;
  }
}

同樣,這與您使用 rating 屬性繫結時執行的程序相同。您在 observedAttributes 中加入 vote,並在 attributeChangedCallback 中設定 vote 屬性。最後,加入一些點擊事件監聽器,提供按鈕功能!

index.js

constructor() {
 super();
 this._rating = 0;
 this._vote = null;
 this._boundOnUpClick = this._onUpClick.bind(this);
 this._boundOnDownClick = this._onDownClick.bind(this);
}

connectedCallback() {
  ...
  this.shadowRoot.querySelector('.thumb_up')
    .addEventListener('click', this._boundOnUpClick);
  this.shadowRoot.querySelector('.thumb_down')
    .addEventListener('click', this._boundOnDownClick);
}

disconnectedCallback() {
  this.shadowRoot.querySelector('.thumb_up')
    .removeEventListener('click', this._boundOnUpClick);
  this.shadowRoot.querySelector('.thumb_down')
    .removeEventListener('click', this._boundOnDownClick);
}

_onUpClick() {
  this.vote = 'up';
}

_onDownClick() {
  this.vote = 'down';
}

constructor 中,您可以將某些點擊事件監聽器繫結至元素,並保留參照。在 connectedCallback 中,您可以監聽按鈕的點擊事件。在 disconnectedCallback 中,您會清理這些事件監聽器,而在點擊事件監聽器本身,您會正確設定 vote

恭喜,您已經擁有功能完整的 Web Component!試著點選一些按鈕!但目前的問題是,我的 JS 檔案現在佔了 96 行,HTML 檔案有 43 行,程式碼就相當冗長,無法滿足這類簡易元件的需求。這時 Google 的 Lit 專案就派上用場了!

7. Lit-html

程式碼查核點

為什麼要使用 lit-html

首先,<template> 標記既實用又效能,但不會與元件的邏輯包裝在一起,因此很難將範本和其他邏輯一起發布。此外,範本元素在本質上的使用方式與命令式程式碼有關,與宣告的程式設計模式相比,在多數情況下,這類程式碼的可讀性較低。

這時 lit-html 就能派上用場!Lit html 是 Lit 的轉譯系統,可讓您在 JavaScript 中編寫 HTML 範本,然後有效率地呈現和重新轉譯這些範本與資料以建立和更新 DOM。這與常見的 JSX 和 VDOM 程式庫類似,但前者是直接在瀏覽器中執行,在許多情況下也能更有效率。

使用 Lit HTML

接下來,請遷移原生 Web 元件 rating-element,以便使用使用標記範本常值的 Lit 範本,這些函式會將範本字串做為具有特殊語法的引數。Lit 接著會使用內部的範本元素來快速轉譯,並提供部分安全性作業的防毒功能。首先,在網頁元件中新增 render() 方法,將 index.html 中的 <template> 遷移至 Lit 範本:

index.js

// Dont forget to import from Lit!
import {render, html} from 'lit';

class RatingElement extends HTMLElement {
  ...
  render() {
    if (!this.shadowRoot) {
      return;
    }

    const template = html`
      <style>
        :host {
          display: inline-flex;
          align-items: center;
        }
        button {
          background: transparent;
          border: none;
          cursor: pointer;
        }

       :host([vote=up]) .thumb_up {
         fill: green;
       }

       :host([vote=down]) .thumb_down {
         fill: red;
       }
      </style>
      <button class="thumb_down">
        <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
      </button>
      <span class="rating">${this.rating}</span>
      <button class="thumb_up">
        <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
      </button>`;

    render(template, this.shadowRoot);
  }
}

您也可以從「index.html」中刪除範本。在這個轉譯方法中,您會定義名為 template 的變數,並叫用 html 標記的範本常值函式。您也會發現,您已使用 ${...} 的範本常值內插語法,在 span.rating 元素中執行簡單的資料繫結。這表示您最終不再需要強制更新該節點。此外,您也可以呼叫淺色的 render 方法,將範本同步算繪到陰影根目錄中。

改用宣告式語法

現在您已捨棄 <template> 元素,請重構程式碼,改為呼叫新定義的 render 方法。請先利用光照的事件監聽器繫結,清除事件監聽器程式碼:

index.js

<button
    class="thumb_down"
    @click=${() => {this.vote = 'down'}}>
...
<button
    class="thumb_up"
    @click=${() => {this.vote = 'up'}}>

Lit 範本可以使用 @EVENT_NAME 繫結語法將事件監聽器新增至節點。在此情況下,在此情況下,每次按下這些按鈕時,您都會更新 vote 屬性。

接著,清除 constructorconnectedCallbackdisconnectedCallback 中的事件監聽器初始化程式碼:

index.js

constructor() {
  super();
  this._rating = 0;
  this._vote = null;
}

connectedCallback() {
  this.attachShadow({mode: 'open'});
  this.render();
}

// remove disonnectedCallback and _onUpClick and _onDownClick

您能夠從全部三個回呼中移除點擊事件監聽器邏輯,甚至能完全移除 disconnectedCallback!您也可以從 connectedCallback 中移除所有 DOM 初始化程式碼,讓它看起來更加優雅。此外,您也可以移除 _onUpClick_onDownClick 事件監聽器方法!

最後,請更新屬性 setter 以使用 render 方法,以便在屬性或屬性變更時更新區塊:

index.js

set rating(value) {
  this._rating = value;
  this.render();
}

...

set vote(newValue) {
  const oldValue = this._vote;
  if (newValue === oldValue) {
    return;
  }

  if (newValue === 'up') {
    if (oldValue === 'down') {
      this.rating += 2;
    } else {
      this.rating += 1;
    }
  } else if (newValue === 'down') {
    if (oldValue === 'up') {
      this.rating -= 2;
    } else {
      this.rating -= 1;
    }
  }

  this._vote = newValue;
  this.setAttribute('vote', newValue);
  // add render method
  this.render();
}

在這裡,您可以從 rating setter 中移除 dom 更新邏輯,並從 vote setter 新增對 render 的呼叫。現在,查看繫結和事件監聽器的套用位置,範本更容易閱讀。

重新整理頁面,即可看到可正常運作的評分按鈕,此按鈕在按下認同票數時看起來應該像這樣!

「喜歡」和「不喜歡」評分滑桿,值為 6,拇指綠色的綠色

8. LitElement

為什麼要使用 LitElement

程式碼仍有一些問題。首先,如果變更 vote 屬性或屬性,可能會變更 rating 屬性,導致呼叫 render 兩次。雖然重複呼叫轉譯的功效大同小異,但 javascript VM 仍會花費時間呼叫該功能兩次。其次,新增屬性和屬性的過程相當繁瑣,因為需要大量的樣板程式碼。這時 LitElement 就能派上用場!

LitElement 是 Lit 的基礎類別,可讓您建立可在各種架構和環境中使用的快速輕量網頁元件。接下來,看看變更實作方式以便使用 LitElement,瞭解 rating-element 可以在 rating-element 中有哪些功能!

使用 LitElement

首先,請從 lit 套件匯入 LitElement 基本類別並建立子類別:

index.js

import {LitElement, html, css} from 'lit';

class RatingElement extends LitElement {
// remove connectedCallback()
...

您匯入 LitElement,是 rating-element 的新基礎類別。接下來,請保留 html 匯入作業,最後使用 css,以便我們針對 CSS 數學、範本和其他功能定義 CSS 標記範本常值。

接著,將樣式從算繪方法移到 Lit 的靜態樣式表:

index.js

class RatingElement extends LitElement {
  static get styles() {
    return css`
      :host {
        display: inline-flex;
        align-items: center;
      }
      button {
        background: transparent;
        border: none;
        cursor: pointer;
      }

      :host([vote=up]) .thumb_up {
        fill: green;
      }

      :host([vote=down]) .thumb_down {
        fill: red;
      }
    `;
  }
 ...

這裡大部分的風格都住在 Lit 中。Lit 將採用這些樣式,並使用「可建置的樣式表」等瀏覽器功能來加快轉譯速度,並且視需要在舊版瀏覽器中透過「網頁元件 polyfill」加以傳遞。

生命週期

Lit 在原生 Web 元件回呼之上,導入一組轉譯生命週期回呼方法。當宣告的 Lit 屬性變更時,就會觸發這些回呼。

如要使用這項功能,您必須以靜態方式宣告哪些屬性會觸發轉譯生命週期。

index.js

static get properties() {
  return {
    rating: {
      type: Number,
    },
    vote: {
      type: String,
      reflect: true,
    }
  };
}

// remove observedAttributes() and attributeChangedCallback()
// remove set rating() get rating()

您可以在這裡定義 ratingvote 會觸發 LitElement 的轉譯生命週期,以及定義要用於將字串屬性轉換為屬性的類型。

<user-profile .name=${this.user.name} .age=${this.user.age}>
  ${this.user.family.map(member => html`
        <family-member
             .name=${member.name}
             .relation=${member.relation}>
        </family-member>`)}
</user-profile>

此外,vote 屬性上的 reflect 旗標也會自動更新您在 vote setter 中手動觸發的主要元素 vote 屬性。

您已建立靜態屬性區塊,就可以移除所有屬性和屬性轉譯更新邏輯。這表示您可以移除下列方法:

  • connectedCallback
  • observedAttributes
  • attributeChangedCallback
  • rating (設定器和 getter)
  • vote (setter 和 getter,但保留 setter 中的變更邏輯)

您要保留的內容是 constructor,並新增 willUpdate 生命週期方法:

index.js

constructor() {
  super();
  this.rating = 0;
  this.vote = null;
}

willUpdate(changedProps) {
  if (changedProps.has('vote')) {
    const newValue = this.vote;
    const oldValue = changedProps.get('vote');

    if (newValue === 'up') {
      if (oldValue === 'down') {
        this.rating += 2;
      } else {
        this.rating += 1;
      }
    } else if (newValue === 'down') {
      if (oldValue === 'up') {
        this.rating -= 2;
      } else {
        this.rating -= 1;
      }
    }
  }
}

// remove set vote() and get vote()

在這裡,您只要初始化 ratingvote,然後將 vote setter 邏輯移至 willUpdate 生命週期方法即可。每當有更新的屬性變更時,系統都會在 render 之前呼叫 willUpdate 方法,因為 LitElement 會批次變更屬性,並且讓算繪作業非同步。變更 willUpdate 中的回應式屬性 (例如 this.rating) 不會觸發不必要的 render 生命週期呼叫。

最後,render 是 LitElement 生命週期方法,需要我們「傳回」 Lit 範本:

index.js

render() {
  return html`
    <button
        class="thumb_down"
        @click=${() => {this.vote = 'down'}}>
      <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
    </button>
    <span class="rating">${this.rating}</span>
    <button
        class="thumb_up"
        @click=${() => {this.vote = 'up'}}>
      <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
    </button>`;
}

您不必再檢查陰影根目錄,也不必再呼叫先前從 'lit' 套件匯入的 render 函式。

您的元素現在應該會顯示在預覽畫面中。就只要按一下!

9. 恭喜

恭喜,您已成功從頭開始建構 Web 元件,並將其發展為 LitElement!

Lit 非常小 (小於 5kb 壓縮 + gzip 壓縮),速度超級快,對編寫程式來說十分有趣!您可以讓其他架構使用元件,也可以用該元件建構出完善的應用程式!

現在您已經瞭解 Web 元件的定義、建構方式,以及 Lit 如何更輕鬆地建立 Web 元件!

程式碼查核點

是否要對照我們的最終程式碼?請按這裡來比較。

後續步驟

查看其他程式碼研究室!

其他資訊

社群