Od komponentu internetowego do elementu Lit

1. Wprowadzenie

Ostatnia aktualizacja: 10 sierpnia 2021 r.

Web Components

Komponenty internetowe to zestaw interfejsów API platformy internetowej, które umożliwiają tworzenie nowych, niestandardowych, wielokrotnego użytku i enkapsulowanych tagów HTML do używania na stronach internetowych i w aplikacjach internetowych. Komponenty niestandardowe i widżety oparte na standardach Web Component będą działać w nowoczesnych przeglądarkach i mogą być używane z dowolną biblioteką lub platformą JavaScript, która współpracuje z HTML.

Czym jest Lit

Lit to prosta biblioteka do tworzenia szybkich i lekkich komponentów internetowych, które działają w dowolnej platformie lub bez niej. Za pomocą Lit możesz tworzyć komponenty, aplikacje, systemy projektowe i inne elementy, które można udostępniać.

Lit udostępnia interfejsy API, które upraszczają typowe zadania związane z komponentami internetowymi, takie jak zarządzanie właściwościami, atrybutami i renderowanie.

Czego się nauczysz

  • Czym jest komponent internetowy
  • Koncepcje komponentów internetowych
  • Jak utworzyć komponent internetowy
  • Czym są lit-html i LitElement
  • Co Lit robi ponad komponentem internetowym

Co utworzysz

  • Prosty komponent sieciowy z kciukiem w górę lub w dół
  • Komponent internetowy oparty na Lit z przyciskiem „Lubię”/„Nie lubię”

Czego potrzebujesz

  • Dowolna zaktualizowana nowoczesna przeglądarka (Chrome, Safari, Firefox, Chromium Edge). Komponenty internetowe działają we wszystkich nowoczesnych przeglądarkach, a w przypadku przeglądarek Microsoft Internet Explorer 11 i Microsoft Edge bez Chromium dostępne są polyfille.
  • Znajomość języków HTML, CSS i JavaScript oraz Narzędzi deweloperskich w Chrome.

2. Konfigurowanie i poznawanie Playgroundu

Dostęp do kodu

W tym ćwiczeniu znajdziesz linki do platformy Lit Playground, np. takie:

Plac zabaw to piaskownica kodu, która działa w całości w przeglądarce. Może kompilować i uruchamiać pliki TypeScript i JavaScript, a także automatycznie rozpoznawać importy do modułów węzła, np.

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

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

Cały samouczek możesz przejść w środowisku Lit Playground, używając tych punktów kontrolnych jako punktów wyjścia. Jeśli używasz VS Code, możesz pobrać kod początkowy dla dowolnego kroku, a także sprawdzić swoją pracę za pomocą tych punktów kontrolnych.

Poznawanie interfejsu podświetlonego placu zabaw

Pasek kart selektora plików jest oznaczony jako Sekcja 1, sekcja edycji kodu jako Sekcja 2, podgląd danych wyjściowych jako Sekcja 3, a przycisk ponownego wczytywania podglądu jako Sekcja 4.

Zrzut ekranu interfejsu Lit Playground z wyróżnionymi sekcjami, których będziesz używać w tym ćwiczeniu.

  1. Selektor plików. Zwróć uwagę na przycisk plusa...
  2. Edytor plików.
  3. Podgląd kodu.
  4. Przycisk Załaduj ponownie.
  5. Przycisk pobierania.

Konfiguracja VS Code (zaawansowana)

Oto zalety korzystania z tej konfiguracji VS Code:

  • Sprawdzanie typu szablonu
  • Inteligentne podpowiedzi i autouzupełnianie w szablonach

Jeśli masz już zainstalowane NPM i VS Code (z wtyczką lit-plugin) i wiesz, jak korzystać z tego środowiska, możesz po prostu pobrać te projekty i je uruchomić, wykonując te czynności:

  • Naciśnij przycisk pobierania
  • Wyodrębnij zawartość pliku tar do katalogu.
  • Zainstaluj serwer deweloperski, który może rozwiązywać specyfikatory modułów podstawowych (zespół Lit zaleca @web/dev-server).
  • Uruchom serwer deweloperski i otwórz przeglądarkę (jeśli używasz @web/dev-server, możesz użyć npx web-dev-server --node-resolve --watch --open).
    • Jeśli używasz przykładu package.json, użyj npm run serve

