מרכיב אינטרנט לרכיב Lit

1. מבוא

העדכון האחרון: 2021-08-10

רכיבי אינטרנט

רכיבי אינטרנט הם קבוצה של ממשקי API של פלטפורמת אינטרנט שמאפשרים ליצור תגי HTML חדשים, מותאמים אישית, לשימוש חוזר ומוצפנים לשימוש בדפי אינטרנט ובאפליקציות אינטרנט. רכיבים ווידג'טים מותאמים אישית שנבנו על בסיס תקני רכיבי האינטרנט יפעלו בדפדפנים מודרניים, ואפשר להשתמש בהם עם כל ספרייה או מסגרת של JavaScript שפועלות עם HTML.

מה זה Lit

Lit היא ספרייה פשוטה ליצירת רכיבי אינטרנט מהירים וקלי משקל, שפועלים בכל מסגרת או בלי מסגרת בכלל. עם Lit אפשר ליצור רכיבים, אפליקציות, מערכות עיצוב ועוד שניתנים לשיתוף.

‫Lit מספק ממשקי API שמפשטים משימות נפוצות של רכיבי אינטרנט, כמו ניהול מאפיינים, מאפיינים (attributes) ועיבוד.

מה תלמדו

  • מהו רכיב אינטרנט
  • מושגים של רכיבי אינטרנט
  • איך בונים רכיב אינטרנט
  • מהם lit-html ו-LitElement
  • מה Lit עושה בנוסף לרכיב אינטרנט

מה תפַתחו

  • רכיב אינטרנט רגיל של סימון לייק או דיסלייק
  • רכיב אינטרנט מבוסס-Lit עם אגודל למעלה או למטה

מה תצטרכו

  • כל דפדפן מודרני מעודכן (Chrome, ‏ Safari, ‏ Firefox, ‏ Chromium Edge). רכיבי אינטרנט פועלים בכל הדפדפנים המודרניים, ויש polyfills זמינים ל-Microsoft Internet Explorer 11 ול-Microsoft Edge שאינו מבוסס על Chromium.
  • ידע ב-HTML‏, CSS‏, JavaScript וכלי הפיתוח ל-Chrome.

2. הגדרה ושימוש בארגז החול

גישה לקוד

במהלך ה-codelab יוצגו קישורים ל-Lit playground כמו זה:

הסביבה הניסיונית היא ארגז חול לקוד שפועל באופן מלא בדפדפן. הוא יכול לקמפל ולהריץ קובצי TypeScript ו-JavaScript, והוא יכול גם לפתור באופן אוטומטי ייבוא של מודולים של node. לדוגמה:

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

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

אתם יכולים לבצע את כל המדריך בסביבת ה-Playground של Lit, ולהשתמש בנקודות הביקורת האלה כנקודות התחלה. אם אתם משתמשים ב-VS Code, אתם יכולים להשתמש בנקודות הבדיקה האלה כדי להוריד את קוד ההתחלה של כל שלב, וגם כדי לבדוק את העבודה שלכם.

הסבר על ממשק המשתמש של סביבת המשחקים המוארת

סרגל הכרטיסיות של בורר הקבצים מסומן כקטע 1, קטע עריכת הקוד מסומן כקטע 2, התצוגה המקדימה של הפלט מסומנת כקטע 3, והלחצן לרענון התצוגה המקדימה מסומן כקטע 4

צילום מסך של ממשק המשתמש של Lit playground שבו מודגשים הקטעים שבהם תשתמשו ב-codelab הזה.

  1. כלי לבחירת קבצים. שימו לב ללחצן הפלוס...
  2. עורך קבצים.
  3. תצוגה מקדימה של הקוד.
  4. לחצן הרענון.
  5. לחצן ההורדה.

הגדרה של VS Code (מתקדם)

אלה היתרונות של השימוש בהגדרה הזו של VS Code:

  • בדיקת סוג התבנית
  • השלמה אוטומטית ו-IntelliSense של תבניות

