1. מבוא
מה זה Lit
Lit היא ספרייה פשוטה ליצירת רכיבי אינטרנט מהירים וקלי משקל שפועלים בכל מסגרת, או ללא מסגרת בכלל. עם Lit אפשר ליצור רכיבים, אפליקציות, מערכות עיצוב ועוד שניתנים לשיתוף.
מה תלמדו
איך לתרגם כמה מושגים של React ל-Lit, כמו:
- JSX ותבניות
- רכיבים ואביזרים
- State & Lifecycle
- תוכן מושך
- ילדים
- הפניות
- מצב תיווך
מה תפַתחו
בסוף ה-codelab הזה תוכלו להמיר מושגים של רכיבי React לאנלוגים שלהם ב-Lit.
הדרישות
- הגרסה העדכנית של Chrome, Safari, Firefox או Edge.
- ידע ב-HTML, CSS, JavaScript וכלי הפיתוח ל-Chrome.
- ידע ב-React
- (מתקדם) אם אתם רוצים ליהנות מחוויית הפיתוח הטובה ביותר, כדאי להוריד את VS Code. תצטרכו גם את lit-plugin ל-VS Code ואת NPM.
2. Lit לעומת React
הרבה מהמושגים והיכולות הבסיסיים של Lit דומים לאלה של React, אבל יש גם כמה הבדלים חשובים:
הוא קטן
Lit הוא קטן מאוד: הוא מגיע ל-5kb בערך אחרי מיניפיקציה ודחיסה ב-gzip, לעומת 40kb ומעלה של React + ReactDOM.

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

אין צורך בבנייה
עם תכונות חדשות בדפדפן כמו מודולי ES וליטרלים של תבניות מתויגות, לא נדרשת קומפילציה כדי להפעיל את Lit. המשמעות היא שאפשר להגדיר סביבות פיתוח באמצעות תג סקריפט + דפדפן + שרת, ואז להתחיל להשתמש בהן.
יכול להיות שאפילו לא תצטרכו NPM כדי להתחיל, אם אתם משתמשים במודולים של ES וב-CDN מודרני כמו Skypack או UNPKG.
אבל אם אתם רוצים, עדיין אפשר ליצור קוד Lit ולבצע בו אופטימיזציה. האיחוד האחרון של מפתחים סביב מודולי ES מקוריים היה טוב ל-Lit – Lit הוא פשוט JavaScript רגיל ואין צורך בממשקי CLI ספציפיים למסגרת או בטיפול בבנייה.
לא תלוי ב-framework
הרכיבים של Lit מבוססים על קבוצה של תקני אינטרנט שנקראים רכיבי אינטרנט. המשמעות היא שאם תיצרו רכיב ב-Lit, הוא יפעל במסגרות קיימות ועתידיות. אם הוא תומך ברכיבי HTML, הוא תומך ברכיבי אינטרנט.
הבעיות היחידות בתאימות בין מסגרות הן כשהמסגרות תומכות ב-DOM באופן מגביל. React היא אחת מהמסגרות האלה, אבל היא מאפשרת פתרונות עקיפה באמצעות Refs, ו-Refs ב-React לא מספק חוויית פיתוח טובה.
צוות Lit עובד על פרויקט ניסיוני בשם @lit-labs/react, שינתח אוטומטית את רכיבי Lit וייצור רכיב wrapper של React, כך שלא תצטרכו להשתמש בהפניות.
בנוסף, התוסף Custom Elements Everywhere יראה לכם אילו מסגרות וספריות פועלות בצורה טובה עם רכיבים בהתאמה אישית.
תמיכה מלאה ב-TypeScript
אפשר לכתוב את כל קוד Lit ב-JavaScript, אבל קוד Lit נכתב ב-TypeScript וצוות Lit ממליץ למפתחים להשתמש גם ב-TypeScript.
צוות Lit עובד עם קהילת Lit כדי לתחזק פרויקטים שמביאים בדיקת סוגים של TypeScript ו-IntelliSense לתבניות Lit בזמן הפיתוח ובמשך זמן של תהליך build באמצעות lit-analyzer ו-lit-plugin.


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

