Lit למפתחי React

Lit למפתחי React

מידע על Codelab זה

subjectהעדכון האחרון: יוני 25, 2021
account_circleנכתב על ידי Elliott Marquez & Brian Vann

1.‏ מבוא

מהי Lit

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

מה תלמדו

איך לתרגם כמה מושגים של React ל-Lite, כמו:

  • JSX ו-JSX טמפל
  • רכיבים אביזרים
  • מדינה ו- מחזור חיים
  • Hooks
  • ילדים
  • הפניות
  • מצב גישור

מה תפַתחו

בסוף השיעור הזה תהיה אפשרות להמיר מושגים של רכיבי React לאנלוגים של Lit.

למה תזדקק?

  • גרסה עדכנית של Chrome , Safari , Firefox או Edge.
  • ידע ב-HTML, ב-CSS, ב-JavaScript ובכלי הפיתוח ל-Chrome.
  • הידע של React
  • (מתקדם) אם אתם רוצים את חוויית הפיתוח הטובה ביותר, כדאי להוריד את VS Code. נדרש גם lit-Plugin עבור VS Code ו-NPM.

2.‏ קריאה ותגובה

מושגי הליבה והיכולות של Lit דומים במובנים רבים ל-React, אבל יש ל-Lit כמה הבדלים והבדלים מרכזיים:

הוא קטן

Lit הוא קטנטן: הוא מוקטן ב-כ-5kb ומקובץ gzip בהשוואה ל-kb 40 + ReactDOM.

תרשים עמודות של גודל החבילה מוקטן ודחוס ב-kb. סרגל ה-Lite הוא 5kb ו-React + React DOM הוא 42.2kb

מהיר

בנקודות השוואה ציבוריות שמשווה בין מערכת יצירת התבניות של Lit, lit-html, ל-VDOM של React, lit-html יוצא מהיר יותר ב-8-10% מ-React במקרה הגרוע ביותר, ומהיר יותר ב-50%ומעלה בתרחישים נפוצים יותר.

LitElement (מחלקת הבסיס של רכיבי LitElement) מוסיפה תקורה מינימלית ל-lit-html, אבל גורמת לביצועים של React ב-16-30% בהשוואה בין תכונות של רכיבים כמו שימוש בזיכרון, אינטראקציה וזמני הפעלה.

תרשים עמודות מקובץ של הביצועים בהשוואה למצב 'בהיר' ל'תגובה' באלפיות השנייה (הנמוך יותר עדיף)

לא נדרש build

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

עם מודולים של ES ורשתות CDN מודרניות כמו Skypack או UNPKG, אולי אפילו לא יהיה צורך ב-NPM כדי להתחיל.

אבל אם רוצים, עדיין אפשר ליצור קוד Lit Code ולבצע בו אופטימיזציה. איחוד המפתחים שבוצע לאחרונה סביב מודולים של ES מקורי הניב תוצאות טובות ל-Lit – Lit הוא פשוט JavaScript רגיל ואין צורך ברכיבי CLI ספציפיים ל-framework או בטיפול ב-build.

שילוב של מסגרת

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

הבעיות היחידות עם יכולת פעולה הדדית של framework הן כאשר ל-frameworks יש תמיכה מוגבלת ב-DOM. התגובה היא אחת מהמסגרות האלה, אבל היא מאפשרת פרצות מילוט דרך Refs, והפניות ב-React לא מספקות חוויית פיתוח טובה.

צוות Lit עובד על פרויקט ניסיוני בשם @lit-labs/react, שינתח באופן אוטומטי את רכיבי ה-Lit וליצור wrapper של תגובה, כך שלא תצטרכו להשתמש בהפניות.

בנוסף, הכלי Custom Elements בכל מקום יראה לכם אילו frameworks וספריות עובדות טוב עם רכיבים מותאמים אישית!

תמיכה ב-TypeScript מהשורה הראשונה

למרות שניתן לכתוב את כל קוד ה-Lit ב-JavaScript, ה-Lit כתוב ב-TypeScript וצוות Lit ממליץ גם למפתחים להשתמש ב-TypeScript!

צוות Lit פועל בשיתוף עם קהילת Lit כדי לנהל פרויקטים שמביאים בדיקה של סוגי TypeScript ותובנות לתבניות Lit גם בפיתוח וגם ב-build באמצעות lit-analyzer ו-lit-plugin.

צילום מסך של סביבת פיתוח משולבת (IDE) שבה מוצגת בדיקת סוג לא נכונה להגדרת מספר בוליאני

צילום מסך של סביבת פיתוח משולבת (IDE) שמציגה הצעות חכמות

כלי הפיתוח מובנים בדפדפן

רכיבי Lit הם רק רכיבי HTML ב-DOM. המשמעות היא שכדי לבדוק את הרכיבים אין צורך להתקין כלים או הפעלות בדפדפן.

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

תמונה של כלי הפיתוח ל-Chrome שמראה ש-$0 מחזיר <mwc-textfield>, $0.value מחזירה hello world, $0.outlined מחזיר true, ו-{$0} מראה את הרחבת הנכס

הוא מבוסס על רינדור בצד השרת (SSR)

Lit 2 פותחה מתוך מחשבה על תמיכה ב-SSR. נכון לזמן כתיבת ה-Codelab הזה, צוות Lit עדיין לא פרסם את כלי ה-SSR בגרסה יציבה, אבל צוות Lit כבר פרס את הרכיבים שעברו רינדור בצד השרת במוצרי Google, ובדק את SSR באפליקציות של React. צוות Lit מצפה להשיק את הכלים האלה באופן חיצוני ב-GitHub בקרוב.

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

הרכישה ממש נמוכה

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

האם יצרתם אפליקציה שלמה ב-Lit ואתם רוצים לעבור לאפליקציה אחרת? עכשיו תוכלו למקם את אפליקציית Lit הנוכחית בתוך המסגרת החדשה ולהעביר את מה שאתם רוצים לרכיבי ה-framework.

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

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

אפשר לעשות זאת בשתי דרכים:

  • אפשר לעשות את זה לגמרי באינטרנט, בדפדפן
  • (מתקדם) אפשר לעשות זאת במחשב המקומי באמצעות VS Code

גישה לקוד

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

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

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

// after
import './my-file.js';
import 'https://cdn.skypack.dev/lit';

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

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

סרגל הכרטיסיות של בורר הקבצים מסומן בתווית &#39;קטע 1&#39;, הקטע של עריכת הקוד כקטע 2, התצוגה המקדימה של הפלט היא קטע 3, ולחצן התצוגה מחדש של התצוגה המקדימה הוא קטע 4.

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

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

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

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

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

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

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

4.‏ JSX ו-JSX טמפל

בקטע הזה נסביר את העקרונות הבסיסיים של יצירת תבניות ב-Lit.

JSX ו-JSX תבניות של דף טקסט

JSX הוא תוסף תחביר ל-JavaScript שמאפשר למשתמשי React לכתוב בקלות תבניות בקוד ה-JavaScript שלהם. לתבניות Lit יש מטרה דומה: ביטוי ממשק המשתמש של רכיב כפונקציה של המצב שלו.

תחביר בסיסי

ב-React היית רוצה ליצור עולם של JSX שלום באופן הבא:

import 'react';
import ReactDOM from 'react-dom';

const name = 'Josh Perez';
const element = (
 
<>
   
<h1>Hello, {name}</h1>
   
<div>How are you?</div>
 
</>
);

ReactDOM.render(
 
element,
 
mountNode
);

בדוגמה שלמעלה, יש שני רכיבים והקובץ "name" כולל מותאם אישית. ב-Lit, מבצעים את הפעולות הבאות:

import {html, render} from 'lit';

const name = 'Josh Perez';
const element = html`
  <
h1>Hello, ${name}</h1>
  <
div>How are you?</div>`;

render(
 
element,
 
mountNode
);

שימו לב שבתבניות Lit לא צריך Fragment (מקטע תגובה) כדי לקבץ מספר רכיבים בתבניות שלו.

ב-Lit, התבניות עטיפות html לתבנית מתויגת LIT, שזה המקום שבו Lit מקבלת את השם שלה!

ערכי התבנית