אם כבר התקנתם את NPM ואת VS Code (עם הפלאגין lit-plugin) ואתם יודעים איך להשתמש בסביבה הזו, אתם יכולים פשוט להוריד את הפרויקטים האלה ולהתחיל לעבוד איתם באופן הבא:

  • לוחצים על לחצן ההורדה
  • חילוץ התוכן של קובץ ה-tar לספרייה
  • מתקינים שרת פיתוח שיכול לפתור מפרטי מודולים פשוטים (צוות Lit ממליץ על ‎@web/dev-server)
  • מריצים את שרת הפיתוח ופותחים את הדפדפן (אם משתמשים ב-@web/dev-server אפשר להשתמש ב-npx web-dev-server --node-resolve --watch --open)
    • אם משתמשים בדוגמה package.json משתמשים ב-npm run serve

3. הגדרת רכיב בהתאמה אישית

רכיבים בהתאמה אישית

רכיבי אינטרנט הם אוסף של 4 ממשקי API מקוריים לאינטרנט. סוגי המשנה הם:

  • מודולים של ES
  • רכיבים בהתאמה אישית
  • Shadow DOM
  • תבניות HTML

כבר השתמשתם במפרט של מודולי ES, שמאפשר ליצור מודולי JavaScript עם ייבוא וייצוא שנטענים לדף באמצעות <script type="module">.

הגדרת רכיב מותאם אישית

מפרט הרכיבים המותאמים אישית מאפשר למשתמשים להגדיר רכיבי HTML משלהם באמצעות JavaScript. השמות חייבים להכיל מקף (-) כדי להבדיל אותם מרכיבים מקוריים של הדפדפן. מנקים את הקובץ 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>. כדי לוודא שההגדרה של רכיב בהתאמה אישית הצליחה, בוחרים באפשרות <rating-element> בכלי לבחירת רכיבים בכלי הפיתוח ל-Chrome, ובמסוף קוראים ל:

$0.constructor

הפלט שיתקבל:

class RatingElement extends HTMLElement {}

מחזור החיים של רכיב מותאם אישית

לרכיבים מותאמים אישית יש קבוצה של ווים (hooks) של מחזור החיים. סוגי המשנה הם:

  • constructor
  • connectedCallback
  • disconnectedCallback
  • attributeChangedCallback
  • adoptedCallback

