מידע על Codelab זה
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.
מהיר
בנקודות השוואה ציבוריות שמשווה בין מערכת יצירת התבניות של 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
.
כלי הפיתוח מובנים בדפדפן
רכיבי Lit הם רק רכיבי HTML ב-DOM. המשמעות היא שכדי לבדוק את הרכיבים אין צורך להתקין כלים או הפעלות בדפדפן.
פשוט פותחים את כלי הפיתוח, בוחרים רכיב ובודקים את המאפיינים או המצב שלו.
הוא מבוסס על רינדור בצד השרת (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
בצילום המסך של ממשק המשתמש של Lit Play מודגשים קטעים שתשתמשו בהם ב-Codelab הזה.
- בורר הקבצים. שים לב ללחצן הפלוס...
- עורך קבצים
- תצוגה מקדימה של הקוד.
- לחצן טעינה מחדש
- לחצן הורדה.
הגדרה של קוד VS (מתקדם)
אלה היתרונות של השימוש בהגדרה הזו של VS Code:
- בדיקה של סוג התבנית
- תובנות על התבניות השלמה אוטומטית
אם כבר התקנתם את האפליקציות NPM ו-VS Code (עם הפלאגין lit-פלאגין) ואתם יודעים איך להשתמש בסביבה, תוכלו פשוט להוריד ולהתחיל את הפרויקטים האלה על ידי ביצוע הפעולות הבאות:
- לוחצים על לחצן ההורדה
- חילוץ התוכן של קובץ ה-tar לספרייה
- (אם מדובר ב-TS) מגדירים Fast 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 ו-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>Σ: {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>Σ ${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 חלקים:
- עדכון מראש
- עדכון
- אחרי עדכון
עדכון מקדים
אחרי requestUpdate
ממתין עדכון מתוזמן.
עדכון
אחרי העדכון
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) לביטול האחזור
- ביטולי שליפות מתבצעות אם מסירים את המטען
- הגדרת הוק (hook) מותאם אישית של
הנה הטמעה של 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
- ללא האטה בדפדפנים מודרניים
מידע נוסף על בניית חבילה המוצגת באופן מותנה זמין באתר התיעוד שלנו כאן.