3. Definiowanie elementu niestandardowego

Elementy niestandardowe

Komponenty internetowe to zbiór 4 natywnych interfejsów API. Są to:

  • Moduły ES
  • Elementy niestandardowe
  • Shadow DOM
  • Szablony HTML

Używasz już specyfikacji modułów ES, która umożliwia tworzenie modułów JavaScript z importami i eksportami wczytywanymi na stronę za pomocą tagu <script type="module">.

Definiowanie elementu niestandardowego

Specyfikacja elementów niestandardowych umożliwia użytkownikom definiowanie własnych elementów HTML za pomocą JavaScriptu. Nazwy muszą zawierać łącznik (-), aby odróżnić je od natywnych elementów przeglądarki. Wyczyść plik index.js i zdefiniuj niestandardową klasę elementu:

index.js

class RatingElement extends HTMLElement {}

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

Element niestandardowy jest definiowany przez powiązanie klasy, która rozszerza HTMLElement, z nazwą tagu z łącznikiem. Wywołanie customElements.define informuje przeglądarkę, że klasa RatingElement jest powiązana z nazwą tagu ‘rating-element'. Oznacza to, że każdy element w dokumencie o nazwie <rating-element> będzie powiązany z tą klasą.

Umieść w treści dokumentu znak <rating-element> i sprawdź, co się wyświetli.

index.html

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

Teraz, patrząc na dane wyjściowe, zobaczysz, że nic się nie renderuje. Jest to oczekiwane, ponieważ nie podano przeglądarce informacji o tym, jak renderować znak <rating-element>. Aby sprawdzić, czy definicja elementu niestandardowego została utworzona, kliknij <rating-element> w selektorze elementów Narzędzi deweloperskich w wersji deweloperskiej Chrome i w konsoli wywołaj:

$0.constructor

Wynik powinien wyglądać tak:

class RatingElement extends HTMLElement {}

Cykl życia elementu niestandardowego

Elementy niestandardowe mają zestaw funkcji cyklu życia. Są to:

  • constructor
  • connectedCallback
  • disconnectedCallback
  • attributeChangedCallback
  • adoptedCallback

Funkcja constructor jest wywoływana, gdy element jest tworzony po raz pierwszy, np. przez wywołanie funkcji document.createElement(‘rating-element') lub new RatingElement(). Konstruktor to dobre miejsce na skonfigurowanie elementu, ale manipulowanie DOM w konstruktorze jest zwykle uważane za złą praktykę ze względu na wydajność „uruchamiania” elementu.

Funkcja connectedCallback jest wywoływana, gdy element niestandardowy jest dołączany do DOM. Zwykle to w tym miejscu dochodzi do początkowych manipulacji DOM.

Funkcja disconnectedCallback jest wywoływana po usunięciu elementu niestandardowego z modelu DOM.

Funkcja attributeChangedCallback(attrName, oldValue, newValue) jest wywoływana, gdy zmieni się którykolwiek z atrybutów określonych przez użytkownika.

Funkcja adoptedCallback jest wywoływana, gdy element niestandardowy zostanie przeniesiony z innego documentFragment do głównego dokumentu za pomocą adoptNode, np. w HTMLTemplateElement.

Renderowanie DOM

Teraz wróć do elementu niestandardowego i powiąż z nim pewne elementy DOM. Ustaw treść elementu, gdy zostanie on dołączony do 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 przechowujesz właściwość instancji o nazwie rating w elemencie. W connectedCallback dodajesz elementy podrzędne DOM do <rating-element>, aby wyświetlić bieżącą ocenę wraz z przyciskami „Lubię” i „Nie lubię”.

4. Shadow DOM

Dlaczego warto korzystać z Shadow DOM?

W poprzednim kroku zauważysz, że selektory wstawionego tagu stylu wybierają na stronie wszystkie elementy oceny, a także wszystkie przyciski. Może to spowodować, że style wyjdą poza element i wybiorą inne węzły, których nie chcesz stylizować. Dodatkowo inne style poza tym elementem niestandardowym mogą nieumyślnie stylizować węzły w jego wnętrzu. Na przykład spróbuj umieścić tag stylu w nagłówku głównego dokumentu:

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>

Dane wyjściowe powinny zawierać czerwone obramowanie wokół zakresu oceny. To prosty przypadek, ale brak hermetyzacji DOM może powodować większe problemy w przypadku bardziej złożonych aplikacji. Tu właśnie przydaje się Shadow DOM.

Dołączanie głównego węzła cienia

Dołącz do elementu Shadow Root i wyrenderuj DOM w tym korzeniu:

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);