תבניות של Lit יכולות לקבל תבניות אחרות של Lit, שנקראות TemplateResult. לדוגמה, צריך להקיף את name בתגים נטויים (<i>) ועוטפים אותו בליטרל של תבנית מתויגת N.B. חשוב להקפיד להשתמש בתו גרש (`) ולא בתו גרש יחיד (').

import {html, render} from 'lit';

const name = html`<i>Josh Perez</i>`;
const element = html`
  <
h1>Hello, ${name}</h1>
  <
div>How are you?</div>`;

render(
 
element,
 
mountNode
);

פונקציות TemplateResult של Lit יכולות לקבל מערכים, מחרוזות, TemplateResult אחרים וגם הוראות.

לתרגיל, נסו להמיר את קוד התגובה הבא ל-Lit:

const itemsToBuy = [
  <li>Bananas</li>,
  <li>oranges</li>,
  <li>apples</li>,
  <li>grapes</li>
];
const element = (
  <>
    <h1>Things to buy:</h1>
    <ol>
      {itemsToBuy}
    </ol>
  </>);

ReactDOM.render(
  element,
  mountNode
);

התשובה:

import {html, render} from 'lit';

const itemsToBuy = [
 
html`<li>Bananas</li>`,
 
html`<li>oranges</li>`,
 
html`<li>apples</li>`,
 
html`<li>grapes</li>`
];
const element = html`
  <
h1>Things to buy:</h1>
  <
ol>
   
${itemsToBuy}
  <
/ol>`;

render(
 
element,
 
mountNode
);

מסירת אביזרים וקביעת אביזרים

אחד ההבדלים הגדולים ביותר בין התחביר של JSX לבין התחביר של Lit הוא התחביר של קישור הנתונים. לדוגמה, ניקח את קלט ה-React הבא עם קישורים:

const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element =
  <input
      disabled={disabled}
      className={`static-class ${myClass}`}
      defaultValue={value}/>;

ReactDOM.render(
  element,
  mountNode
);

בדוגמה שלמעלה מוגדר קלט שמבצע את הפעולות הבאות:

  • קבוצות מושבתות למשתנה מוגדר (false במקרה הזה)
  • הגדרת המחלקה כ-static-class עם משתנה (במקרה הזה "static-class my-class")
  • הגדרה של ערך ברירת מחדל

ב-Lit, מבצעים את הפעולות הבאות:

import {html, render} from 'lit';

const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element = html`
  <
input
     
?disabled=${disabled}
     
class="static-class ${myClass}"
     
.value=${value}>`;

render(
 
element,
 
mountNode
);

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

לאחר מכן, יש קישור ישיר למאפיין class ולא ל-className. אפשר להוסיף למאפיין class כמה קישורים, אלא אם משתמשים בהנחיה classMap, שעוזרת הצהרתית להפעלת מחלקות.

לבסוף, המאפיין value מוגדר בקלט. בניגוד ל-React, הפעולה הזו לא תגדיר את רכיב הקלט לקריאה בלבד כי הוא תואם להטמעה ולהתנהגות של הקלט במקור.

תחביר של קישור פרויקט ל-Lite

html`<my-element ?attribute-name=${booleanVar}>`;
  • הקידומת ? היא התחביר המקשר להחלפת מאפיין ברכיב
  • שווה ערך ל-inputRef.toggleAttribute('attribute-name', booleanVar)
  • הפונקציה הזו שימושית לרכיבים שמשתמשים ב-disabled כ-disabled="false" עדיין נחשבת כ-True על ידי ה-DOM כי inputElement.hasAttribute('disabled') === true
html`<my-element .property-name=${anyVar}>`;
  • הקידומת . היא התחביר המקשר להגדרת מאפיין של אלמנט
  • שווה ערך ל-inputRef.propertyName = anyVar
  • טוב להעברת נתונים מורכבים כמו אובייקטים, מערכים או מחלקות
html`<my-element attribute-name=${stringVar}>`;
  • קישור למאפיין של אלמנט
  • שווה ערך ל-inputRef.setAttribute('attribute-name', stringVar)
  • מתאים לערכים בסיסיים, בוררי כללי סגנון ובוררי שאילתות

handlers מוסרים

const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element =
  <input
      onClick={() => console.log('click')}
      onChange={e => console.log(e.target.value)} />;

ReactDOM.render(
  element,
  mountNode
);

בדוגמה שלמעלה מוגדר קלט שמבצע את הפעולות הבאות:

  • תיעוד המילה "קליק" כשמשתמש לוחץ על הקלט
  • רישום של הערך של הקלט כשהמשתמש מקליד תו

ב-Lit, מבצעים את הפעולות הבאות:

import {html, render} from 'lit';

const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element = html`
  <
input
     
@click=${() => console.log('click')}
     
@input=${e => console.log(e.target.value)}>`;

render(
 
element,
 
mountNode
);

בדוגמת Lit, נוסף אוזן לאירוע click עם @click.

לאחר מכן, במקום להשתמש במאפיין onChange, יש קישור לאירוע input המקורי של <input>, כי האירוע change המקורי מופעל רק ב-blur (התגובה מופשטת לגבי האירועים האלה).

תחביר של הגורם המטפל באירועים של אותיות רישיות

html`<my-element @event-name=${() => {...}}></my-element>`;
  • הקידומת @ היא התחביר המקשר של פונקציות event listener
  • שווה ערך ל-inputRef.addEventListener('event-name', ...)
  • נעשה שימוש בשמות אירועי DOM מקוריים

5.‏ רכיבים אביזרים

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

רכיבי הכיתה LitElement

המושג של Lit לרכיב של מחלקה React הוא LitElement, והמושג של Lit של Lit (מאפיינים תגובתיים) הוא שילוב של האביזרים והמצב של React. לדוגמה:

import React from 'react';
import ReactDOM from 'react-dom';

class Welcome extends React.Component {
 
constructor(props) {
   
super(props);
   
this.state = {name: ''};
 
}

 
render() {
   
return <h1>Hello, {this.props.name}</h1>;
 
}
}

const element = <Welcome name="Elliott"/>
ReactDOM.render(
 
element,
 
mountNode
);

בדוגמה שלמעלה יש רכיב React:

  • הפונקציה יוצרת name
  • הגדרת ערך ברירת המחדל של name למחרוזת ריקה ("")
  • הקצאה מחדש של name למשתמש "Elliott"

כך עושים את זה ב-LitElement

ב-TypeScript:

import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';

@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
 
@property({type: String})
 
name = '';

 
render() {
   
return html`<h1>Hello, ${this.name}</h1>`
 
}
}

ב-JavaScript:

import {LitElement, html} from 'lit';

class WelcomeBanner extends LitElement {
 
static get properties() {
   
return {
     
name: {type: String}
   
}
 
}

 
constructor() {
   
super();
   
this.name = '';
 
}

 
render() {
   
return html`<h1>Hello, ${this.name}</h1>`
 
}
}

customElements.define('welcome-banner', WelcomeBanner);

ובקובץ ה-HTML:

<!-- index.html -->
<head>
  <script type="module" src="./index.js"></script>
</head>
<body>
  <welcome-banner name="Elliott"></welcome-banner>
</body>

סקירה של מה שקורה בדוגמה שלמעלה:

@property({type: String})
name = '';
  • מגדיר מאפיין תגובתי ציבורי – חלק מה-API הציבורי של הרכיב
  • חושף מאפיין (כברירת מחדל) וגם מאפיין ברכיב
  • קביעה איך לתרגם את מאפיין הרכיב (שהן מחרוזות) לערך
static get properties() {
  return {
    name: {type: String}
  }
}
  • הוא משמש באותה פונקציה כמו העיצוב של @property TS, אבל הוא פועל במקור ב-JavaScript
render() {
  return html`<h1>Hello, ${this.name}</h1>`
}
  • היא נקראת בכל פעם שמאפיין תגובתי משתנה
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
  ...
}
  • הפעולה הזו משייכת שם תג של רכיב HTML להגדרת מחלקה
  • בהתאם לתקן של רכיבים מותאמים אישית, שם התג חייב לכלול מקף (-)
  • this ב-LitElement מתייחס למופע של הרכיב המותאם אישית (<welcome-banner> במקרה הזה)
customElements.define('welcome-banner', WelcomeBanner);
  • הגרסה המקבילה של JavaScript לקישוט של @customElement TS
<head>
  <script type="module" src="./index.js"></script>
</head>
  • ייבוא של הגדרת הרכיב המותאם אישית
<body>
 
<welcome-banner name="Elliott"></welcome-banner>
</body>
  • הוספת הרכיב המותאם אישית לדף
  • הגדרת המאפיין name כ-'Elliott'

רכיבי הפונקציות

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

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

const element = <Welcome name="Elliott"/>
ReactDOM.render(
  element,
  mountNode
);

במצב הזה, זה יהיה:

import {html, render} from 'lit';

function Welcome(props) {
 
return html`<h1>Hello, ${props.name}</h1>`;
}

render(
 
Welcome({name: 'Elliott'}),
 
document.body.querySelector('#root')
);

6.‏ מדינה ו- מחזור חיים

בקטע הזה נסביר על המצב ומחזור החיים של Lit.

מדינה

הקונספט של Lit ל-"Reactive Properties" הוא שילוב של מצב התגובה והאביזרים. מאפיינים תגובתיים יכולים להפעיל את מחזור החיים של הרכיב. למאפיינים תגובתיים יש שתי וריאציות:

נכסים תגובתיים ציבוריים

// React
import React from 'react';

class MyEl extends React.Component {
 
constructor(props) {
   
super(props)
   
this.state = {name: 'there'}
 
}

 
componentWillReceiveProps(nextProps) {
   
if (this.props.name !== nextProps.name) {
     
this.setState({name: nextProps.name})
   
}
 
}
}

// Lit (TS)
import {LitElement} from 'lit';
import {property} from 'lit/decorators.js';

class MyEl extends LitElement {
 
@property() name = 'there';
}
  • מוגדר על ידי @property
  • דומה לאביזרים ולמצב של React, אבל ניתן לשינוי
  • ממשק API ציבורי שצרכנים של הרכיב ניגשים אליו ומגדירו אותו

מצב תגובה פנימי

// React
import React from 'react';

class MyEl extends React.Component {
 
constructor(props) {
   
super(props)
   
this.state = {name: 'there'}
 
}
}

// Lit (TS)
import {LitElement} from 'lit';
import {state} from 'lit/decorators.js';

class MyEl extends LitElement {
 
@state() name = 'there';
}
  • מוגדר על ידי @state
  • דומה למצב של React אבל ניתן לשינוי
  • מצב פנימי פרטי שאליו ניגשים בדרך כלל מתוך הרכיב או מחלקות המשנה

מחזור חיים

מחזור החיים של Lit דומה למחזור של React, אבל יש כמה הבדלים חשובים.

constructor

// React (js)
import React from 'react';

class MyEl extends React.Component {
 
constructor(props) {
   
super(props);
   
this.state = { counter: 0 };
   
this._privateProp = 'private';
 
}
}

// Lit (ts)
class MyEl extends LitElement {
 
@property({type: Number}) counter = 0;
 
private _privateProp = 'private';
}

// Lit (js)
class MyEl extends LitElement {
 
static get properties() {
   
return { counter: {type: Number} }
 
}
 
constructor() {
   
this.counter = 0;
   
this._privateProp = 'private';
 
}
}
  • מילת המפתח המקבילה היא גם constructor
  • אין צורך להעביר שום דבר לשיחת-העל
  • הופעל על ידי (לא כולל את כל האפשרויות):
    • document.createElement
    • document.innerHTML
    • new ComponentClass()
    • אם קיים בדף שם תג לא משודרג, וההגדרה נטענה ונרשמה אצל @customElement או customElements.define
  • דומה בפונקציה של constructor של React

render

// React
render() {
  return <div>Hello World</div>
}

// Lit
render() {
  return html`<div>Hello World</div>`;
}
  • מילת המפתח המקבילה היא גם render
  • המערכת יכולה להחזיר כל תוצאה שניתנת לעיבוד, למשל: TemplateResult או string וכו'
  • בדומה ל-React, render() צריכה להיות פונקציה טהור
  • המערכת תעבד את הצומת createRenderRoot() שיוחזר (ShadowRoot כברירת מחדל)

componentDidMount

הערך componentDidMount דומה לשילוב של הקריאות החוזרות (callback) של מחזור החיים firstUpdated ו-connectedCallback של Lit.

firstUpdated

import Chart from 'chart.js';

// React
componentDidMount() {
 
this._chart = new Chart(this.chartElRef.current, {...});
}

// Lit
firstUpdated() {
 
this._chart = new Chart(this.chartEl, {...});
}
  • בוצעה קריאה בפעם הראשונה שבה מתבצע רינדור של התבנית של הרכיב לשורש של הרכיב
  • תתבצע קריאה רק אם הרכיב מחובר. למשל, לא בוצעה קריאה דרך document.createElement('my-component') עד שהצומת הזה יצורף לעץ ה-DOM
  • זהו מקום טוב לבצע הגדרת רכיבים שמחייבת את ה-DOM שעבר רינדור על ידי הרכיב
  • בניגוד לשינויים ב-componentDidMount במאפיינים של תגובות ב-firstUpdated, השינויים יגרמו לעיבוד מחדש, אבל הדפדפן בדרך כלל יקבץ את השינויים באותה מסגרת. אם השינויים האלה לא מחייבים גישה ל-DOM של הרמה הבסיסית (root), בדרך כלל הם אמורים להופיע ב-willUpdate.

connectedCallback

// React
componentDidMount() {
  this.window.addEventListener('resize', this.boundOnResize);
}

// Lit
connectedCallback() {
  super.connectedCallback();
  this.window.addEventListener('resize', this.boundOnResize);
}
  • מתבצעת קריאה בכל פעם שרכיב מותאם אישית נוסף לעץ ה-DOM
  • בניגוד לרכיבי React, כשרכיבים מותאמים אישית מנותקים מה-DOM, הם לא מושמדים ולכן אפשר "לחבר" אותם. כמה פעמים
    • לא תתבצע שיחה חוזרת אל firstUpdated
  • שימושי לאתחול מחדש של ה-DOM או לצירוף מחדש של פונקציות event listener שנוקו לאחר הניתוק
  • הערה: ייתכן שהקריאה אל connectedCallback תתבצע לפני firstUpdated, לכן בשיחה הראשונה יכול להיות שה-DOM לא יהיה זמין

componentDidUpdate

// React
componentDidUpdate(prevProps) {
  if (this.props.title !== prevProps.title) {
    this._chart.setTitle(this.props.title);
  }
}

// Lit (ts)
updated(prevProps: PropertyValues<this>) {
  if (prevProps.has('title')) {
    this._chart.setTitle(this.title);
  }
}
  • הערך המקביל באנגלית הוא updated (לפי זמן העבר באנגלית update)
  • בניגוד ל-React, מתבצעת קריאה ל-updated גם בעיבוד הראשוני
  • דומה בפונקציה של componentDidUpdate של React

componentWillUnmount

// React
componentWillUnmount() {
  this.window.removeEventListener('resize', this.boundOnResize);
}

// Lit
disconnectedCallback() {
  super.disconnectedCallback();
  this.window.removeEventListener('resize', this.boundOnResize);
}
  • מילת המפתח המקבילה דומה ל-disconnectedCallback
  • בניגוד לרכיבי React, כשרכיבים מותאמים אישית מנותקים מה-DOM הרכיב לא מושמד
  • שלא כמו componentWillUnmount, disconnectedCallback נקרא אחרי שהאלמנט מסיר מהעץ
  • ה-DOM שבתוך השורש עדיין מצורף לעץ המשנה של השורש
  • שימושי לניקוי פונקציות event listener והפניות דליפות כדי שהדפדפן יוכל לאסוף באשפה את הרכיב

פעילות גופנית

import React from 'react';
import ReactDOM from 'react-dom';

class Clock extends React.Component {
 
constructor(props) {
   
super(props);
   
this.state = {date: new Date()};
 
}

 
componentDidMount() {
   
this.timerID = setInterval(
     
() => this.tick(),
     
1000
   
);
 
}

 
componentWillUnmount() {
   
clearInterval(this.timerID);
 
}

 
tick() {
   
this.setState({
     
date: new Date()
   
});
 
}

 
render() {
   
return (
     
<div>
       
<h1>Hello, world!</h1>
       
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
     
</div>
   
);
 
}
}

ReactDOM.render(
  <
Clock />,
 
document.getElementById('root')
);

בדוגמה שלמעלה יש שעון פשוט שמבצע את הפעולות הבאות:

  • הוא מציג את ההודעה "Hello World! " זה אכן" ואז מציג את השעה
  • בכל שנייה השעון יתעדכן
  • בניתוק מרווח הזמן של הסימונים

קודם כל צריך להתחיל בהצהרה לגבי סיווג הרכיב:

// Lit (TS)
// some imports here are imported in advance
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';

@customElement('lit-clock')
class LitClock extends LitElement {
}

// Lit (JS)
// `html` is imported in advance
import {LitElement, html} from 'lit';

class LitClock extends LitElement {
}

customElements.define('lit-clock', LitClock);

בשלב הבא צריך לאתחל את date ולהצהיר עליו כמאפיין תגובתי פנימי עם @state, כי המשתמשים ברכיב לא יגדירו את date באופן ישיר.

// Lit (TS)
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';

@customElement('lit-clock')
class LitClock extends LitElement {
 
@state() // declares internal reactive prop
 
private date = new Date(); // initialization
}

// Lit (JS)
import {LitElement, html} from 'lit';

class LitClock extends LitElement {
 
static get properties() {
   
return {
     
// declares internal reactive prop
     
date: {state: true}
   
}
 
}

 
constructor() {
   
super();
   
// initialization
   
this.date = new Date();
 
}
}

customElements.define('lit-clock', LitClock);

לאחר מכן, מעבדים את התבנית.

// Lit (JS & TS)
render
() {
  return html`
    <div>
      <h1>Hello, World!</h1>
      <h2>It is ${this.date.toLocaleTimeString()}.</h2>
    </div>
  `;
}

עכשיו מטמיעים את שיטת הסימון.

tick() {
 
this.date = new Date();
}

השלב הבא הוא ההטמעה של componentDidMount. שוב, השפה האנלוגית הזו היא שילוב של firstUpdated ו-connectedCallback. במקרה של הרכיב הזה, קריאה ל-tick באמצעות setInterval לא מחייבת גישה ל-DOM שבתוך הרמה הבסיסית (root). בנוסף, מרווח הזמן ינוקה כאשר הרכיב יוסר מעץ המסמך, כך שאם הוא יצורף מחדש, יהיה צורך להתחיל מחדש את מרווח הזמן. לכן, האפשרות connectedCallback מתאימה יותר במקרה הזה.

// Lit (TS)
@customElement('lit-clock')
class LitClock extends LitElement {
  @state()
  private date = new Date();
  // initialize timerId for TS
  private timerId = -1 as unknown as ReturnType<typeof setTimeout>;

  connectedCallback() {
    super.connectedCallback();
    this.timerId = setInterval(
      () => this.tick(),
      1000
    );
  }

  ...
}

// Lit (JS)
constructor() {
  super();
  // initialization
  this.date = new Date();
  this.timerId = -1; // initialize timerId for JS
}

connectedCallback() {
  super.connectedCallback();
  this.timerId = setInterval(
    () => this.tick(),
    1000
  );
}

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

// Lit (TS & JS)
disconnectedCallback() {
  super.disconnectedCallback();
  clearInterval(this.timerId);
}

סיכום של כל המידע אמור להיראות כך:

// Lit (TS)
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';

@customElement('lit-clock')
class LitClock extends LitElement {
 
@state()
 
private date = new Date();
 
private timerId = -1 as unknown as ReturnType<typeof setTimeout>;

 
connectedCallback() {
   
super.connectedCallback();
   
this.timerId = setInterval(
     
() => this.tick(),
     
1000
   
);
 
}

 
tick() {
   
this.date = new Date();
 
}

 
render() {
   
return html`
      <
div>
        <
h1>Hello, World!</h1>
        <
h2>It is ${this.date.toLocaleTimeString()}.</h2>
      <
/div>
   
`;
 
}

 
disconnectedCallback() {
   
super.disconnectedCallback();
   
clearInterval(this.timerId);
 
}
}

// Lit (JS)
import {LitElement, html} from 'lit';

class LitClock extends LitElement {
 
static get properties() {
   
return {
     
date: {state: true}
   
}
 
}

 
constructor() {
   
super();
   
this.date = new Date();
 
}

 
connectedCallback() {
   
super.connectedCallback();
   
this.timerId = setInterval(
     
() => this.tick(),
     
1000
   
);
 
}

 
tick() {
   
this.date = new Date();
 
}

 
render() {
   
return html`
      <
div>
        <
h1>Hello, World!</h1>
        <
h2>It is ${this.date.toLocaleTimeString()}.</h2>
      <
/div>
   
`;
 
}

 
disconnectedCallback() {
   
super.disconnectedCallback();
   
clearInterval(this.timerId);
 
}
}

customElements.define('lit-clock', LitClock);

7.‏ Hooks

בקטע הזה תלמדו איך לתרגם מושגים של React Hook ל-Lit.

מהם המושגים של הוקים (hooks) לתגובות

קטעי הוק (hooks) לתגובות מאפשרים לרכיבי פונקציות "לנתק" למצב של. יש לזה כמה יתרונות.

  • הם מפשטים את השימוש החוזר בלוגיקה של שמירת מצב
  • עזרה לפיצול רכיב לפונקציות קטנות יותר

בנוסף, ההתמקדות ברכיבים מבוססי-פונקציות טיפלה בבעיות מסוימות בתחביר מבוסס המחלקה של React, כמו:

  • נדרשת מעבר של props מ-constructor אל super
  • אתחול לא מסודר של מאפיינים ב-constructor
    • זו הסיבה שצוינה על ידי צוות React באותו זמן, אבל נפתרה עד ES2019
  • בעיות שנגרמו על ידי this כבר לא מתייחסות לרכיב

תגובות למושגים של הוקים (hooks) ב-Lite

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

// React (at the time of making hooks)
import React from 'react';
import ReactDOM from 'react-dom';

class MyEl extends React.Component {
 
constructor(props) {
   
super(props); // Leaky implementation
   
this.state = {count: 0};
   
this._chart = null; // Deemed messy
 
}

 
render() {
   
return (
     
<>
       
<div>Num times clicked {count}</div>
       
<button onClick={this.clickCallback}>click me</button>
      <
/>
   
);
 
}

 
clickCallback() {
   
// Errors because `this` no longer refers to the component
   
this.setState({count: this.count + 1});
 
}
}

// Lit (ts)
class MyEl extends LitElement {
 
@property({type: Number}) count = 0; // No need for constructor to set state
 
private _chart = null; // Public class fields introduced to JS in 2019

 
render() {
   
return html`
        <
div>Num times clicked ${count}</div>
        <
button @click=${this.clickCallback}>click me</button>`;
 
}

 
private clickCallback() {
   
// No error because `this` refers to component
   
this.count++;
 
}
}

איך Lit מטפלת בבעיות האלה?

  • הפונקציה constructor לא מקבלת ארגומנטים
  • כל קישורי @event מקושרים אוטומטית אל this
  • this ברוב המקרים מתייחס לקובץ העזר של האלמנט המותאם אישית
  • עכשיו אפשר ליצור נכסי כיתה כחברים בכיתה. הפעולה הזו מנקה הטמעות שמבוססות על constructor

בקרים מגיבים

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

בקר תגובתי הוא ממשק של אובייקט שיכול להתחבר למחזור החיים של העדכון של מארח בקר כמו LitElement.

מחזור החיים של ReactiveController ו-reactiveControllerHost הוא:

interface ReactiveController {
  hostConnected(): void;
  hostUpdate(): void;
  hostUpdated(): void;
  hostDisconnected(): void;
}
interface ReactiveControllerHost {
  addController(controller: ReactiveController): void;
  removeController(controller: ReactiveController): void;
  requestUpdate(): void;
  readonly updateComplete: Promise<boolean>;
}

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

import React from 'react';
import ReactDOM from 'react-dom';

class Clock extends React.Component {
 
constructor(props) {
   
super(props);
   
this.state = {date: new Date()};
 
}

 
componentDidMount() {
   
this.timerID = setInterval(
     
() => this.tick(),
     
1000
   
);
 
}

 
componentWillUnmount() {
   
clearInterval(this.timerID);
 
}

 
tick() {
   
this.setState({
     
date: new Date()
   
});
 
}

 
render() {
   
return (
     
<div>
       
<h1>Hello, world!</h1>
       
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
     
</div>
   
);
 
}
}

ReactDOM.render(
  <
Clock />,
 
document.getElementById('root')
);

בדוגמה שלמעלה יש שעון פשוט שמבצע את הפעולות הבאות:

  • הוא מציג את ההודעה "Hello World! " זה אכן" ואז מציג את השעה
  • בכל שנייה השעון יתעדכן
  • בניתוק מרווח הזמן של הסימונים

בניית הרכיב של הפיגומים

קודם צריך להתחיל עם ההצהרה על מחלקת הרכיבים ולהוסיף את הפונקציה render.

// Lit (TS) - index.ts
import {LitElement, html} from 'lit';
import {customElement} from 'lit/decorators.js';

@customElement('my-element')
class MyElement extends LitElement {
 
render() {
   
return html`
      <
div>
        <
h1>Hello, world!</h1>
        <
h2>It is ${'time to get Lit'}.</h2>
      <
/div>
   
`;
 
}
}

// Lit (JS) - index.js
import {LitElement, html} from 'lit';

class MyElement extends LitElement {
 
render() {
   
return html`
      <
div>
        <
h1>Hello, world!</h1>
        <
h2>It is ${'time to get Lit'}.</h2>
      <
/div>
   
`;
 
}
}

customElements.define('my-element', MyElement);

פיתוח אמצעי הבקרה

עכשיו צריך לעבור אל clock.ts, ליצור מחלקה לClockController ולהגדיר את constructor:

// Lit (TS) - clock.ts
import {ReactiveController, ReactiveControllerHost} from 'lit';

export class ClockController implements ReactiveController {
 
private readonly host: ReactiveControllerHost;

 
constructor(host: ReactiveControllerHost) {
   
this.host = host;
   
host.addController(this);
 
}

 
hostConnected() {
 
}

 
private tick() {
 
}

 
hostDisconnected() {
 
}
}

// Lit (JS) - clock.js
export class ClockController {
 
constructor(host) {
   
this.host = host;
   
host.addController(this);
 
}

 
hostConnected() {
 
}

 
tick() {
 
}

 
hostDisconnected() {
 
}
}

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

עכשיו צריך לתרגם את הקריאות החוזרות (callback) של מחזור החיים של React לקריאות חוזרות (callback) של בקר. בקצרה:

  • componentDidMount
    • אל connectedCallback של LitElement
    • אל hostConnected של נאמן המידע
  • ComponentWillUnmount
    • אל disconnectedCallback של LitElement
    • אל hostDisconnected של נאמן המידע

למידע נוסף על תרגום מחזור החיים של React למחזור החיים של Lit, ניתן לעיין במצב הקטע 'מחזור חיים'.

בשלב הבא, מטמיעים את הקריאה החוזרת של hostConnected ואת ה-methods tick, ומנקים את המרווח ב-hostDisconnected כמו בדוגמה במצב ו- הקטע 'מחזור חיים'.

// Lit (TS) - clock.ts
export class ClockController implements ReactiveController {
  private readonly host: ReactiveControllerHost;
  private interval = 0 as unknown as ReturnType<typeof setTimeout>;
  date = new Date();

  constructor(host: ReactiveControllerHost) {
    this.host = host;
    host.addController(this);
  }

  hostConnected() {
    this.interval = setInterval(() => this.tick(), 1000);
  }

  private tick() {
    this.date = new Date();
  }

  hostDisconnected() {
    clearInterval(this.interval);
  }
}

// Lit (JS) - clock.js
export class ClockController {
  interval = 0;
  host;
  date = new Date();

  constructor(host) {
    this.host = host;
    host.addController(this);
  }

  hostConnected() {
    this.interval = setInterval(() => this.tick(), 1000);
  }

  tick() {
    this.date = new Date();
  }

  hostDisconnected() {
    clearInterval(this.interval);
  }
}

שימוש בשלט רחוק

כדי להשתמש בבקר השעון, צריך לייבא את השלט רחוק ולעדכן את הרכיב ב-index.ts או ב-index.js.

// Lit (TS) - index.ts
import {LitElement, html, ReactiveController, ReactiveControllerHost} from 'lit';
import {customElement} from 'lit/decorators.js';
import {ClockController} from './clock.js';

@customElement('my-element')
class MyElement extends LitElement {
 
private readonly clock = new ClockController(this); // Instantiate

 
render() {
   
// Use controller
   
return html`
      <
div>
        <
h1>Hello, world!</h1>
        <
h2>It is ${this.clock.date.toLocaleTimeString()}.</h2>
      <
/div>
   
`;
 
}
}

// Lit (JS) - index.js
import {LitElement, html} from 'lit';
import {ClockController} from './clock.js';

class MyElement extends LitElement {
 
clock = new ClockController(this); // Instantiate

 
render() {
   
// Use controller
   
return html`
      <
div>
        <
h1>Hello, world!</h1>
        <
h2>It is ${this.clock.date.toLocaleTimeString()}.</h2>
      <
/div>
   
`;
 
}
}

customElements.define('my-element', MyElement);

כדי להשתמש בבקר, צריך ליצור מופע של הבקר על ידי העברת הפניה למארח של הבקר (שהוא הרכיב <my-element>), ולאחר מכן להשתמש בבקר בשיטה render.

הפעלת רינדור מחדש בבקר

שימו לב שתוצג לכם השעה, אבל השעה לא מתעדכנת. הסיבה לכך היא שהבקר מגדיר תאריך בכל שנייה, אבל המארח לא מתעדכן. הסיבה לכך היא שה-date משתנה במחלקה ClockController וכבר לא ברכיב. כלומר, אחרי שמגדירים את date בבקר, צריך לקבל בקשה למארח להפעיל את מחזור החיים של העדכון באמצעות host.requestUpdate().

// Lit (TS & JS) - clock.ts / clock.js
private tick() {
  this.date = new Date();
  this.host.requestUpdate();
}

עכשיו השעון אמור לתקתק!

להשוואה מפורטת יותר בין תרחישים לדוגמה נפוצים עם קטעי הוק (hooks), אפשר לעיין בקטע נושאים מתקדמים – תוכן מושך.

8.‏ ילדים

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

מכונות מזל בנות

יחידות קיבולת (Slot) מאפשרות הרכבה בכך שהן מאפשרות להציב רכיבים בתוך רכיב.

בתגובה, ילדים עוברים בירושה דרך אביזרים. משבצת ברירת המחדל היא props.children והפונקציה render מגדירה את המיקום של משבצת ברירת המחדל. לדוגמה:

const MyArticle = (props) => {
 return <article>{props.children}</article>;
};

חשוב לזכור ש-props.children הם רכיבי React ולא רכיבי HTML.

ב-Lit, ילדים מורכבים בפונקציית העיבוד עם רכיבים של משבצות. שימו לב שצאצאים לא עוברים בירושה כמו React. ב-Lit, צאצאים הם רכיבי HTMLElements שמצורפים למשבצות. הקובץ המצורף הזה נקרא הקרנה.

@customElement("my-article")
export class MyArticle extends LitElement {
  render() {
    return html`
      <article>
        <slot></slot>
      </article>
   `;
  }
}

יחידות קיבולת (Slot)

בתגובה, הוספה של כמה משבצות היא למעשה ירושה של עוד אביזרים.

const MyArticle = (props) => {
  return (
    <article>
      <header>
        {props.headerChildren}
      </header>
      <section>
        {props.sectionChildren}
      </section>
    </article>
  );
};

באופן דומה, הוספה של עוד רכיבי <slot> יוצרת מקומות נוספים ב-Lit. מוגדרים מספר משבצות עם המאפיין name: <slot name="slot-name">. כך הילדים יכולים להצהיר איזו משבצת יוקצה להם.

@customElement("my-article")
export class MyArticle extends LitElement {
  render() {
    return html`
      <article>
        <header>
          <slot name="headerChildren"></slot>
        </header>
        <section>
          <slot name="sectionChildren"></slot>
        </section>
      </article>
   `;
  }
}

תוכן ברירת המחדל של משבצת הזמן

מיקומי המשבצות יציגו את עץ המשנה שלהם אם לא צפויים צמתים לחריץ הזה. כאשר צמתים צפויים לעמוד במיקום, הוא לא יציג את עץ המשנה שלו, ובמקום זאת יציג צמתים חזויים.

@customElement("my-element")
export class MyElement extends LitElement {
  render() {
    return html`
      <section>
        <div>
          <slot name="slotWithDefault">
            <p>
             This message will not be rendered when children are attached to this slot!
            <p>
          </slot>
        </div>
      </section>
   `;
  }
}

הקצאת ילדים למשבצות

בתגובה, צאצאים מוקצים למשבצות באמצעות המאפיינים של רכיב. בדוגמה הבאה, רכיבי התגובה מועברים אל המאפיינים headerChildren ו-sectionChildren.

const MyNewsArticle = () => {
 return (
   <MyArticle
     headerChildren={<h3>Extry, Extry! Read all about it!</h3>}
     sectionChildren={<p>Children are props in React!</p>}
   />
 );
};

ב-Lit, ילדים מוקצים למשבצות באמצעות המאפיין slot.

@customElement("my-news-article")
export class MyNewsArticle extends LitElement {
  render() {
    return html`
      <my-article>
        <h3 slot="headerChildren">
          Extry, Extry! Read all about it!
        </h3>
        <p slot="sectionChildren">
          Children are composed with slots in Lit!
        </p>
      </my-article>
   `;
  }
}

אם אין משבצת ברירת מחדל (למשל, <slot>) ואין משבצת עם מאפיין name (למשל <slot name="foo">) שתואם למאפיין slot של הצאצאים של הרכיב המותאם אישית (למשל <div slot="foo">), לא תהיה צפי של הצומת הזה ולא יוצג.

9.‏ הפניות

מדי פעם, ייתכן שמפתח יצטרך לגשת ל-API של HTMLElement.

בקטע הזה נסביר איך ליצור הפניות לרכיבים ב-Lit.

הפניות לתגובות

רכיב React עובר טרנספורמציה לסדרה של קריאות לפונקציות שיוצרות DOM וירטואלי כשמפעילים אותו. ה-DOM הווירטואלי הזה מפורש על ידי ReactDOM ומעבד את HTMLElements.

ב-React, קובצי עזר הם מקום בזיכרון שמכיל HTMLElement שנוצר.

const RefsExample = (props) => {
 const inputRef = React.useRef(null);
 const onButtonClick = React.useCallback(() => {
   inputRef.current?.focus();
 }, [inputRef]);

 return (
   <div>
     <input type={"text"} ref={inputRef} />
     <br />
     <button onClick={onButtonClick}>
       Click to focus on the input above!
     </button>
   </div>
 );
};

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

  • עיבוד קלט של טקסט ריק ולחצן עם טקסט
  • מיקוד הקלט לאחר לחיצה על הלחצן

אחרי העיבוד הראשוני, התגובה inputRef.current תוגדר ל-HTMLInputElement שנוצר באמצעות המאפיין ref.

Lit "References" (קובצי עזר) עם @query

Lit גרה קרוב לדפדפן ויוצרת הפשטה קצרה מאוד של תכונות הדפדפן המקוריות.

התגובה המקבילה ל-refs ב-Lit היא רכיב ה-HTMLElement שהוחזר על-ידי המעצבים @query ו-@queryAll.

@customElement("my-element")
export class MyElement extends LitElement {
  @query('input') // Define the query
  inputEl!: HTMLInputElement; // Declare the prop

  // Declare the click event listener
  onButtonClick() {
    // Use the query to focus
    this.inputEl.focus();
  }

  render() {
    return html`
      <input type="text">
      <br />
      <!-- Bind the click listener -->
      <button @click=${this.onButtonClick}>
        Click to focus on the input above!
      </button>
   `;
  }
}

בדוגמה שלמעלה, רכיב ה-Lit מבצע את הפעולות הבאות:

  • מגדיר נכס ב-MyElement באמצעות כלי העיצוב @query (יצירת getter של HTMLInputElement).
  • מצהיר ומצרף קריאה חוזרת (callback) של אירוע קליק בשם onButtonClick.
  • התמקדות בקלט בלחיצה על הלחצן

ב-JavaScript, מעצבי העיצוב @query ו-@queryAll מניבים ביצועים של querySelector ו-querySelectorAll בהתאמה. זהו JavaScript המקביל ל-@query('input') inputEl!: HTMLInputElement;

get inputEl() {
 
return this.renderRoot.querySelector('input');
}

אחרי שרכיב ה-Lit מבצע את התבנית של ה-method render לשורש של my-element, העיצוב של @query מאפשר עכשיו ל-inputEl להחזיר את רכיב input הראשון שנמצא ברמה הבסיסית של העיבוד. הפונקציה תחזיר את הערך null אם @query לא מצליח למצוא את הרכיב שצוין.

אם היו מספר רכיבי input ברמה הבסיסית (root) של העיבוד, הפונקציה @queryAll תחזיר רשימת צמתים.

10.‏ מצב גישור

בקטע הזה נסביר איך לתווך בין המצבים בין רכיבים ב-Lit.

רכיבים לשימוש חוזר

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

const CounterButton = (props) => {
  const label = props.step < 0
    ? `- ${-1 * props.step}`
    : `+ ${props.step}`;


  return (
    <button
      onClick={() =>
        props.addToCounter(props.step)}>{label}</button>
  );
};

בדוגמה שלמעלה, רכיב React מבצע את הפעולות הבאות:

  • יצירת תווית על סמך הערך props.step.
  • הצגת לחצן עם התווית +step או -step
  • עדכון רכיב ההורה על ידי קריאה ל-props.addToCounter עם props.step כארגומנט בקליק

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

@customElement('counter-button')
export class CounterButton extends LitElement {
  @property({type: Number}) step: number = 0;

  onClick() {
    const event = new CustomEvent('update-counter', {
      bubbles: true,
      detail: {
        step: this.step,
      }
    });

    this.dispatchEvent(event);
  }

  render() {
    const label = this.step < 0
      ? `- ${-1 * this.step}`  // "- 1"
      : `+ ${this.step}`;      // "+ 1"

    return html`
      <button @click=${this.onClick}>${label}</button>
    `;
  }
}

בדוגמה שלמעלה, רכיב Lit יבצע את הפעולות הבאות:

  • יצירת המאפיין התגובה step
  • שליחת אירוע מותאם אישית בשם update-counter שמכיל את הערך step של הרכיב בזמן לחיצה

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

רכיבים עם שמירת מצב

בתגובה, מקובל להשתמש בקודים (hooks) לניהול מצב. אפשר ליצור רכיב MyCounter באמצעות שימוש חוזר ברכיב CounterButton. שימו לב איך addToCounter מועבר לשני המופעים של CounterButton.

const MyCounter = (props) => {
 const [counterSum, setCounterSum] = React.useState(0);
 const addToCounter = useCallback(
   (step) => {
     setCounterSum(counterSum + step);
   },
   [counterSum, setCounterSum]
 );

 return (
   <div>
     <h3>&Sigma;: {counterSum}</h3>
     <CounterButton
       step={-1}
       addToCounter={addToCounter} />
     <CounterButton
       step={1}
       addToCounter={addToCounter} />
   </div>
 );
};

הדוגמה שלמעלה מבצעת את הפעולות הבאות:

  • יוצר מצב count.
  • הפונקציה יוצרת קריאה חוזרת (callback) שמוסיפה מספר למצב count.
  • CounterButton משתמש ב-addToCounter כדי לעדכן את count עד step בכל קליק.

אפשר להשיג הטמעה דומה של MyCounter ב-Lit. שימו לב איך addToCounter לא מועבר אל counter-button. במקום זאת, הקריאה החוזרת משויכת כרכיב event listener לאירוע @update-counter ברכיב הורה.

@customElement("my-counter")
export class MyCounter extends LitElement {
  @property({type: Number}) count = 0;

  addToCounter(e: CustomEvent<{step: number}>) {
    // Get step from detail of event or via @query
    this.count += e.detail.step;
  }

  render() {
    return html`
      <div @update-counter="${this.addToCounter}">
        <h3>&Sigma; ${this.count}</h3>
        <counter-button step="-1"></counter-button>
        <counter-button step="1"></counter-button>
      </div>
    `;
  }
}

הדוגמה שלמעלה מבצעת את הפעולות הבאות:

  • יצירת מאפיין תגובתי בשם count שיעדכן את הרכיב כאשר הערך ישתנה
  • קישור הקריאה החוזרת של addToCounter ל-event listener של @update-counter
  • המערכת מעדכנת את count על ידי הוספת הערך שנמצא בdetail.step של האירוע update-counter
  • מגדיר את הערך step של counter-button באמצעות המאפיין step

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

הגישה הזו פועלת בהתאם לשיטות המומלצות ופועלת בהתאם למטרה של Lit למתן תמיכה ברכיבי אינטרנט בפלטפורמות שונות.

11.‏ עיצוב

בקטע הזה נסביר על עיצוב ב-Lite.

עיצוב

ב-Lit יש כמה דרכים לעצב רכיבים וגם פתרון מובנה.

סגנונות מוטבעים

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

import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators.js';

@customElement('my-element')
class MyElement extends LitElement {
 
render() {
   
return html`
      <
div>
        <
h1 style="color:orange;">This text is orange</h1>
        <
h1 style="color:rebeccapurple;">This text is rebeccapurple</h1>
      <
/div>
   
`;
 
}
}

בדוגמה שלמעלה יש 2 כותרות עם סגנון מוטבע.

עכשיו מייבאים ומקשרים גבול מ-border-color.js לטקסט הכתום:

...
import borderColor from './border-color.js';

...

html`
 
...
  <
h1 style="color:orange;${borderColor}">This text is orange</h1>
 
...`

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

styleMap

ההנחיה styleMap מאפשרת להשתמש ב-JavaScript בקלות רבה יותר להגדרת סגנונות מוטבעים. לדוגמה:

import {LitElement, html, css} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {styleMap} from 'lit/directives/style-map.js';

@customElement('my-element')
class MyElement extends LitElement {
 
@property({type: String})
 
color = '#000'

 
render() {
   
// Define the styleMap
   
const headerStyle = styleMap({
     
'border-color': this.color,
   
});

   
return html`
      <
div>
        <
h1
         
style="border-style:solid;
          <
!-- Use the styleMap -->
         
border-width:2px;${headerStyle}">
         
This div has a border color of ${this.color}
        <
/h1>
        <
input
         
type="color"
         
@input=${e => (this.color = e.target.value)}
         
value="#000">
      <
/div>
   
`;
 
}
}

הדוגמה שלמעלה מבצעת את הפעולות הבאות:

  • מציגה h1 עם גבול ובוחר צבעים
  • שינוי border-color לערך מבוחר הצבעים

בנוסף, יש styleMap שמשמש להגדרת הסגנונות של h1. התחביר של styleMap דומה לתחביר של קישור המאפיינים style של React.

CSSResult

הדרך המומלצת לעצב רכיבים היא להשתמש בליטרל של תבנית מתויגת css.

import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators.js';

const ORANGE = css`orange`;

@customElement('my-element')
class MyElement extends LitElement {
 
static styles = [
   
css`
     
#orange {
       
color: ${ORANGE};
     
}

     
#purple {
       
