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

1. מבוא

עדכון אחרון:10 באוגוסט 2021

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

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

מה זה Lit

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

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

מה תלמדו

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

מה תפַתחו

  • רכיב אינטרנט מסוג וונילה מסוג 'אהבתי' / 'לא אהבתי'
  • רכיב אינטרנט מבוסס 'אהבתי' / 'לא אהבתי'

מה צריך להכין

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

2. תהליך ההגדרה ו לחקור את מגרש המשחקים

גישה לקוד

במהלך ה-Codelab יהיו קישורים ל-Lite Playground בצורה הבאה:

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

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

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

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

היכרות עם ממשק המשתמש המואר של Playground

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

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

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

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

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

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

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

  • לוחצים על לחצן ההורדה
  • חילוץ התוכן של קובץ ה-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
  • רכיבים מותאמים אישית
  • 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(). ה-constructor הוא מקום טוב להגדיר בו את הרכיב, אבל בדרך כלל הדבר נחשב לשיטה לא מומלצת לביצוע פעולות מניפולציה של DOM ב-constructor של הרכיב "אתחול" ספציפיות לביצועים.

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

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

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

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

עיבוד 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.

צירוף של מקור צל

מחברים לרכיב Shadow Root ומעבדים את ה-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);

כשתרעננו את הדף, תוכלו לראות שהסגנונות במסמך הראשי כבר לא יכולים לבחור את הצמתים שבתוך הרמה הבסיסית (root) של ההצללה.

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

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

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

DOM קל

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

index.html

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

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

5. תבניות HTML

למה כדאי להשתמש ב-Templates

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

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

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

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

קישורי מאפיינים

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

index.html

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

עכשיו הדירוג אמור להתעדכן באופן הצהרתי!

הפונקציונליות של הלחצן

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

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

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 שמשתמשת בליטרלים של תבניות מתויגות, שהן פונקציות שמקבלות מחרוזות תבנית כארגומנטים עם תחביר מיוחד. לאחר מכן, Lit משתמשת ברכיבים של תבניות כדי לספק עיבוד מהיר ולספק כמה תכונות ניקיון לצורכי אבטחה. כדי להתחיל, מעבירים את <template> שב-index.html לתבנית Lit. לשם כך, מוסיפים method 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. תוכלו גם לראות שביצעתם קישור נתונים פשוט בתוך הרכיב span.rating באמצעות התחביר של אינטרפולציה מילולית של התבנית של ${...}. כלומר, בסופו של דבר לא יהיה צורך לעדכן באופן הכרחי את הצומת. בנוסף, מפעילים את השיטה render המוארת, שמעבדת את התבנית באופן סינכרוני לשורש ההצללה.

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

עכשיו, לאחר שהסרת את הרכיב <template>, עליך לארגן מחדש את הקוד כך שיקרא במקום זאת ל-method render שהוגדרה לאחרונה. אתם יכולים להתחיל במינוף קישור ה-event listener של light כדי לנקות את קוד ה-listen:

index.js

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

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

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

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

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

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

פס הזזה לדירוג &#39;אהבתי&#39; ו&#39;לא אהבתי&#39; עם ערך של 6 והסימון של &#39;לייק&#39; בצבע ירוק למעלה

8. LitElement

למה כדאי להשתמש ב-LitElement

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

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

מחזור חיים

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

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

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

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

  • connectedCallback
  • observedAttributes
  • attributeChangedCallback
  • rating (מגדירים וקובעים)
  • vote (מגדירים וקובעים, אבל שומרים על לוגיקת השינוי מהמפענח)

מה שיישמר הוא 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 של מחזור החיים. מתבצעת קריאה ל-method 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>`;
}

כבר לא צריך לחפש את הרמה הבסיסית (root) של הצללית, וכבר אין צורך להפעיל את הפונקציה render שיובאה בעבר מחבילת 'lit'.

הרכיב שלכם אמור להיות מעובד בתצוגה המקדימה. מומלץ ללחוץ על הלחצן!

9. מזל טוב

כל הכבוד! פיתחתם בהצלחה רכיב אינטרנט חדש ופיתחתם אותו ל-LitElement!

Lit היא קטנה במיוחד (פחות מ-5kb מוקטנת + gzipped), מהירה במיוחד וממש כיף לתכנת! תוכלו ליצור רכיבים לשימוש ב-frameworks אחרות, או לפתח אפליקציות מלאות!

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

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

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

מה השלב הבא?

התנסות בכמה ממעבדות הקוד האחרות!

קריאה נוספת

קהילה