מרכיב אינטרנט לרכיב 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. הגדרה ושימוש ב-Playground

גישה לקוד

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

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

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

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

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

הסבר על ממשק המשתמש של Playground

סרגל הכרטיסיות של בורר הקבצים מסומן כקטע 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 מלפני כן בתוך ה-shadow root.

עכשיו אמור להופיע shadow root שניתן להרחבה ומכיל את התוכן. כל מה שנמצא בתוך שורש ה-Shadow נקרא 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

נקודת ביקורת של קוד

למה כדאי להשתמש ב-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 של 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

הצלחת להסיר את הלוגיקה של רכיב click listener מכל שלושת ה-callback ואפילו להסיר את 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 מה-setter של rating והוספת קריאה ל-render מה-setter של vote. עכשיו התבנית קריאה הרבה יותר כי אפשר לראות איפה מוחלים הקישורים והפונקציות 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 ולבסוף את 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 ומעבירים את הלוגיקה של setter 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 מקל על הבנייה שלהם.

נקודת ביקורת של קוד

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

מה השלב הבא?

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

קריאה נוספת

קהילה