color: rebeccapurple;
     
}
   
`
 
];

 
render() {
   
return html`
      <
div>
        <
h1 id="orange">This text is orange</h1>
        <
h1 id="purple">This text is rebeccapurple</h1>
      <
/div>
   
`;
 
}
}

הדוגמה שלמעלה מבצעת את הפעולות הבאות:

  • מצהירה על ליטרל של תבנית מתויגת של CSS עם קישור
  • הגדרת הצבעים של שתי מחרוזות h1 עם מזהים

יתרונות השימוש בתג התבנית css:

  • ניתוח פעם אחת לכל כיתה לעומת לכל מופע
  • הוטמע מתוך מחשבה על שימוש חוזר במודולים
  • יכולים להפריד בקלות סגנונות לקבצים משלהם
  • תואם ל-polyfill של המאפיינים המותאמים אישית של CSS

בנוסף, שימו לב לתג <style> ב-index.html:

<!-- index.html -->
<style>
  h1 {
    color: red !important;
  }
</style>

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

תגי סגנון

אפשר גם פשוט להטביע תגי <style> בתבניות. הדפדפן יבטל את הכפילויות של תגי הסגנון האלה, אך על ידי הצבתם בתבניות שלך, הם ינותחו לכל מופע של רכיב, ולא לכל כיתה כמו במקרה של התבנית המתויגת css. בנוסף, ביטול הכפילויות של CSSResult בדפדפן הרבה יותר מהיר.

גם שימוש ב-<link rel="stylesheet"> בתבנית יכול להתאים לסגנונות, אבל זה לא מומלץ כי הוא עלול לגרום להבהוב ראשוני של תוכן ללא עיצוב (FOUC).

12.‏ נושאים מתקדמים (אופציונלי)

JSX ו-JSX טמפל

ספרות ואותיות DOM וירטואלי

Lit-html לא כולל DOM קונבנציונלי של DOM ששונה כל צומת בנפרד. במקום זאת, היא משתמשת בתכונות ביצועים מובנות למפרט ליטרל של תבנית מתויגת ב-ES2015. ליטרלים של תבנית מתויגות הם מחרוזות מילוליות של תבנית עם פונקציות של תגים.

הנה דוגמה לליטרל של תבנית:

const str = 'string';
console.log(`This is a template literal ${str}`);

הנה דוגמה לליטרל של תבנית מתויגת:

const tag = (strings, ...values) => ({strings, values});
const f = (x) => tag`hello ${x} how are you`;
console.log(f('world')); // {strings: ["hello ", " how are you"], values: ["world"]}
console.log(f('world').strings === f(1 + 2).strings); // true

בדוגמה שלמעלה, התג הוא הפונקציה tag והפונקציה f מחזירה הפעלה של ליטרל תבנית מתויגת.

חלק גדול מהקסם של הביצועים ב-Lit מגיע כתוצאה מכך שמערכי המחרוזת שמועברים לפונקציית התג כוללים את אותו מצביע (כפי שמוצג ב-console.log השני). הדפדפן לא יוצר מחדש מערך strings חדש בכל הפעלה של פונקציית תג, כי הוא משתמש באותו אופן של תבנית (כלומר, באותו מיקום ב-AST). לכן הקישור, הניתוח ושמירת התבניות במטמון של Lit יכולים לנצל את התכונות האלה בלי שיהיו הבדלים רבים מהתקורה בסביבת זמן הריצה.

ההתנהגות המובנית הזו בדפדפן של ייצוגי מילים של תבניות מתויגות נותנת ל-Lit יתרון משמעותי בביצועים. רוב יחידות ה-DOM הווירטואליות המסורתיות מבצעות את רוב עבודתן ב-JavaScript. עם זאת, ליטרלים של תבניות מתויגות עושים את רוב ההבדל ב-C++ של הדפדפן.

אם אתם רוצים להתחיל להשתמש בליטרלים של תבניות עם תגים של HTML באמצעות React או Preact, צוות Lit ממליץ על הספרייה htm.

עם זאת, כמו באתר Google Codelabs ובכמה עורכי קוד דיגיטליים, תבחינו שהדגשת תחביר מילולי של תבנית מתויגת אינה נפוצה מאוד. יש סביבות פיתוח משולבות (IDE) ועורכי טקסט שתומכים בהן כברירת מחדל, למשל Atom ומדגיש הקוד של GitHub ב-GitHub. הצוות של Lit גם עובד בשיתוף פעולה הדוק עם הקהילה כדי לתחזק פרויקטים כמו lit-plugin, שהוא פלאגין של VS Code שמוסיף הדגשת תחביר, בדיקת סוגים ובהירות לפרויקטים.

ספרות ואותיות JSX + תגובה DOM

JSX לא פועל בדפדפן אלא משתמש במעבד מראש כדי להמיר JSX לקריאות פונקציות של JavaScript (בדרך כלל באמצעות Babel).

לדוגמה, Babel ישנה את הערך הזה:

const element = <div className="title">Hello World!</div>;
ReactDOM.render(element, mountNode);

בתוך זה:

const element = React.createElement('div', {className: 'title'}, 'Hello World!');
ReactDOM.render(element, mountNode);

לאחר מכן התגובה DOM לוקחת את הפלט של ה-React ומתרגמת אותו ל-DOM בפועל – מאפיינים, מאפיינים, פונקציות event listener והכל.

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

<!DOCTYPE html>
<html>
 
<head>
   
<script type="module">
     
import {html, render} from 'https://cdn.skypack.dev/lit';

     
render(
       
html`<div>Hello World!</div>`,
       
document.querySelector('.root')
     
)
   
</script>
 
</head>
 
<body>
   
<div class="root"></div>
 
</body>
</html>

בנוסף, מכיוון שמערכת התבניות של Lit, lit-html, לא משתמשת ב-DOM רגיל קונבנציונלי אלא משתמשת ישירות ב-DOM API, הגודל של Lit 2 מוקטן והופך באמצעות gzip בהשוואה ל-React (2.8kb) + 40kb מוקטן ו-gizip של תגובת ה-DOM (39.4kb).

אירועים

התגובה משתמשת במערכת סינתטית של אירועים. כלומר, צריך להגדיר כל אירוע שישמש בכל רכיב, ולספק ערך השווה ל-event listener לכל סוג של צומת. כתוצאה מכך, ל-JSX אין שיטה להגדרת האזנה לאירועים בהתאמה אישית, והמפתחים חייבים להשתמש ב-ref ולאחר מכן באופן הכרחי להחיל listener. כשמשלבים ספריות שלא מבוססות על React, צריך לכתוב wrapper ספציפי ל-React.

Lit-html ניגש ישירות ל-DOM ומשתמש באירועים נייטיב, כך שאפשר להוסיף פונקציות event listener בלחיצה על @event-name=${eventNameListener}. פירוש הדבר הוא שמתבצע פחות ניתוח של זמן ריצה להוספת פונקציות event listener וגם להפעלה של אירועי הפעלה.

רכיבים אביזרים

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

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

יש כמה יתרונות לרכיבים מותאמים אישית כמערכת רכיבים:

  • מקורי לדפדפן ואין צורך בכלים כלשהם
  • התאמה לכל API של דפדפן מ-innerHTML ומ-document.createElement עד querySelector
  • בדרך כלל ניתן להשתמש ב-frameworks שונות
  • אפשר להירשם באופן מדורג ב-customElements.define וב-"hydrate" DOM

יש חסרונות של רכיבים מותאמים אישית בהשוואה לרכיבי תגובה:

  • לא ניתן ליצור רכיב מותאם אישית בלי להגדיר מחלקה (כלומר, לא ניתן ליצור רכיבים פונקציונליים דמויי JSX)
  • חייב להכיל תג סוגר
    • הערה: למרות שהספקים של דפדפן הנוחות למפתחים נוטים להתחרט על המפרט של התגים לסגירה עצמית, ולכן מפרטים חדשים יותר בדרך כלל לא כוללים תגים לסגירה עצמית.
  • הוספת צומת נוסף לעץ ה-DOM שעלול לגרום לבעיות בפריסה
  • נדרש רישום באמצעות JavaScript

ב-Lite עוברים בין רכיבים מותאמים אישית למערכת רכיבים מותאמת אישית, כי הרכיבים המותאמים אישית מובנים בדפדפן, וצוות Lit סבור שהיתרונות של עיבוד חלק מה-framework עולים על היתרונות שמספקים שכבת הפשטה של רכיב. למעשה, מאמציו של צוות Lit בתחום lit-ssr התגברו על הבעיות העיקריות ברישום ל-JavaScript. בנוסף, חברות מסוימות כמו GitHub מנצלות את האפשרות של רישום מדורגת של רכיבים מותאמים אישית כדי לשפר את הדפים בהדרגה עם סגנון אופציונלי.

העברת נתונים לרכיבים מותאמים אישית

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

לדוגמה, עם הגדרת ה-LitElement הבאה:

// data-test.ts
import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';

@customElement('data-test')
class DataTest extends LitElement {
 
@property({type: Number})
 
num = 0;

 
@property({attribute: false})
 
data = {a: 0, b: null, c: [html`<div>hello</div>`, html`<div>world</div>`]}

 
render() {
   
return html`
      <