הוא מבוסס על עיבוד בצד השרת (SSR)
הספרייה Lit 2 נבנתה עם תמיכה ב-SSR. בזמן כתיבת ה-codelab הזה, צוות Lit עדיין לא פרסם את כלי ה-SSR בצורה יציבה, אבל הוא כבר פרסם רכיבים שעברו רינדור בצד השרת במוצרי Google, ובדק את ה-SSR באפליקציות React. צוות Lit צפוי לפרסם את הכלים האלה ב-GitHub בקרוב.
בינתיים, אפשר לעקוב אחרי ההתקדמות של צוות Lit כאן.
ההסכמה נמוכה
לא נדרשת התחייבות משמעותית כדי להשתמש ב-Lit. אפשר ליצור רכיבים ב-Lit ולהוסיף אותם לפרויקט קיים. אם אתם לא אוהבים אותם, אתם לא צריכים להמיר את כל האפליקציה בבת אחת כי רכיבי אינטרנט פועלים במסגרות אחרות!
יצרתם אפליקציה שלמה ב-Lit ואתם רוצים לעבור לטכנולוגיה אחרת? במקרה כזה, אפשר להציב את אפליקציית Lit הנוכחית בתוך המסגרת החדשה ולהעביר את מה שרוצים לרכיבים של המסגרת החדשה.
בנוסף, מסגרות מודרניות רבות תומכות בפלט ברכיבי אינטרנט, כך שבדרך כלל הן יכולות להתאים בתוך רכיב Lit.
3. הגדרה ושימוש בארגז החול
יש שתי דרכים לעבוד עם ה-Codelab הזה:
- אפשר לעשות את זה באינטרנט, בדפדפן
- (מתקדם) אפשר לעשות את זה במחשב המקומי באמצעות VS Code
גישה לקוד
במהלך ה-codelab יוצגו קישורים ל-Lit playground, כמו זה:
הסביבה הניסיונית היא ארגז חול לקוד שפועל באופן מלא בדפדפן. הוא יכול לקמפל ולהריץ קובצי TypeScript ו-JavaScript, והוא יכול גם לפתור באופן אוטומטי ייבוא של מודולים של node. לדוגמה:
// before
import './my-file.js';
import 'lit';
// after
import './my-file.js';
import 'https://cdn.skypack.dev/lit';
אתם יכולים לבצע את כל ההדרכה ב-Lit playground, ולהשתמש בנקודות הביקורת האלה כנקודות התחלה. אם אתם משתמשים ב-VS Code, אתם יכולים להשתמש בנקודות הבדיקה האלה כדי להוריד את קוד ההתחלה של כל שלב, וגם כדי לבדוק את העבודה שלכם.
הסבר על ממשק המשתמש של סביבת המשחקים המוארת