Po odświeżeniu strony zauważysz, że style w głównym dokumencie nie mogą już wybierać węzłów w Shadow Root.

Jak to zrobiłeś? W connectedCallback wywołujesz funkcję this.attachShadow, która dołącza do elementu główny cień. Tryb open oznacza, że treść w cieniu jest dostępna do sprawdzenia, a do korzenia cienia można też uzyskać dostęp za pomocą this.shadowRoot. Sprawdź też komponent internetowy w inspektorze Chrome:

Drzewo DOM w inspektorze Chrome. Element <rating-element> ma element a#shadow-root (open) jako element podrzędny, a w tym elemencie shadowroot znajduje się DOM z wcześniejszego przykładu.

Powinien wyświetlić się rozwijany element shadow root zawierający treści. Wszystko w tym korzeniu cienia nazywa się Shadow DOM. Jeśli wybierzesz element oceny w Narzędziach deweloperskich w Chrome i wywołasz $0.children, zauważysz, że nie zwraca on żadnych elementów podrzędnych. Dzieje się tak, ponieważ model Shadow DOM nie jest uważany za część tego samego drzewa DOM co bezpośrednie elementy podrzędne, ale za drzewo cieni.

Light DOM

Eksperyment: dodaj węzeł jako bezpośredni element podrzędny elementu <rating-element>:

index.html

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

Odśwież stronę, a zobaczysz, że ten nowy węzeł DOM w Light DOM tego elementu niestandardowego nie pojawia się na stronie. Dzieje się tak, ponieważ Shadow DOM ma funkcje, które kontrolują sposób, w jaki węzły Light DOM są przenoszone do Shadow DOM za pomocą elementów <slot>.

5. Szablony HTML

Dlaczego warto korzystać z szablonów

Używanie innerHTML i ciągów literałów szablonu bez oczyszczania może powodować problemy z bezpieczeństwem związane z wstrzykiwaniem skryptów. W przeszłości stosowano m.in. elementy DocumentFragment, ale wiązały się one z innymi problemami, takimi jak ładowanie obrazów i uruchamianie skryptów w momencie definiowania szablonów, a także utrudniały ponowne wykorzystanie. W tym miejscu pojawia się element <template>. Szablony zapewniają nieaktywny DOM, wysoce wydajną metodę klonowania węzłów i szablony wielokrotnego użytku.

Używanie szablonów

Następnie zmień komponent, aby używał szablonów 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>

W tym przypadku przeniesiono zawartość DOM do tagu szablonu w DOM głównego dokumentu. Teraz refaktoryzuj definicję elementu niestandardowego:

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);

Aby użyć tego elementu szablonu, musisz wysłać zapytanie do szablonu, pobrać jego zawartość i sklonować te węzły za pomocą funkcji templateContent.cloneNode, w której argument true wykonuje głębokie klonowanie. Następnie inicjujesz DOM za pomocą danych.

Gratulacje! Masz już komponent internetowy. Niestety na razie nic nie robi, więc dodajmy do niego trochę funkcji.

6. Dodawanie funkcji

Wiązania usługi

Obecnie jedynym sposobem ustawienia oceny w elemencie oceny jest utworzenie elementu, ustawienie właściwości rating w obiekcie, a następnie umieszczenie go na stronie. Niestety natywne elementy HTML zwykle nie działają w ten sposób. Natywne elementy HTML są zwykle aktualizowane zarówno w przypadku zmian właściwości, jak i atrybutów.