div>num + 1 = ${this.num + 1}</div>
      <
div>data.a = ${this.data.a}</div>
      <
div>data.b = ${this.data.b}</div>
      <
div>data.c = ${this.data.c}</div>`;
 
}
}

מאפיין תגובתי פרימיטיבי num מוגדר שימיר את ערך המחרוזת של מאפיין ל-number, ולאחר מכן נוצר מבנה נתונים מורכב עם attribute:false, שמשבית את הטיפול במאפיינים של Lit.

כך מעבירים נתונים לרכיב המותאם אישית הזה:

<head>
 
<script type="module">
   
import './data-test.js'; // loads element definition
   
import {html} from './data-test.js';

   
const el = document.querySelector('data-test');
   
el.data = {
     
a: 5,
     
b: null,
     
c: [html`<div>foo</div>`,html`<div>bar</div>`]
   
};
 
</script>
</head>
<body>
 
<data-test num="5"></data-test>
</body>

מדינה ו- מחזור חיים

קריאות חוזרות אחרות למחזור החיים של React

static getDerivedStateFromProps

אין מקבילה ב-Lit כי אביזרים ומצב הם אותם מאפייני מחלקה

shouldComponentUpdate

  • ערך הטקסט המקביל הוא shouldUpdate
  • בוצעה קריאה ברינדור הראשון, בניגוד ל-React
  • דומה בפונקציה של shouldComponentUpdate של React

getSnapshotBeforeUpdate

ב-Lit, getSnapshotBeforeUpdate דומה גם ל-update וגם ל-willUpdate

willUpdate

  • התקשרת לפני update
  • שלא כמו getSnapshotBeforeUpdate, הקריאה אל willUpdate מתבצעת לפני render
  • שינויים במאפיינים הראקטיביים ב-willUpdate לא יפעילו מחדש את מחזור העדכון
  • מקום טוב לחשב בו ערכי נכסים שתלויים בנכסים אחרים ושנעשה בהם שימוש בשאר תהליך העדכון
  • השיטה הזו נקראת בשרת ב-SSR, לכן לא מומלץ לגשת ל-DOM

update

  • התקשרת אחרי willUpdate
  • בניגוד ל-getSnapshotBeforeUpdate, הקריאה אל update מתבצעת לפני render
  • שינויים במאפיינים הראקטיביים ב-update לא יפעילו מחדש את מחזור העדכון אם הם שונו לפני הקריאה ל-super.update
  • מקום טוב לקלוט מידע מה-DOM שמקיף את הרכיב לפני שהפלט המעובד מחויב ל-DOM
  • לשיטה הזו לא מתבצעת קריאה בשרת ב-SSR

קריאות חוזרות אחרות למחזור החיים של Lit

יש כמה קריאות חוזרות (callback) במחזור החיים שלא הוזכרו בקטע הקודם כי אין להן אנלוגיות ב-React. אלו הם:

attributeChangedCallback

היא מופעלת כשאחד מה-observedAttributes של הרכיב משתנה. גם observedAttributes וגם attributeChangedCallback הם חלק ממפרט הרכיבים המותאמים אישית, והם הוטמעו על ידי Lit באופן כללי כדי לספק API של מאפיין לרכיבי Lit.

adoptedCallback

מופעל כשהרכיב מועבר למסמך חדש, למשל מ-documentFragment של HTMLTemplateElement עד ל-document הראשי. הקריאה החוזרת (callback) הזו היא גם חלק ממפרט הרכיבים המותאמים אישית, ויש להשתמש בה רק בתרחישים מתקדמים לדוגמה כשהרכיב משנה מסמכים.

שיטות ומאפיינים אחרים של מחזור החיים

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

updateComplete

מדובר בשדה Promise שמסתיים אחרי סיום העדכון של הרכיב כי מחזורי החיים של העדכון והרינדור הם אסינכרוניים. דוגמה:

async nextButtonClicked() {
 
this.step++;
 
// Wait for the next "step" state to render
  await
this.updateComplete;
 
this.dispatchEvent(new Event('step-rendered'));
}

getUpdateComplete

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

class MyElement extends LitElement {
  ...
  async getUpdateComplete() {
    await super.getUpdateComplete();
    await this.myChild.updateComplete;
  }
}

performUpdate

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

hasUpdated

מאפיין זה הוא true אם הרכיב עודכן פעם אחת לפחות.

isConnected

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

תצוגה חזותית של מחזור חיים של עדכון Lit

מחזור החיים של העדכון כולל 3 חלקים:

  • עדכון מראש
  • עדכון
  • אחרי עדכון

עדכון מקדים

גרף אציקלי מכוון של צמתים עם שמות קריאה חוזרת (callback). כדי requestUpdate. @property to Property Setter. IdChangedCallback ל-property Setter השדה &#39;רכיב מגדיר&#39; של הנכס השתנה. בוצע שינוי ל-requestUpdate. requestUpdate נקודות לתרשים הבא של מחזור החיים.

אחרי requestUpdate ממתין עדכון מתוזמן.

עדכון

גרף אציקלי מכוון של צמתים עם שמות קריאה חוזרת (callback). חץ מהתמונה הקודמת של הנקודות לפני העדכון של מחזור החיים לביצוע paymentsUpdate. PerformanceUpdate to shouldUpdate. צריך לעדכן נקודות גם ל&#39;עדכון מלא אם false&#39; וגם ל-&#39;updateUpdate&#39;. אעדכן כדי לעדכן. עדכון גם לעיבוד וגם לתרשים הבא של מחזור החיים לאחר העדכון. רינדור נקודות גם לתרשים הבא של מחזור החיים, לאחר העדכון.

אחרי העדכון

גרף אציקלי מכוון של צמתים עם שמות קריאה חוזרת (callback). חץ מהתמונה הקודמת של הנקודות במחזור החיים של העדכון אל firstUpdated. עודכן לראשונה לעודכן. עודכן ל-updatecomplete.

Hooks

למה קטעי הוק (hooks)

נוספו קטעי הוק (hooks) ל-React לצורך תרחישים לדוגמה של רכיבי פונקציה פשוטים שדרשו מצב נדרש. במקרים פשוטים רבים, רכיבי פונקציה עם קטעי הוק נוטים להיות פשוטים וקריאים יותר מרכיבי המחלקה המקבילים. עם זאת, כשמכניסים עדכוני מצב אסינכרוניים וגם מעבירים נתונים בין קטעי הוק (hooks) או אפקטים, דפוס ה-hooks בדרך כלל לא מספיק, ופתרון מבוסס-מחלקה כמו בקרים ריאקטיביים נוטה לבלוט.

קטעי הוק (hooks) של בקשות API בקרים

במקרים רבים כותבים הוק (hook) שמבקש נתונים מ-API. לדוגמה, ניקח את הרכיב הזה של פונקציית React שמבצע את הפעולות הבאות:

  • index.tsx
    • עיבוד הטקסט
    • הצגת התשובה של useAPI
      • מזהה משתמש + שם משתמש
      • הודעת שגיאה
        • 404 כשמגיע למשתמש 11 (מבחינת התכנון)
        • ביטול השגיאה אם אחזור ה-API בוטל
      • טוען הודעה
    • הצגת לחצן פעולה
      • המשתמש הבא: אחזור ה-API של המשתמש הבא
      • ביטול: ביטול האחזור של ה-API והצגת שגיאה
  • useApi.tsx
    • הגדרת הוק (hook) מותאם אישית של useApi
    • פעולה אסינכרונית תאחזר אובייקט משתמש מ-API
    • אזכורים:
      • שם משתמש
      • האם האחזור נטען
      • הודעות שגיאה כלשהן
      • קריאה חוזרת (callback) לביטול האחזור
    • ביטולי שליפות מתבצעות אם מסירים את המטען

הנה הטמעה של Lit + Reactive Controller.

מסקנות:

  • בקרים תגובתיים הם לרוב כמו קטעי הוק (hooks) בהתאמה אישית
  • העברת נתונים שאינם ניתנים לעיבוד בין קריאה חוזרת לבין אפקטים
    • התגובה משתמשת ב-useRef כדי להעביר נתונים בין useEffect ל-useCallback
    • Google Lit משתמשת בנכס כיתה פרטית
    • התגובה היא בעצם שמחקה את ההתנהגות של נכס כיתה פרטית

בנוסף, אם אתם מאוד אוהבים את התחביר של רכיב פונקציית React עם הוקים (hooks), אבל אותה סביבה אבסולוטית של Lit, צוות Lit ממליץ מאוד על הספרייה Haunted.

ילדים

משבצת ברירת המחדל

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

@customElement("my-element")
export class MyElement extends LitElement {
  render() {
    return html`
      <section>
        <div>
          <slot></slot>
        </div>
        <div>
          <slot name="custom-slot"></slot>
        </div>
      </section>
   `;
  }
}

@customElement("my-app")
export class MyApp extends LitElement {
  render() {
    return html`
      <my-element>
        <p slot="custom-slot">
          This paragraph will be placed in the custom-slot!
        </p>
        <p>
          This paragraph will be placed in the unnamed default slot!
        </p>
      </my-element>
   `;
  }
}

עדכונים לגבי משבצות

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

@customElement("my-element")
export class MyElement extends LitElement {
  onSlotChange(e: Event) {
    const slot = this.shadowRoot.querySelector('slot');
    console.log(slot.assignedNodes({flatten: true}));
  }

  render() {
    return html`
      <section>
        <div>
          <slot @slotchange="{this.onSlotChange}"></slot>
        </div>
      </section>
   `;
  }
}

הפניות

יצירת קובצי עזר

גם Lit וגם React חושפים הפניה ל-HTMLElement אחרי קריאה לפונקציות render שלהן. אבל כדאי לבדוק איך React ו-Lit מחברים את ה-DOM שמוחזר מאוחר יותר באמצעות מעצב Lit @query או באמצעות React Reference.

React הוא צינור עיבוד נתונים פונקציונלי שיוצר רכיבי React ולא HTMLElements. מכיוון שהוצהר על Ref לפני עיבוד של HTMLElement, מוקצה מקום בזיכרון. לכן הערך null הוא הערך הראשוני של ההפניה, כי רכיב ה-DOM בפועל עדיין לא נוצר (או עבר רינדור), כלומר useRef(null).

אחרי ש-ReactDOM ממיר רכיב React ל-HTMLElement, הוא מחפש מאפיין בשם ref ב-ReactComponent. אם האפשרות זמינה, ReactDOM ממקמת את ההפניה של HTMLElement אל ref.current.

LitElement משתמש בפונקציית התג html של התבנית מ-lit-html כדי להרכיב רכיב תבנית מתחת למכסה הקדמי. LitElement חותם את תוכן התבנית ב-DOM DOM של רכיב מותאם אישית לאחר העיבוד. ה-DOM של הצללית הוא עץ DOM עם היקף שמוקף על ידי שורש הצל. בשלב הבא, כלי העיצוב @query יוצר נכס getter לנכס, שלמעשה מבצע this.shadowRoot.querySelector ברמה הבסיסית (root) של ההיקף.

שאילתה של מספר רכיבים

בדוגמה הבאה, כלי העיצוב @queryAll מחזיר את שתי הפסקאות מהשורש של הצללית כ-NodeList.

@customElement("my-element")
export class MyElement extends LitElement {
  @queryAll('p')
  paragraphs!: NodeList;

  render() {
    return html`
      <p>Hello, world!</p>
      <p>How are you?</p>
   `;
  }
}

למעשה, הפונקציה @queryAll יוצרת רכיב getter עבור paragraphs שמחזיר את התוצאות של this.shadowRoot.querySelectorAll(). ב-JavaScript, אפשר להצהיר על רכיב קריאה שיבצע את אותה מטרה:

get paragraphs() {
 
return this.renderRoot.querySelectorAll('p');
}

רכיבים של שינוי שאילתה

העיצוב של @queryAsync מתאים יותר לטפל בצומת שיכול להשתנות בהתאם למצב של מאפיין רכיב אחר.

בדוגמה הבאה, @queryAsync ימצא את רכיב הפסקה הראשונה. עם זאת, רכיב פסקה יוצג רק כאשר renderParagraph ייצור מספר אי-זוגי באופן אקראי. ההוראה @queryAsync תחזיר הבטחה שתפתור את הבעיה כשהפסקה הראשונה תהיה זמינה.

@customElement("my-dissappearing-paragraph")
export class MyDisapppearingParagraph extends LitElement {
  @queryAsync('p')
  paragraph!: Promise<HTMLElement>;

  renderParagraph() {
    const randomNumber = Math.floor(Math.random() * 10)
    if (randomNumber % 2 === 0) {
      return "";
    }

    return html`<p>This checkbox is checked!`
  }

  render() {
    return html`
      ${this.renderParagraph()}
   `;
  }
}

מצב גישור

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

מצב חיצוני

אפשר להשתמש ב-Redux, ב-MobX או בכל ספרייה אחרת לניהול מדינה לצד Lit.

רכיבי ה-Lite נוצרים ברמת הדפדפן. כך שכל ספרייה שקיימת גם ברמת הדפדפן זמינה ל-Lit. ספריות מדהימות רבות נבנו כדי להשתמש במערכות ניהול המצב הקיימות ב-Lit.

לפניכם סדרה של Vaadin שמסבירה איך להשתמש ב-Redux ברכיב Lit.

צפו ב-lit-mobx של Adobe כדי לראות איך אתר בקנה מידה גדול יכול למנף את MobX ב-Lit.

בנוסף, עיינו במאמר על olo Elements כדי לראות איך המפתחים כוללים את GraphQL ברכיבי האינטרנט שלהם.

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

עיצוב

DOM של הצללה

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

Shadow DOM כולל גם מושגים וסלקטורים חדשים במפרט ה-CSS:

:host, :host(:hover), :host([hover]) {
  /* Styles the element in which the shadow root is attached to */
}

slot[name="title"]::slotted(*), slot::slotted(:hover), slot::slotted([hover]) {
  /*
   * Styles the elements projected into a slot element. NOTE: the spec only allows
   * styling the direcly slotted elements. Children of those elements are not stylable.
   */
}

סגנונות שיתוף

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

// typography.ts
export const body1 = css`
 