הפונקציה constructor מופעלת כשהרכיב נוצר לראשונה: לדוגמה, על ידי הפעלת הפונקציה document.createElement(‘rating-element') או new RatingElement(). הקונסטרוקטור הוא מקום טוב להגדיר את האלמנט, אבל בדרך כלל לא מומלץ לבצע מניפולציות ב-DOM בקונסטרוקטור מסיבות שקשורות לביצועים של 'הפעלת' האלמנט.

הפונקציה connectedCallback מופעלת כשהרכיב המותאם אישית מצורף ל-DOM. בדרך כלל, כאן מתבצעות מניפולציות ראשוניות ב-DOM.

הפונקציה disconnectedCallback נקראת אחרי שהרכיב המותאם אישית מוסר מה-DOM.

הפונקציה attributeChangedCallback(attrName, oldValue, newValue) מופעלת כשמשתנה אחד או יותר מהמאפיינים שהמשתמש הגדיר.

הפונקציה adoptedCallback מופעלת כשאלמנט מותאם אישית מועבר ממסמך documentFragment אחר למסמך הראשי באמצעות adoptNode, כמו ב-HTMLTemplateElement.

Render 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. Shadow DOM

למה כדאי להשתמש ב-Shadow DOM?

בשלב הקודם, אפשר לראות שהסלקטורים בתג הסגנון שהוספתם בוחרים כל רכיב של דירוג בדף וגם כל לחצן. יכול להיות שהסגנונות יזלגו מהרכיב וייבחרו צמתים אחרים שלא התכוונתם להחיל עליהם סגנון. בנוסף, סגנונות אחרים מחוץ לרכיב המותאם אישית הזה עשויים לעצב את הצמתים בתוך הרכיב המותאם אישית בלי כוונה. לדוגמה, אפשר לנסות להוסיף תג סגנון ל-head של המסמך הראשי:

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.

צירוף Shadow Root

מצרפים Shadow Root לרכיב ומעבדים את ה-DOM בתוך ה-root:

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

אחרי רענון הדף, תשימו לב שהסגנונות במסמך הראשי כבר לא יכולים לבחור את הצמתים בתוך Shadow Root.

איך עשית את זה? ב-connectedCallback שהתקשרתם אליו this.attachShadow מצורף shadow root לאלמנט. מצב open מאפשר לבדוק את תוכן ה-shadow ונותן גישה ל-shadow root גם דרך this.shadowRoot. כדאי גם לבדוק את רכיב האינטרנט בכלי לבדיקת רכיבים ב-Chrome:

עץ ה-DOM בכלי לבדיקת רכיבים ב-Chrome. יש <rating-element> עם#shadow-root (open) כצאצא, וה-DOM מלפני כן בתוך ה-shadowroot.

עכשיו אמור להופיע shadow root שניתן להרחבה ומכיל את התוכן. כל מה שנמצא בתוך ה-shadow root נקרא Shadow DOM. אם תבחרו את רכיב הדירוג בכלי הפיתוח ל-Chrome ותקראו ל-$0.children, תראו שהוא לא מחזיר צאצאים. הסיבה לכך היא ש-Shadow DOM לא נחשב לחלק מאותו עץ DOM כמו צאצאים ישירים, אלא לחלק מעץ Shadow.

Light DOM

ניסוי: מוסיפים צומת כצאצא ישיר של <rating-element>:

index.html

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

מרעננים את הדף ורואים שצומת ה-DOM החדש הזה ב-Light DOM של הרכיב המותאם אישית הזה לא מופיע בדף. הסיבה לכך היא של-Shadow DOM יש תכונות שמאפשרות לשלוט באופן ההקרנה של צומתי Light DOM לתוך Shadow DOM באמצעות רכיבי <slot>.

5. תבניות HTML

למה כדאי להשתמש בתבניות

שימוש ב-innerHTML ובמחרוזות תבנית ללא ניקוי עלול לגרום לבעיות אבטחה שקשורות להחדרת סקריפטים. בעבר, שיטות שונות כללו שימוש ב-DocumentFragment, אבל גם לשיטות האלה יש בעיות אחרות, כמו טעינת תמונות והפעלת סקריפטים כשהתבניות מוגדרות, וגם מכשולים לשימוש חוזר. כאן נכנס לתמונה הרכיב <template>. תבניות מספקות 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 לתג template ב-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 מבצע שיבוט עמוק. לאחר מכן מאתחלים את ה-DOM עם הנתונים.

כל הכבוד, עכשיו יש לך רכיב אינטרנט! לצערנו, הוא עדיין לא עושה כלום, אז בשלב הבא מוסיפים לו פונקציונליות.

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 למאפיין הדירוג, ואז מעדכנים את הטקסט של רכיב הדירוג אם הוא זמין. כלומר, אם תגדירו את מאפיין הדירוג ברכיב, התצוגה תתעדכן. אתם יכולים לבצע בדיקה מהירה במסוף של כלי הפיתוח.

Attribute Bindings

עכשיו מעדכנים את התצוגה כשהמאפיין משתנה. זה דומה לעדכון של תצוגת קלט כשמגדירים את <input value="newValue">. למזלנו, מחזור החיים של רכיב האינטרנט כולל את attributeChangedCallback. כדי לעדכן את הדירוג, מוסיפים את השורות הבאות:

index.js

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

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

כדי שהפונקציה attributeChangedCallback תופעל, צריך להגדיר פונקציית getter סטטית ל-RatingElement.observedAttributes which defines the attributes to be observed for changes. לאחר מכן מגדירים את הדירוג באופן הצהרתי ב-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. עכשיו צריך להטמיע את הלוגיקה הזו על ידי יצירת מאפיין או תכונה משקפים עבור vote, בדומה לאופן שבו הטמעתם את rating. מתחילים עם הפונקציות להגדרת מאפיינים ולקבלת מאפיינים:

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

מפעילים את מאפיין המופע _vote עם null ב-constructor, ובשיטת ה-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. מוסיפים את vote אל observedAttributes ומגדירים את הנכס vote ב-attributeChangedCallback. ולבסוף, מוסיפים כמה מאזינים לאירועי קליקים כדי להעניק ללחצנים פונקציונליות.

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 בהתאם.

ברכות, עכשיו יש לך רכיב אינטרנט עם כל התכונות. כדאי לנסות ללחוץ על כמה לחצנים. הבעיה עכשיו היא שקובץ ה-JS שלי מגיע ל-96 שורות, קובץ ה-HTML ל-43 שורות, והקוד די מפורט ואימפרטיבי לרכיב פשוט כזה. כאן נכנס לתמונה פרויקט Lit של Google.

7. ‫Lit-html

Code Checkpoint

למה כדאי להשתמש ב-lit-html

קודם כל, התג <template> שימושי ויעיל, אבל הוא לא מגיע עם הלוגיקה של הרכיב, ולכן קשה להפיץ את התבנית עם שאר הלוגיקה. בנוסף, השימוש ברכיבי תבנית מוביל באופן טבעי לקוד אימפרטיבי, ובמקרים רבים לקוד שפחות קל לקריאה בהשוואה לדפוסי קידוד דקלרטיביים.

כאן נכנס לתמונה lit-html! ‫Lit html היא מערכת העיבוד של Lit שמאפשרת לכתוב תבניות HTML ב-JavaScript, ואז לעבד מחדש את התבניות האלה ביעילות יחד עם נתונים כדי ליצור ולעדכן את ה-DOM. היא דומה לספריות פופולריות של JSX ו-VDOM, אבל היא פועלת באופן מקורי בדפדפן ויעילה הרבה יותר במקרים רבים.

שימוש ב-Lit HTML

בשלב הבא, מעבירים את רכיב האינטרנט המקורי rating-element לשימוש בתבנית Lit שמשתמשת ב-Tagged Template Literals, שהן פונקציות שמקבלות מחרוזות תבנית כארגומנטים עם תחביר מיוחד. ‫Lit משתמש ברכיבי תבנית מתחת לפני השטח כדי לספק עיבוד מהיר, וגם מספק כמה תכונות של ניקוי נתונים לצורך אבטחה. כדי להתחיל, מעבירים את <template> ב-index.html לתבנית Lit על ידי הוספת שיטת 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);
  }
}