צילום מסך של ממשק המשתמש של Lit playground שבו מודגשים הקטעים שבהם תשתמשו ב-codelab הזה.
- כלי לבחירת קבצים. שימו לב ללחצן הפלוס...
- עורך קבצים.
- תצוגה מקדימה של הקוד.
- לחצן הרענון.
- לחצן ההורדה.
הגדרה של VS Code (מתקדם)
אלה היתרונות של השימוש בהגדרה הזו של VS Code:
- בדיקת סוג התבנית
- השלמה אוטומטית ו-IntelliSense של תבניות
אם כבר התקנתם את NPM ואת VS Code (עם הפלאגין lit-plugin) ואתם יודעים איך להשתמש בסביבה הזו, אתם יכולים פשוט להוריד את הפרויקטים האלה ולהתחיל לעבוד איתם. כדי לעשות את זה:
- לוחצים על לחצן ההורדה
- חילוץ התוכן של קובץ ה-tar לספרייה
- (אם משתמשים ב-TS) מגדירים quick tsconfig שמפיק מודולים של es ו-es2015+
- מתקינים שרת פיתוח שיכול לפתור מפרטים של מודולים חשופים (צוות Lit ממליץ על @web/dev-server)
- דוגמה
package.json
- דוגמה
- מריצים את שרת הפיתוח ופותחים את הדפדפן (אם משתמשים ב- @web/dev-server אפשר להשתמש ב-
npx web-dev-server --node-resolve --watch --open)- אם משתמשים בדוגמה
package.jsonמשתמשים ב-npm run dev
- אם משתמשים בדוגמה
4. JSX ותבניות
בקטע הזה נלמד את היסודות של תבניות ב-Lit.
תבניות JSX ו-Lit
JSX הוא תוסף לתחביר של JavaScript שמאפשר למשתמשי React לכתוב בקלות תבניות בקוד JavaScript שלהם. תבניות Lit משרתות מטרה דומה: הן מאפשרות להציג את ממשק המשתמש של רכיב כפונקציה של המצב שלו.
תחביר בסיסי
ב-React, אפשר להציג את המחרוזת 'hello world' ב-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
);
בדוגמה שלמעלה, יש שני רכיבים ומשתנה 'שם' שכלול בהם. ב-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 לא צריך React Fragment כדי לקבץ כמה רכיבים בתבניות.
ב-Lit, התבניות עטופות ב-html tagged template LITeral, שזה המקום שבו Lit מקבל את השם שלו!
ערכי התבנית
תבניות Lit יכולות לקבל תבניות Lit אחרות, שנקראות TemplateResult. לדוגמה, עוטפים את name בתגי הטיה (<i>) ועוטפים אותו בתבנית מילולית מתויגת הערה: חשוב להשתמש בתו גרש הפוך (`) ולא בתו גרש בודד (').
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
);
Lit TemplateResults יכולים לקבל מערכים, מחרוזות, TemplateResults אחרים וגם הנחיות.
לדוגמה, נסו להמיר את קוד React הבא ל-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
);
בדוגמה שלמעלה, מוגדר קלט שמבצע את הפעולות הבאות:
- הגדרת המשתנה disabled (מושבת) למשתנה מוגדר (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, הפעולה הזו לא תגדיר את רכיב הקלט כקריאה בלבד, כי היא פועלת לפי ההטמעה וההתנהגות המקוריות של הקלט.
תחביר של קישור מאפיינים מילוליים
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) - מתאים לערכים בסיסיים, לסלקטורים של כללי סגנון ול-querySelectors
העברת רכיבי handler
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
);
בדוגמה שלמעלה, מוגדר קלט שמבצע את הפעולות הבאות:
- רישום המילה click (לחיצה) ביומן כשהקלט נלחץ
- רישום ביומן של ערך הקלט כשמשתמש מקליד תו
ב-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, יש listener שנוסף לאירוע click עם @click.
בשלב הבא, במקום להשתמש ב-onChange, יש קשר לאירוע input המקורי של <input>, כי אירוע change המקורי מופעל רק ב-blur (React מבצע הפשטה של האירועים האלה).
תחביר של גורם מטפל באירועים ב-Lit
html`<my-element @event-name=${() => {...}}></my-element>`;
- הקידומת
@היא תחביר הקישור של פונקציית event listener - שווה ערך ל-
inputRef.addEventListener('event-name', ...) - משתמש בשמות אירועים מקוריים של DOM
5. רכיבים ואביזרים
בקטע הזה נסביר על פונקציות ורכיבים של כיתות ב-Lit. בקטעים הבאים נסביר על מצב ועל Hooks.
רכיבי כיתה ו-LitElement
האלמנט LitElement הוא המקבילה של Lit לרכיב מחלקה ב-React, והמושג 'מאפיינים תגובתיים' ב-Lit הוא שילוב של props ו-state ב-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}
}
}
- הפונקציה הזו זהה ל-
@propertyTS decorator, אבל היא פועלת באופן מקורי ב-JavaScript
render() {
return html`<h1>Hello, ${this.name}</h1>`
}
- הפונקציה הזו מופעלת בכל פעם שמשנים מאפיין ריאקטיבי
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
...
}
- התג הזה משייך שם של תג HTML Element להגדרת מחלקה
- בגלל התקן של רכיבים מותאמים אישית, שם התג חייב לכלול מקף (-).
-
thisב-LitElement מתייחס למופע של הרכיב המותאם אישית (<welcome-banner>במקרה הזה)
customElements.define('welcome-banner', WelcomeBanner);
- זוהי הגרסה המקבילה ב-JavaScript של ה-decorator
@customElementב-TS
<head>
<script type="module" src="./index.js"></script>
</head>
- ייבוא ההגדרה של הרכיב המותאם אישית
<body>
<welcome-banner name="Elliott"></welcome-banner>
</body>
- הוספת האלמנט המותאם אישית לדף
- המאפיין
nameמוגדר לערך'Elliott'
רכיבים של פונקציות
ב-Lit אין פרשנות של רכיב פונקציה אחד לאחד, כי הוא לא משתמש ב-JSX או במעבד מקדים. עם זאת, די פשוט ליצור פונקציה שמקבלת מאפיינים ומציגה DOM על סמך המאפיינים האלה. לדוגמה:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Elliott"/>
ReactDOM.render(
element,
mountNode
);
ב-Lit זה יהיה:
import {html, render} from 'lit';
function Welcome(props) {
return html`<h1>Hello, ${props.name}</h1>`;
}
render(
Welcome({name: 'Elliott'}),
document.body.querySelector('#root')
);
6. State & Lifecycle
בקטע הזה נסביר על הסטטוס ומחזור החיים של Lit.
מדינה (State)
המושג 'מאפיינים ריאקטיביים' ב-Lit הוא שילוב של מצב ומאפיינים ב-React. מאפיינים ריאקטיביים, כשמשנים אותם, יכולים להפעיל את מחזור החיים של הרכיב. יש שני סוגים של נכסים ריאקטיביים:
מאפיינים ריאקטיביים ציבוריים
// 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 - דומה ל-props ול-state של 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';
}
}
- הערך המקביל ב-Lit הוא גם
constructor - אין צורך להעביר שום דבר לקריאת השיטה של מחלקת האב
- הפעולה בוצעה על ידי (לא כולל את כל האפשרויות):
document.createElementdocument.innerHTMLnew ComponentClass()- אם שם תג שלא שודרג מופיע בדף וההגדרה נטענת ונרשמת באמצעות
@customElementאוcustomElements.define
- פונקציה דומה ל-
constructorב-React
render
// React
render() {
return <div>Hello World</div>
}
// Lit
render() {
return html`<div>Hello World</div>`;
}
- הערך המקביל ב-Lit הוא גם
render - יכולה להחזיר כל תוצאה שניתן לעבד, למשל
TemplateResultאוstringוכו'. - בדומה ל-React,
render()צריכה להיות פונקציה טהורה - הרכיב יוצג בכל צומת שמוחזר על ידי
createRenderRoot()(ShadowRootכברירת מחדל)
componentDidMount
componentDidMount דומה לשילוב של שני רכיבי ה-callback של מחזור החיים של Lit: firstUpdated ו-connectedCallback.
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 שמעובד על ידי הרכיב
- בניגוד ל-React, שינויים ב-
componentDidMountשל מאפיינים ריאקטיביים ב-firstUpdatedיגרמו לרינדור מחדש, למרות שהדפדפן בדרך כלל יקבץ את השינויים לאותו פריים. אם השינויים האלה לא דורשים גישה ל-DOM של רכיב הבסיס, בדרך כלל הם צריכים להיכלל ב-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);
}
}
- התקן המקביל של Lit הוא
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);
}
- הפונקציה Lit equivalent דומה ל-
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>
`;
}
עכשיו מטמיעים את ה-method tick.
tick() {
this.date = new Date();
}
השלב הבא הוא הטמעה של componentDidMount. שוב, המקבילה של Lit היא תערובת של firstUpdated ו-connectedCallback. במקרה של הרכיב הזה, קריאה ל-tick עם setInterval לא דורשת גישה ל-DOM בתוך רכיב הבסיס. בנוסף, פרק הזמן יתבטל כשהרכיב יוסר מעץ המסמך, כך שאם הוא יצורף מחדש, פרק הזמן יצטרך להתחיל מחדש. לכן, 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. תוכן מושך
בקטע הזה נסביר איך לתרגם מושגי React Hook ל-Lit.
המושגים של React Hooks
ה-Hooks ב-React מאפשרים לרכיבי פונקציה להתחבר למצב. יש לכך כמה יתרונות.
- הם מפשטים את השימוש החוזר בלוגיקה עם שמירת מצב
- עזרה בפיצול רכיב לפונקציות קטנות יותר
בנוסף, ההתמקדות ברכיבים מבוססי-פונקציות פתרה בעיות מסוימות בתחביר מבוסס-המחלקות של React, כמו:
- צריך להעביר את
propsמconstructorאלsuper - אתחול לא מסודר של מאפיינים ב-
constructor- זו הייתה סיבה שצוינה על ידי צוות React בזמנו, אבל היא נפתרה ב-ES2019
- בעיות שנגרמות כתוצאה מכך ש-
thisכבר לא מתייחס לרכיב
מושגים של React Hooks ב-Lit
כמו שצוין בקטע Components & 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ברוב המקרים מתייחס להפניה של הרכיב המותאם אישית - אפשר עכשיו ליצור מופעים של מאפייני כיתה כחברים בכיתה. הפעולה הזו מנקה הטמעות שמבוססות על בנאי
בקרים תגובתיים
המושגים העיקריים שמאחורי Hooks קיימים ב-Lit בתור reactive controllers. דפוסי בקרה תגובתיים מאפשרים שיתוף של לוגיקה עם מצב, פיצול רכיבים לחלקים קטנים יותר ומודולריים יותר, וגם התחברות למחזור החיים של עדכון רכיב.
בקר תגובתי הוא ממשק אובייקט שיכול להתחבר למחזור החיים של עדכון מארח בקר כמו 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 & Lifecycle:
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, אבל צוות Lit מעדיף להשתמש בתבנית של מחלקה עם constructor שיכולה לקבל ממשק ReactiveControllerHost וגם מאפיינים אחרים שנדרשים לאתחול הבקר, ברוב המקרים הבסיסיים.
עכשיו צריך לתרגם את הקריאות החוזרות (callbacks) של מחזור החיים של React לקריאות חוזרות של בקר. בקצרה:
componentDidMount- אל
connectedCallbackשל LitElement - לשלט
hostConnected
- אל
ComponentWillUnmount- אל
disconnectedCallbackשל LitElement - לשלט
hostDisconnected
- אל
מידע נוסף על תרגום מחזור החיים של React למחזור החיים של Lit זמין בקטע State & Lifecycle.
לאחר מכן מטמיעים את הקריאה החוזרת hostConnected ואת השיטות tick, ומנקים את המרווח ב-hostDisconnected כמו בדוגמה שבקטע State & Lifecycle.
// 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 מופיעה בקטע נושאים מתקדמים – Hooks.
8. ילדים
בקטע הזה נסביר איך משתמשים ב-slots כדי לנהל ילדים ב-Lit.
ערוצים וילדים
משבצות מאפשרות קומפוזיציה על ידי קינון רכיבים.
ב-React, רכיבי צאצא עוברים בירושה דרך מאפייני props. משבצת ברירת המחדל היא props.children והפונקציה render מגדירה את המיקום של משבצת ברירת המחדל. לדוגמה:
const MyArticle = (props) => {
return <article>{props.children}</article>;
};
חשוב לזכור ש-props.children הם רכיבי React ולא רכיבי HTML.
ב-Lit, הילדים מורכבים בפונקציית הרינדור עם רכיבי slot. שימו לב: ילדים לא עוברים בירושה באותו אופן כמו React. ב-Lit, רכיבי צאצא הם רכיבי HTML שמצורפים לרכיבי placeholder. הקובץ המצורף הזה נקרא פרויקט.
@customElement("my-article")
export class MyArticle extends LitElement {
render() {
return html`
<article>
<slot></slot>
</article>
`;
}
}
כמה חריצים
ב-React, הוספה של כמה רכיבי 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>
`;
}
}
הקצאת ילדים למקומות
ב-React, רכיבי צאצא מוקצים למקומות שמורים באמצעות המאפיינים של רכיב. בדוגמה שלמטה, רכיבי React מועברים למאפיינים 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 References
רכיב React עובר קומפילציה ל-JavaScript (transpilation) לסדרה של קריאות לפונקציות שיוצרות DOM וירטואלי כשהן מופעלות. ה-DOM הווירטואלי הזה מפוענח על ידי ReactDOM ומעבד רכיבי HTML.
ב-React, Refs הם מקום בזיכרון שמכיל 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>
);
};
בדוגמה שלמעלה, רכיב React יבצע את הפעולות הבאות:
- איך מעבדים קלט טקסט ריק וכפתור עם טקסט
- העברת המיקוד לקלט כשלוחצים על הלחצן
אחרי הרינדור הראשוני, React יגדיר את inputRef.current ל-HTMLInputElement שנוצר באמצעות המאפיין ref.
הדלקת 'הפניות' באמצעות @query
Lit פועל קרוב לדפדפן ויוצר הפשטה דקה מאוד מעל תכונות הדפדפן המקוריות.
המקבילה ב-React ל-refs ב-Lit היא HTMLElement שמוחזר על ידי ה-decorators @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באמצעות ה-decorator@query(יצירת getter עבורHTMLInputElement). - מגדיר ומצרף קריאה חוזרת (callback) לאירוע מסוג קליק בשם
onButtonClick. - הקלט מתמקד בקליקים על לחצנים
ב-JavaScript, קישוטי @query ו-@queryAll מבצעים querySelector ו-querySelectorAll בהתאמה. זהו קוד ה-JavaScript ששווה ערך ל-@query('input') inputEl!: HTMLInputElement;
get inputEl() {
return this.renderRoot.querySelector('input');
}
אחרי שרכיב Lit מעביר את התבנית של שיטת render אל הבסיס של my-element, הדקורטור @query מאפשר ל-inputEl להחזיר את רכיב input הראשון שנמצא בבסיס העיבוד. הפונקציה תחזיר null אם היא לא תוכל למצוא את הרכיב שצוין.@query
אם יש כמה רכיבי input בשורש העיבוד, הפונקציה @queryAll תחזיר רשימה של צמתים.
10. מצב תיווך
בקטע הזה תלמדו איך לנהל את המצב בין רכיבים ב-Lit.
רכיבים לשימוש חוזר
React מחקה צינורות עיבוד נתונים פונקציונליים עם זרימת נתונים מלמעלה למטה. הורים מספקים מצב לילדים באמצעות מאפיינים. ילדים מתקשרים עם ההורים באמצעות קריאות חוזרות שנמצאות במאפיינים.
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כארגומנט בלחיצה
אפשר להעביר קריאות חוזרות ב-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בלחיצה
אירועים בדפדפן עולים מרכיבי צאצא לרכיבי הורה. אירועים מאפשרים לילדים לשדר אירועי אינטראקציה ושינויים במצב. ב-React, העברת הסטטוס מתבצעת בכיוון ההפוך, ולכן לא נפוץ לראות רכיבי React ששולחים אירועים ומאזינים להם באותו אופן כמו רכיבי Lit.
רכיבים ששומרים מצב
ב-React, נהוג להשתמש ב-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>Σ: {counterSum}</h3>
<CounterButton
step={-1}
addToCounter={addToCounter} />
<CounterButton
step={1}
addToCounter={addToCounter} />
</div>
);
};
בדוגמה שלמעלה:
- יוצרת מצב
count. - יוצרת קריאה חוזרת שמוסיפה מספר ל
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>Σ ${this.count}</h3>
<counter-button step="-1"></counter-button>
<counter-button step="1"></counter-button>
</div>
`;
}
}
בדוגמה שלמעלה:
- יוצרת מאפיין ריאקטיבי בשם
countשיעדכן את הרכיב כשהערך ישתנה - קושר את הקריאה החוזרת (callback) של
addToCounterאל ה-event listener של@update-counter - מתעדכן
countעל ידי הוספת הערך שנמצא ב-detail.stepשל האירועupdate-counter - הגדרת הערך
stepשלcounter-buttonבאמצעות המאפייןstep
מקובל יותר להשתמש במאפיינים ריאקטיביים ב-Lit כדי להעביר שינויים מהורה לצאצא. באופן דומה, מומלץ להשתמש במערכת האירועים של הדפדפן כדי להעביר פרטים מלמטה למעלה.
הגישה הזו תואמת לשיטות המומלצות ולמטרה של Lit: לספק תמיכה ברכיבי אינטרנט בפלטפורמות שונות.
11. עיצוב
בקטע הזה נסביר על סגנון ב-Lit.
עיצוב
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 דומה לתחביר של קישור מאפיינים ב-React style.
CSSResult
הדרך המומלצת להגדיר סגנון לרכיבים היא באמצעות css tagged template literal.
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 מותאמים אישית, כי הם יכולים לחדור להיקף הסגנון של Lit.
תגי סגנון
אפשר גם להשתמש בתגי <style> מוטבעים בתבניות. הדפדפן יבטל את הכפילויות של תגי הסגנון האלה, אבל אם תמקמו אותם בתבניות, הם ינותחו לכל מופע של רכיב, ולא לכל מחלקה, כמו במקרה של תבנית עם התג css. בנוסף, ביטול הכפילויות של CSSResult בדפדפן מהיר הרבה יותר.
תגי קישור
אפשרות נוספת להגדרת סגנונות היא שימוש ב-<link rel="stylesheet"> בתבנית, אבל גם זה לא מומלץ כי זה עלול לגרום להבהוב ראשוני של תוכן לא מעוצב (FOUC).
12. נושאים מתקדמים (אופציונלי)
JSX ותבניות
Lit ו-Virtual DOM
Lit-html לא כולל DOM וירטואלי רגיל שמבצע השוואה בין כל צומת בנפרד. במקום זאת, הוא משתמש בתכונות ביצועים שמוטמעות במפרט של תבנית מילולית עם תג ב-ES2015. תבניות מילוליות עם תג הן מחרוזות של תבניות מילוליות עם פונקציות תג שמצורפות אליהן.
דוגמה ל-template literal:
const str = 'string';
console.log(`This is a template literal ${str}`);
דוגמה ל-template literal עם תג:
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 tagged template literals עם React או Preact, צוות Lit ממליץ על htmהספרייה.
עם זאת, כמו במקרה של אתר Google Codelabs וכמה עורכי קוד באינטרנט, תראו שהדגשת תחביר של תבניות מילוליות עם תגים היא לא נפוצה במיוחד. חלק מהסביבות לפיתוח משולב (IDE) ועורכי הטקסט תומכים בהם כברירת מחדל, כמו Atom והכלי להדגשת בלוקים של קוד ב-GitHub. צוות Lit עובד בשיתוף פעולה הדוק עם הקהילה כדי לתחזק פרויקטים כמו lit-plugin, שהוא תוסף ל-VS Code שמוסיף הדגשת תחביר, בדיקת סוגים ו-IntelliSense לפרויקטים של Lit.
Lit & JSX + React 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);
לאחר מכן, React DOM לוקח את הפלט של React ומתרגם אותו ל-DOM בפועל – מאפיינים, תכונות, listeners לאירועים וכל השאר.
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, לא משתמשת ב-Virtual DOM רגיל, אלא ב-DOM API ישירות. לכן, הגודל של Lit 2 הוא פחות מ-5kb אחרי מיניפיקציה ודחיסה ב-gzip, לעומת 40kb אחרי מיניפיקציה ודחיסה ב-gzip של React (2.8kb) + react-dom (39.4kb).
אירועים
React משתמשת במערכת אירועים סינתטית. כלומר, בספריית react-dom צריך להגדיר כל אירוע שישמש כל רכיב, ולספק מקבילה של event listener בפורמט camelCase לכל סוג של צומת. כתוצאה מכך, ל-JSX אין שיטה להגדרת event listener לאירוע מותאם אישית, ומפתחים צריכים להשתמש ב-ref ואז להחיל listener באופן אימפרטיבי. כך נוצרת חוויית פיתוח לא מספיק טובה כשמשלבים ספריות שלא מיועדות ל-React, ולכן צריך לכתוב רכיב wrapper ספציפי ל-React.
Lit-html ניגש ישירות ל-DOM ומשתמש באירועים מקוריים, כך שהוספת event listeners היא פשוטה כמו @event-name=${eventNameListener}. המשמעות היא שמתבצע פחות ניתוח בזמן ריצה כדי להוסיף פונקציות event listener וגם כדי להפעיל אירועים.
רכיבים ואביזרים
רכיבי React ורכיבים בהתאמה אישית
מתחת לפני השטח, LitElement משתמש ברכיבים מותאמים אישית כדי לארוז את הרכיבים שלו. כשמדובר בחלוקה לרכיבים, יש כמה פשרות שצריך לעשות בין רכיבי React כשמשתמשים ברכיבים בהתאמה אישית (הנושאים 'מצב' ו'מחזור חיים' מוסברים בהמשך בקטע מצב ומחזור חיים).
אלה כמה מהיתרונות של רכיבים מותאמים אישית כמערכת רכיבים:
- הם מובנים בדפדפן ולא דורשים כלים
- מתאים לכל API של דפדפן, מ-
innerHTMLו-document.createElementעדquerySelector - בדרך כלל אפשר להשתמש בהם בכל המסגרות
- אפשר לרשום אותם באופן עצלני באמצעות
customElements.defineולבצע הידרציה של ה-DOM
אלה כמה חסרונות של רכיבים בהתאמה אישית בהשוואה לרכיבי React:
- אי אפשר ליצור רכיב בהתאמה אישית בלי להגדיר מחלקה (לכן אין רכיבים פונקציונליים דמויי JSX)
- חייב להכיל תג סוגר
- הערה: למרות הנוחות למפתחים, ספקי דפדפנים בדרך כלל מצטערים על מפרט התגים שנסגרים מעצמם, ולכן מפרטים חדשים יותר לא כוללים תגים שנסגרים מעצמם.
- הוספת צומת נוסף לעץ ה-DOM, שעלול לגרום לבעיות בפריסה
- חובה להירשם באמצעות JavaScript
צוות Lit בחר להשתמש ברכיבים מותאמים אישית במקום במערכת רכיבים בהתאמה אישית, כי הרכיבים המותאמים אישית מוטמעים בדפדפן. הצוות מאמין שהיתרונות של שימוש ברכיבים מותאמים אישית בכל המסגרות עולים על היתרונות של שכבת הפשטה של רכיבים. למעשה, המאמצים של צוות 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>
State & Lifecycle
קריאות חוזרות אחרות במחזור החיים של React
static getDerivedStateFromProps
אין מקבילה ב-Lit כי גם props וגם state הם מאפייני מחלקה זהים
shouldComponentUpdate
- הערך המקביל ב-Lit הוא
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
מופעל כשמעבירים את הרכיב למסמך חדש, למשל מ-HTMLTemplateElement's documentFragment ל-document הראשי. הקריאה החוזרת הזו היא גם חלק ממפרט הרכיבים המותאמים אישית, וצריך להשתמש בה רק בתרחישי שימוש מתקדמים שבהם הרכיב משנה מסמכים.
מאפיינים ושיטות אחרים של מחזור החיים
השיטות והמאפיינים האלה הם חברים במחלקה שאפשר לקרוא להם, לבטל אותם או להמתין להם כדי לשנות את תהליך מחזור החיים.
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
השיטה הזו היא זו שמפעילה את הקריאות החוזרות של מחזור החיים של העדכון. בדרך כלל לא צריך לעשות את זה, אלא במקרים נדירים שבהם צריך לבצע את העדכון באופן סינכרוני או לתזמן אותו בהתאמה אישית.
hasUpdated
הערך של המאפיין הזה הוא true אם הרכיב עודכן לפחות פעם אחת.
isConnected
המאפיין הזה הוא חלק ממפרט הרכיבים בהתאמה אישית, והערך שלו יהיה true אם הרכיב מצורף כרגע לעץ המסמך הראשי.
תרשים מחזור החיים של עדכוני Lit
מחזור החיים של העדכון כולל 3 חלקים:
- לפני העדכון
- עדכון
- אחרי העדכון
לפני העדכון

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