.body1 {
   
...
 
}
`;

// my-el.ts
import {body1} from './typography.ts';

@customElement('my-el')
class MyEl Extends {
 
static get styles = [
   
body1,
   
css`/* local styles come after so they will override bod1 */`
 
]

 
render() {
   
return html`<div class="body1">...</div>`
 
}
}

בחירת עיצוב

שורשים מוצללים מתקשים במידה מסוימת בעיצוב הקונבנציונלי, שבדרך כלל הוא גישות לתגי סגנון מלמעלה למטה. הדרך המקובלת לטפל ברכיבי אינטרנט באמצעות רכיבי אינטרנט שמשתמשים ב-shadow DOM היא לחשוף API של סגנון דרך מאפייני CSS מותאמים אישית. לדוגמה, זוהי דוגמת עיצוב שבה Material Design משתמש:

.mdc-textfield-outline {
  border-color: var(--mdc-theme-primary, /* default value */ #...);
}
.mdc-textfield--input {
  caret-color: var(--mdc-theme-primary, #...);
}

לאחר מכן המשתמש ישנה את עיצוב האתר על ידי החלת ערכי מאפיינים מותאמים אישית:

html {
  --mdc-theme-primary: #F00;
}
html[dark] {
  --mdc-theme-primary: #F88;
}

אם עיצוב מלמעלה למטה הוא חובה ואתם לא יכולים לחשוף סגנונות, תמיד אפשר להשבית את Shadow DOM על ידי החלפה של createRenderRoot כדי להחזיר את הערך this, וכך תגרום לעיבוד של הרכיבים' את התבנית לרכיב המותאם אישית עצמו ולא לשורש הצללית שמקושר לרכיב המותאם אישית. פעולה זו תאבד: הצפנת סגנון, אנקפסולציה של DOM ומשבצות.

ייצור

IE 11

אם אתם צריכים לתמוך בדפדפנים ישנים יותר כמו IE 11, תצטרכו לטעון כמה polyfills שיוצאים עוד כ-33kb. מידע נוסף זמין כאן

חבילות מותנות

צוות Lit ממליץ למלא שתי חבילות שונות, אחת ל-IE 11 ואחת לדפדפנים מודרניים. יש לזה כמה יתרונות:

  • ההצגה של ES 6 מהירה יותר ותשרת את רוב הלקוחות שלך
  • ES 5 שעבר המרה מגדילה באופן משמעותי את גודל החבילה
  • חבילות מותנות מאפשרות לך ליהנות משני העולמות
    • תמיכה ב-IE 11
    • ללא האטה בדפדפנים מודרניים

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