אפשר גם למחוק את התבנית מ-index.html. בשיטת הרינדור הזו מגדירים משתנה בשם template ומפעילים את הפונקציה html tagged template literal. בנוסף, אפשר לראות שביצעתם קשירת נתונים פשוטה בתוך רכיב span.rating באמצעות תחביר האינטרפולציה של תבנית ליטרלית ${...}. המשמעות היא שבסופו של דבר לא תצטרכו יותר לעדכן את הצומת הזה באופן אימפרטיבי. בנוסף, קוראים לשיטה lit render שמבצעת רינדור סינכרוני של התבנית ל-shadow root.

מעבר לתחביר הצהרתי

עכשיו, אחרי שהסרתם את רכיב <template>, צריך לשנות את מבנה הקוד כדי לקרוא במקום זאת לשיטה render שהוגדרה לאחרונה. אפשר להתחיל להשתמש ב-event listener binding של lit כדי לנקות את קוד ה-listener:

index.js

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

תבניות Lit יכולות להוסיף event listener לצומת באמצעות תחביר הקישור ‎@EVENT_NAME, ובמקרה הזה, אתם מעדכנים את המאפיין vote בכל פעם שלוחצים על הלחצנים האלה.

לאחר מכן, מפנים את קוד האתחול של ה-event listener ב-constructor, ב-connectedCallback וב-disconnectedCallback:

index.js

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

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

// remove disonnectedCallback and _onUpClick and _onDownClick

הצלחת להסיר את הלוגיקה של מאזין הקליקים מכל שלושת הקריאות החוזרות, ואפילו להסיר את disconnectedCallback לגמרי. הצלחת גם להסיר את כל קוד האתחול של ה-DOM מconnectedCallback, וכך הוא נראה הרבה יותר אלגנטי. זה גם אומר שאפשר להיפטר משיטות ההאזנה _onUpClick ו-_onDownClick.

לבסוף, מעדכנים את הפונקציות להגדרת מאפיינים כדי להשתמש בשיטה render, כך שניתן יהיה לעדכן את ה-DOM כשמשנים את המאפיינים או את האטריבוטים:

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

כאן, הצלחת להסיר את הלוגיקה של עדכון ה-DOM מהפונקציה rating setter והוספת קריאה ל-render מהפונקציה vote setter. עכשיו התבנית קריאה הרבה יותר, כי אפשר לראות איפה מוחלים הקישורים והפונקציות event listener.

מרעננים את הדף, ואז אמור להופיע כפתור דירוג תקין שייראה כך כשלוחצים על כפתור הלייק!

מחוון דירוג עם לייק ודיסלייק, עם ערך של 6 והלייק צבוע בירוק

8. LitElement

למה LitElement