Spraw, aby element niestandardowy aktualizował widok, gdy zmieni się właściwość rating, dodając te wiersze:

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;
}

Dodajesz funkcje ustawiającą i pobierającą dla właściwości ocena, a następnie aktualizujesz tekst elementu ocena, jeśli jest dostępny. Oznacza to, że jeśli ustawisz właściwość oceny na elemencie, widok zostanie zaktualizowany. Szybko przetestuj to w konsoli narzędzi deweloperskich.

Wiązania atrybutów

Teraz zaktualizuj widok, gdy zmieni się atrybut. Działa to podobnie do aktualizowania widoku przez element wejściowy po ustawieniu <input value="newValue">. Na szczęście cykl życia komponentu internetowego obejmuje attributeChangedCallback. Zaktualizuj ocenę, dodając te wiersze:

index.js

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

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

Aby wywołać attributeChangedCallback, musisz ustawić statyczny getter dla RatingElement.observedAttributes which defines the attributes to be observed for changes. Następnie deklaratywnie ustawiasz ocenę w DOM. Wypróbuj:

index.html

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

Ocena powinna teraz być aktualizowana deklaratywnie.

Funkcje przycisków

Brakuje tylko funkcjonalności przycisku. Działanie tego komponentu powinno umożliwiać użytkownikowi oddanie pojedynczego głosu (w górę lub w dół) i przekazywać mu wizualną informację zwrotną. Możesz to zrobić za pomocą niektórych odbiorników zdarzeń i właściwości odbijającej, ale najpierw zaktualizuj style, aby zapewnić wizualne informacje zwrotne, dodając te wiersze:

index.html

<style>
...

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

W modelu Shadow DOM selektor :host odnosi się do węzła lub elementu niestandardowego, do którego jest dołączony element Shadow Root. W takim przypadku, jeśli atrybut vote ma wartość "up", przycisk „Lubię to” zmieni kolor na zielony, ale jeśli atrybut vote ma wartość "down", then it will turn the thumb-down button red. Teraz zaimplementuj logikę, tworząc odzwierciedlającą właściwość lub atrybut dla vote podobnie jak w przypadku rating. Zacznij od ustawienia i pobrania właściwości:

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;
}

Właściwość instancji _vote inicjujesz wartością null w funkcji constructor, a w funkcji ustawiającej sprawdzasz, czy nowa wartość jest inna. W takim przypadku odpowiednio dostosuj ocenę i co ważne, przekaż atrybut vote z wartością this.setAttribute z powrotem do hosta.

Następnie skonfiguruj powiązanie atrybutu:

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;
  }
}

Jest to ten sam proces, który został przeprowadzony w przypadku powiązania atrybutu rating. Dodajesz vote do observedAttributes i ustawiasz właściwość vote w attributeChangedCallback. Na koniec dodaj detektory zdarzeń kliknięcia, aby przyciski działały.

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 wiążesz z elementem niektóre detektory kliknięć i zachowujesz odwołania. W connectedCallback nasłuchujesz zdarzeń kliknięcia przycisków. W disconnectedCallback zwalniasz miejsce dla tych detektorów, a w samych detektorach kliknięć ustawiasz odpowiednio vote.

Gratulujemy! Masz teraz w pełni funkcjonalny komponent internetowy. Spróbuj kliknąć niektóre przyciski. Problem polega na tym, że mój plik JS ma teraz 96 wierszy, plik HTML – 43 wiersze, a kod jest dość rozbudowany i imperatywny jak na tak prosty komponent. Właśnie dlatego powstał projekt Lit od Google.

7. Lit-html

Punkt kontrolny kodu

Dlaczego lit-html

Przede wszystkim tag <template> jest przydatny i skuteczny, ale nie zawiera logiki komponentu, co utrudnia rozpowszechnianie szablonu z pozostałą logiką. Sposób używania elementów szablonu z natury sprzyja też kodowi imperatywnemu, który w wielu przypadkach jest mniej czytelny niż wzorce kodowania deklaratywnego.