אחרי העדכון

תוכן מושך
למה כדאי להשתמש בטיזרים
הוספנו ל-React את Hooks כדי לתת מענה לתרחישי שימוש פשוטים ברכיבי פונקציה שנדרש בהם מצב. בהרבה מקרים פשוטים, רכיבי פונקציה עם Hooks נוטים להיות פשוטים וקריאים יותר מאשר רכיבי המחלקה המקבילים. עם זאת, כשמציגים עדכוני מצב אסינכרוניים וגם מעבירים נתונים בין Hooks או אפקטים, דפוס ה-Hooks לא מספיק, ופתרון מבוסס-מחלקות כמו בקרי תגובה נוטה להיות יעיל יותר.
בקשות API, ווים ובקרים
מקובל לכתוב Hook שמבקש נתונים מ-API. לדוגמה, ניקח את רכיב הפונקציה הזה של React שמבצע את הפעולות הבאות:
index.tsx- הצגת טקסט
- הצגת התשובה של
useAPI- מזהה משתמש + שם משתמש
- הודעת השגיאה
- 404 כשהמשתמש מגיע למשתמש 11 (כך זה אמור להיות)
- הפסקת השגיאה אם אחזור הנתונים מה-API מופסק
- טוען הודעה
- מעבד כפתור פעולה
- המשתמש הבא: מאחזר את ה-API של המשתמש הבא
- ביטול: הפעולה הזו מבטלת את האחזור של ה-API ומציגה שגיאה
useApi.tsx- הגדרת וו מותאם אישית
useApi - הפונקציה תבצע אחזור אסינכרוני של אובייקט משתמש מ-API
- הפלט:
- שם משתמש
- אם הטעינה של האחזור מתבצעת
- הודעות שגיאה
- קריאה חוזרת לביטול האחזור
- מבטל אחזור נתונים בתהליך אם הרכיב מוסר
- הגדרת וו מותאם אישית
כאן אפשר לראות את ההטמעה של Lit + Reactive Controller.
מסקנות:
- רכיבי בקרה תגובתיים דומים יותר ל-custom hooks
- העברת נתונים שלא ניתן לעבד בין פונקציות קריאה חוזרת ואפקטים
- React משתמשת ב-
useRefכדי להעביר נתונים ביןuseEffectלביןuseCallback - Lit משתמש במאפיין פרטי של מחלקה
- התגובה בעצם מחקה את ההתנהגות של מאפיין מחלקה פרטית
- React משתמשת ב-
בנוסף, אם אתם מאוד אוהבים את תחביר רכיבי הפונקציה של React עם Hooks, אבל אתם רוצים את אותו סביבה ללא בנייה של Lit, צוות Lit ממליץ מאוד על ספריית Haunted.
ילדים
מיקום ברירת מחדל
אם לא מציינים מאפיין slot לרכיבי HTML, הם מוקצים למשבצת ברירת המחדל ללא שם. בדוגמה שלמטה, הפונקציה 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>
`;
}
}
עדכונים לגבי משבצות
כשמשתנה המבנה של צאצאים של slot, מופעל אירוע slotchange. רכיב Lit יכול לקשור event listener לאירוע 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.
React הוא צינור פונקציונלי שיוצר רכיבי React ולא רכיבי HTMLElements. מכיוון שרכיב Ref מוצהר לפני שרכיב HTMLElement מעובד, מוקצה מקום בזיכרון. לכן, הערך ההתחלתי של Ref הוא null, כי רכיב ה-DOM בפועל עדיין לא נוצר (או עבר רינדור), כלומר useRef(null).
אחרי ש-ReactDOM ממיר רכיב React ל-HTMLElement, הוא מחפש מאפיין בשם ref ב-ReactComponent. אם יש אפשרות, ReactDOM ממקם את ההפניה של HTMLElement אל ref.current.
LitElement משתמש בפונקציית תג התבנית html מ-lit-html כדי ליצור רכיב תבנית מתחת לפני השטח. LitElement מטביע את התוכן של התבנית בshadow DOM של רכיב מותאם אישית אחרי העיבוד. Shadow DOM הוא עץ DOM בהיקף מוגדר, שעטוף ב-shadow root. הדקורטור @query יוצר getter למאפיין, שבעצם מבצע this.shadowRoot.querySelector בשורש של ההיקף.
שאילתה של כמה רכיבים
בדוגמה שלמטה, העיטור @queryAll יחזיר את שתי הפסקאות ב-shadow root בתור 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, אפשר להצהיר על getter כדי לבצע את אותה מטרה:
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, נהוג להשתמש בפונקציות קריאה חוזרת כי המצב מנוהל על ידי React עצמה. ספריית React עושה כמיטב יכולתה כדי לא להסתמך על מצב שמסופק על ידי רכיבים. ה-DOM הוא פשוט תוצאה של תהליך הרינדור.
מצב חיצוני
אפשר להשתמש ב-Redux, ב-MobX או בכל ספרייה אחרת לניהול מצבים לצד Lit.
רכיבי Lit נוצרים בהיקף הדפדפן. לכן, כל ספרייה שקיימת גם בהיקף הדפדפן זמינה ל-Lit. ספריות רבות ומדהימות נבנו כדי להשתמש במערכות קיימות לניהול מצב ב-Lit.
בסדרה הזו של Vaadin מוסבר איך להשתמש ב-Redux ברכיב Lit.
כדאי לעיין ב-lit-mobx של Adobe כדי לראות איך אתר בקנה מידה גדול יכול להשתמש ב-MobX ב-Lit.
כדאי גם לעיין ב-Apollo Elements כדי לראות איך מפתחים משלבים GraphQL ברכיבי האינטרנט שלהם.
Lit פועל עם תכונות מובנות של הדפדפן, ואפשר להשתמש ברכיב Lit ברוב הפתרונות לניהול מצב בהיקף הדפדפן.
עיצוב
Shadow DOM
כדי להצפין סגנונות ו-DOM באופן מקורי בתוך רכיב בהתאמה אישית, Lit משתמש ב-Shadow DOM. התכונה 'שורשי צללים' יוצרת עץ צללים נפרד מעץ המסמך הראשי. המשמעות היא שרוב הסגנונות מוגבלים למסמך הזה. סגנונות מסוימים חודרים, כמו צבע וסגנונות אחרים שקשורים לגופן.
בנוסף, 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>`
}
}
בחירת עיצוב
שורשי צל יוצרים אתגר מסוים לשיטות עיצוב רגילות, שבדרך כלל מבוססות על תגי סגנון מלמעלה למטה. הדרך המקובלת לטפל בעיצוב באמצעות Web Components שמשתמשים ב-Shadow DOM היא לחשוף API של סגנון באמצעות CSS Custom Properties. לדוגמה, זהו דפוס שמשמש ב-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;
}
אם אתם חייבים להשתמש בערכות נושא מלמעלה למטה ולא יכולים לחשוף סגנונות, תמיד אפשר להשבית את DOM על ידי החלפת createRenderRoot ב-this. כך התבנית של הרכיבים תעבור רינדור לרכיב המותאם אישית עצמו ולא ל-shadow root שמצורף לרכיב המותאם אישית. המשמעות היא שתאבדו את התכונות הבאות: הכמסת סגנונות, הכמסת DOM ורכיבי placeholder.
ייצור
IE 11
אם אתם צריכים לתמוך בדפדפנים ישנים יותר כמו IE 11, תצטרכו לטעון כמה polyfills, שגודלם הכולל הוא כ-33kb נוספים. מידע נוסף
חבילות מותנות
צוות Lit ממליץ להציג שני חבילות שונות, אחת לדפדפן IE 11 ואחת לדפדפנים מודרניים. יש לכך כמה יתרונות:
- השימוש ב-ES 6 מהיר יותר וישרת את רוב הלקוחות שלכם
- הידור של ES 5 מגדיל משמעותית את גודל החבילה
- חבילות מותנות מאפשרות לכם ליהנות מכל העולמות
- תמיכה ב-IE 11
- אין האטה בדפדפנים מודרניים
כאן אפשר למצוא מידע נוסף על יצירת חבילה שמוצגת בהתאם לתנאים באתר התיעוד שלנו.