עדיין יש כמה בעיות בקוד. קודם כל, אם משנים את המאפיין או את האטריבוט vote, יכול להיות שיופיע שינוי במאפיין rating, וכתוצאה מכך הפונקציה render תופעל פעמיים. למרות שקריאות חוזרות של render הן למעשה פעולות לא פעילות ויעילות, מכונת ה-VM של JavaScript עדיין מבזבזת זמן על קריאה לפונקציה הזו פעמיים באופן סינכרוני. שנית, הוספה של מאפיינים ומאפיינים חדשים היא תהליך מייגע כי היא דורשת הרבה קוד שחוזר על עצמו (boilerplate). כאן LitElement נכנס לתמונה.

LitElement היא מחלקת הבסיס של Lit ליצירת רכיבי אינטרנט מהירים וקלי משקל שאפשר להשתמש בהם במסגרות ובסביבות שונות. עכשיו, בואו נראה איך אפשר לשנות את ההטמעה כדי להשתמש ב-LitElement ב-rating-element.

שימוש ב-LitElement

מתחילים בייבוא של מחלקת הבסיס LitElement מהחבילה lit ויצירת מחלקת משנה שלה:

index.js

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

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

מייבאים את LitElement, שהוא מחלקת הבסיס החדשה של rating-element. לאחר מכן שומרים את html import ולבסוף את 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 ייקח את הסגנונות האלה וישתמש בתכונות של הדפדפן, כמו Constructable Stylesheets, כדי לספק זמני רינדור מהירים יותר, וגם יעביר אותם דרך ה-polyfill של Web Components בדפדפנים ישנים יותר, אם צריך.

מחזור חיים

‫Lit מציג קבוצה של שיטות קריאה חוזרת במחזור חיים של הרינדור, בנוסף לקריאות החוזרות של רכיבי האינטרנט המקוריים. הקריאות החוזרות האלה מופעלות כשמשנים מאפייני Lit מוצהרים.

כדי להשתמש בתכונה הזו, צריך להצהיר באופן סטטי אילו מאפיינים יפעילו את מחזור החיים של הרינדור.

index.js

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

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

כאן מגדירים ש-rating ו-vote יפעילו את מחזור החיים של העיבוד ב-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>

בנוסף, הדגל reflect במאפיין vote מעדכן באופן אוטומטי את המאפיין vote של רכיב המארח שהפעלתם ידנית ב-setter של vote.

עכשיו, כשיש לכם את בלוק המאפיינים הסטטיים, אתם יכולים להסיר את כל הלוגיקה של עדכון העיבוד של המאפיינים. כלומר, אפשר להסיר את השיטות הבאות:

  • connectedCallback
  • observedAttributes
  • attributeChangedCallback
  • rating (setters ו-getters)
  • vote (setters and getters but keep the change logic from the 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()

כאן פשוט מאתחלים את rating ואת vote ומעבירים את הלוגיקה של הפונקציה להגדרת הערך של vote לשיטת מחזור החיים willUpdate. השיטה willUpdate נקראת לפני render בכל פעם שמשנים מאפיין כלשהו שמתעדכן, כי LitElement מבצעת את שינויי המאפיינים בקבוצות ומבצעת את הרינדור באופן אסינכרוני. שינויים במאפיינים ריאקטיביים (כמו this.rating) ב-willUpdate לא יפעילו קריאות מיותרות למחזור החיים של 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>`;
}

לא צריך יותר לבדוק את ה-shadow root, ולא צריך יותר לקרוא לפונקציה render שיוצאה בעבר מהחבילה 'lit'.

האלמנט אמור להיות מוצג בתצוגה המקדימה. לוחצים עליו.

9. מזל טוב

כל הכבוד, הצלחתם ליצור רכיב אינטרנט מאפס ולהפוך אותו ל-LitElement.

‫Lit הוא קטן מאוד (פחות מ-5kb אחרי מיניפיקציה ודחיסה), מהיר מאוד וכיף מאוד לכתוב איתו קוד! אתם יכולים ליצור רכיבים לשימוש במסגרות אחרות, או לבנות איתה אפליקציות מלאות.

עכשיו אתם יודעים מהו רכיב אינטרנט, איך בונים אותו ואיך Lit מקל על הבנייה שלו.

Code Checkpoint

רוצה לבדוק את הקוד הסופי שלך מול הקוד שלנו? אפשר להשוות בין התוכניות כאן.

מה השלב הבא?

כדאי לנסות עוד codelabs!

קריאה נוספת

קהילה