Tu właśnie wkracza lit-html. Lit HTML to system renderowania Lit, który umożliwia pisanie szablonów HTML w JavaScript, a następnie wydajne renderowanie i ponowne renderowanie tych szablonów wraz z danymi w celu tworzenia i aktualizowania DOM. Jest podobna do popularnych bibliotek JSX i VDOM, ale działa natywnie w przeglądarce i w wielu przypadkach jest znacznie wydajniejsza.

Używanie Lit HTML

Następnie przenieś natywny komponent internetowy rating-element, aby używać szablonu Lit, który korzysta z oznaczonych literałów szablonu, czyli funkcji, które przyjmują ciągi szablonu jako argumenty ze specjalną składnią. Lit używa elementów szablonu, aby zapewnić szybkie renderowanie i funkcje oczyszczania danych, które zwiększają bezpieczeństwo. Zacznij od przeniesienia <template>index.html do szablonu Lit, dodając do komponentu internetowego metodę render():

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);
  }
}

Możesz też usunąć szablon z index.html. W tej metodzie renderowania zdefiniuj zmienną o nazwie template i wywołaj funkcję literału szablonu otagowanego html. Zauważysz też, że w elemencie span.rating wykonano proste powiązanie danych za pomocą składni interpolacji literału szablonu ${...}. Oznacza to, że w przyszłości nie będzie już konieczne imperatywne aktualizowanie tego węzła. Dodatkowo wywołujesz metodę lit render, która synchronicznie renderuje szablon w katalogu głównym cienia.

Przechodzenie na składnię deklaratywną

Po usunięciu elementu <template> zrefaktoryzuj kod, aby zamiast niego wywoływać nowo zdefiniowaną metodę render. Możesz zacząć od użycia powiązania detektora zdarzeń lit, aby uprościć kod detektora:

index.js

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

Szablony Lit mogą dodawać do węzła detektor zdarzeń za pomocą składni wiązania @EVENT_NAME, w której w tym przypadku aktualizujesz właściwość vote za każdym razem, gdy klikniesz te przyciski.

Następnie wyczyść kod inicjowania detektora zdarzeń w plikach constructor, connectedCallbackdisconnectedCallback:

index.js

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

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

// remove disonnectedCallback and _onUpClick and _onDownClick

Udało Ci się usunąć logikę detektora kliknięć ze wszystkich 3 wywołań zwrotnych, a nawet całkowicie usunąć disconnectedCallback. Udało Ci się też usunąć z connectedCallback cały kod inicjowania DOM, dzięki czemu wygląda on znacznie lepiej. Oznacza to również, że możesz pozbyć się metod detektora _onUpClick_onDownClick.

Na koniec zaktualizuj ustawienia właściwości, aby korzystały z metody render, dzięki czemu DOM będzie się aktualizować, gdy zmienią się właściwości lub atrybuty:

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();
}

W tym miejscu udało Ci się usunąć logikę aktualizacji DOM z funkcji ustawiającej rating i dodać wywołanie funkcji render z funkcji ustawiającej vote. Szablon jest teraz bardziej czytelny, ponieważ widać, gdzie są stosowane powiązania i detektory zdarzeń.

Odśwież stronę. Powinien pojawić się działający przycisk oceny, który po kliknięciu przycisku „W górę” będzie wyglądać tak:

Suwak oceny z kciukiem w górę i w dół, wartość 6, kciuk w górę w kolorze zielonym

8. LitElement

Dlaczego LitElement

W kodzie nadal występują pewne problemy. Po pierwsze, jeśli zmienisz właściwość lub atrybut vote, może to spowodować zmianę właściwości rating, co spowoduje dwukrotne wywołanie funkcji render. Mimo że wielokrotne wywoływanie funkcji render jest w zasadzie bezczynne i wydajne, maszyna wirtualna JavaScript nadal poświęca czas na synchroniczne wywoływanie tej funkcji dwa razy. Po drugie, dodawanie nowych właściwości i atrybutów jest żmudne, ponieważ wymaga dużej ilości powtarzalnego kodu. W tym momencie wkracza LitElement.

LitElement to klasa bazowa Lit do tworzenia szybkich i lekkich komponentów internetowych, które można wykorzystywać w różnych platformach i środowiskach. Następnie sprawdźmy, co LitElement może dla nas zrobić w rating-element, zmieniając implementację, aby z niej korzystać.

Korzystanie z LitElement

Zacznij od zaimportowania i utworzenia podklasy klasy bazowej LitElement z pakietu lit:

index.js

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

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

Importujesz LitElement, czyli nową klasę bazową dla rating-element. Następnie zachowujesz html import i na koniec css, co pozwala nam zdefiniować literały szablonu oznaczone tagiem CSS dla obliczeń CSS, szablonów i innych funkcji.

Następnie przenieś style z metody renderowania do statycznego arkusza stylów 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;
      }
    `;
  }
 ...

W tym miejscu w Lit znajdują się style. Lit wykorzystuje te style i funkcje przeglądarki, takie jak arkusze stylów z możliwością tworzenia, aby zapewnić szybsze renderowanie. W razie potrzeby przekazuje je też przez polyfill Web Components w starszych przeglądarkach.

Cykl życia

Lit wprowadza zestaw metod wywołań zwrotnych cyklu życia renderowania oprócz natywnych wywołań zwrotnych komponentów internetowych. Te wywołania zwrotne są wywoływane, gdy zmieniają się zadeklarowane właściwości Lit.

Aby korzystać z tej funkcji, musisz statycznie zadeklarować, które właściwości będą wywoływać cykl życia renderowania.

index.js

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

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

W tym miejscu określasz, że rating i vote będą wywoływać cykl życia renderowania LitElement, a także definiujesz typy, które będą używane do przekształcania atrybutów ciągu w właściwości.

<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>

Dodatkowo flaga reflect we właściwości vote automatycznie zaktualizuje atrybut vote elementu hosta, który został ręcznie wywołany w funkcji ustawiającej vote.

Teraz, gdy masz już blok właściwości statycznych, możesz usunąć całą logikę aktualizacji renderowania atrybutów i właściwości. Oznacza to, że możesz usunąć te metody:

  • connectedCallback
  • observedAttributes
  • attributeChangedCallback
  • rating (settery i gettery)
  • vote (settery i gettery, ale zachowaj logikę zmiany z settera)

Zachowasz constructor, a także dodasz nową metodę cyklu życia 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()

W tym przypadku wystarczy zainicjować rating i vote oraz przenieść logikę ustawiającą vote do metody cyklu życia willUpdate. Metoda willUpdate jest wywoływana przed render za każdym razem, gdy zmienia się dowolna aktualizowana właściwość, ponieważ LitElement grupuje zmiany właściwości i wykonuje renderowanie asynchronicznie. Zmiany właściwości reaktywnych (np. this.rating) w willUpdate nie będą wywoływać niepotrzebnych wywołań cyklu życia render.

render to metoda cyklu życia LitElement, która wymaga zwrócenia szablonu 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>`;
}

Nie musisz już sprawdzać elementu shadow root ani wywoływać funkcji render, która była wcześniej importowana z pakietu 'lit'.

Element powinien być teraz widoczny w podglądzie. Kliknij go.

9. Gratulacje

Gratulacje, udało Ci się od podstaw utworzyć komponent internetowy i przekształcić go w element Lit!

Lit jest bardzo mały (< 5 KB po zminifikowaniu i skompresowaniu), bardzo szybki i przyjemny w użyciu. Możesz tworzyć komponenty do wykorzystania w innych platformach lub budować za ich pomocą pełne aplikacje.

Wiesz już, czym są komponenty internetowe, jak je tworzyć i jak Lit ułatwia ich tworzenie.

Punkt kontrolny kodu

Chcesz porównać swój kod z naszym? Porównaj go tutaj.

Co dalej?

Wypróbuj inne ćwiczenia z programowania.

Więcej informacji

Społeczność