1. מבוא
מה זה Lit
Lit היא ספרייה פשוטה ליצירת רכיבי אינטרנט מהירים וקלים שעובדים בכל מסגרת או ללא מסגרת בכלל. בעזרת Lit תוכלו ליצור רכיבים, אפליקציות, מערכות עיצוב ועוד שאפשר לשתף.
מה תלמדו
איך לתרגם כמה מושגים של React ל-Lite, כמו:
- JSX ויצירת תבניות
- רכיבים ואביזרים
- מצב ומחזור חיים
- Hooks
- ילדים
- Refs
- מצב תיווך
מה תפַתחו
בסוף השיעור הזה תהיה אפשרות להמיר מושגים של רכיבי React לאנלוגים של Lit.
מה נדרש
- גרסה עדכנית של Chrome , Safari , Firefox או Edge.
- ידע ב-HTML, ב-CSS, ב-JavaScript וב-Chrome DevTools.
- הידע של 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 (מחלקת הבסיס של רכיבי Lit) מוסיפה תקורה מינימלית ל-lit-html, אבל גורמת לביצועים של React ב-16-30% בהשוואה בין תכונות של רכיבים כמו שימוש בזיכרון, אינטראקציה וזמני הפעלה.
לא נדרש build
בעזרת תכונות דפדפן חדשות כמו מודולים של ES ומילולי template מתויגים, לא צריך לבצע הידור כדי להריץ את Lit. כלומר, אפשר להגדיר סביבות פיתוח באמצעות תג סקריפט + דפדפן + שרת, והכול מוכן לפעולה.
עם מודולים של ES ורשתות CDN מודרניות כמו Skypack או UNPKG, אולי אפילו לא יהיה צורך ב-NPM כדי להתחיל.
עם זאת, אם אתם רוצים, עדיין תוכלו ליצור ולבצע אופטימיזציה של קוד Lit. המיזוג האחרון של מפתחים סביב מודולים מקומיים של ES היה טוב ל-Lit – Lit הוא פשוט JavaScript רגיל ואין צורך בממשקי CLI ספציפיים למסגרת או בטיפול ב-build.
ללא תלות ב-framework
הרכיבים של Lit מבוססים על קבוצה של תקני אינטרנט שנקראים Web Components. המשמעות היא שרכיב שנוצר ב-Lit יפעל במסגרות קיימות ועתידיות. אם היא תומכת ברכיבי HTML, היא תומכת ברכיבי אינטרנט.
הבעיות היחידות שקשורות ליכולת הפעולה ההדדית בין המסגרות מתרחשות כשיש למסגרות תמיכה מוגבלת ב-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 כאן.
סכום ההשקעה נמוך
אין צורך להתחייב לשימוש משמעותי ב-Lit! אפשר ליצור רכיבים ב-Lit ולהוסיף אותם לפרויקט הקיים. אם הם לא מוצאים חן בעיניך, אין צורך להמיר את כל האפליקציה בבת אחת מכיוון שרכיבי אינטרנט פועלים במסגרות אחרות!
האם יצרתם אפליקציה שלמה ב-Lit ואתם רוצים לעבור לאפליקציה אחרת? עכשיו תוכלו למקם את אפליקציית Lit הנוכחית בתוך המסגרת החדשה ולהעביר את מה שאתם רוצים לרכיבי ה-framework.
בנוסף, מסגרות מודרניות רבות תומכות בפלט ברכיבי אינטרנט, כך שבדרך כלל הן יכולות להשתלב ברכיב Lit בעצמן.
3. בתהליך הגדרה וגילוי של Playground
אפשר לעשות זאת בשתי דרכים:
- אפשר לעשות את זה לגמרי באינטרנט, בדפדפן
- (מתקדם) אפשר לעשות זאת במחשב המקומי באמצעות VS Code
גישה לקוד
לאורך הקודלאב יופיעו קישורים למרחב העבודה של Lit, כמו זה:
מגרש המשחקים הוא ארגז חול של קוד שפועל במלואו בדפדפן שלך. הוא יכול להדר ולהריץ קובצי TypeScript ו-JavaScript, ויכול גם לפתור באופן אוטומטי ייבוא למודולים של צמתים. למשל,
// before
import './my-file.js';
import 'lit';
// after
import './my-file.js';
import 'https://cdn.skypack.dev/lit';
אפשר לבצע את כל המדריך ב-Lite Playground ולהשתמש בנקודות הביקורת האלה כנקודות ההתחלה. אם אתם משתמשים ב-VS Code, תוכלו להשתמש בנקודות הבקרה האלה כדי להוריד את קוד ההתחלה של כל שלב, וגם כדי לבדוק את העבודה שלכם.
הכרת הממשק של Lit Playground
בצילום המסך של ממשק המשתמש של Lit Play מודגשים קטעים שתשתמשו בהם ב-Codelab הזה.
- בורר קבצים. שים לב ללחצן הפלוס...
- עורך קבצים.
- תצוגה מקדימה של הקוד.
- לחצן הטעינה מחדש.
- לחצן הורדה.
הגדרת VS Code (מתקדם)
אלה היתרונות של השימוש בהגדרה הזו של VS Code:
- בדיקה של סוג התבנית
- השלמה אוטומטית ותכונות IntelliSense של תבניות
אם כבר התקנתם את האפליקציות NPM ו-VS Code (עם הפלאגין lit-plugin) ואתם יודעים איך להשתמש בסביבה, תוכלו פשוט להוריד ולהתחיל את הפרויקטים האלה על ידי ביצוע הפעולות הבאות:
- לוחצים על לחצן ההורדה
- חילוץ התוכן של קובץ ה-tar לספרייה
- (אם TS) מגדירים 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 היית רוצה ליצור עולם של 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>
) ועוטפים אותו בביטוי מילולי מתויג של תבנית הערה חשוב להשתמש בתו נטוי (`
) ולא בתו מירכאות בודדות ('
).
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
אחרים וגם הוראות.
כדאי לנסות להמיר את קוד ה-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, הפעולה הזו לא תגדיר את רכיב הקלט לקריאה בלבד כי הוא תואם להטמעה ולהתנהגות של הקלט במקור.
תחביר של קישור פרויקט ל-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, הוסף listener לאירוע click
באמצעות @click
.
לאחר מכן, במקום להשתמש ב-onChange
, יש קישור לאירוע input
המקורי של <input>
, כי האירוע change
המקורי מופעל רק ב-blur
(React מספק אבסטרקציה של האירועים האלה).
תחביר של הגורם המטפל באירועים של אותיות רישיות
html`<my-element @event-name=${() => {...}}></my-element>`;
- הקידומת
@
היא תחביר הקישור של מאזין לאירועים - שווה ערך ל-
inputRef.addEventListener('event-name', ...)
- נעשה שימוש בשמות אירועים מקומיים של DOM
5. רכיבים ואביזרים
בקטע הזה תלמדו על הרכיבים והפונקציות של מחלקת Lit. בהמשך המאמר נפרט על המצבים וההוקס.
רכיבי Class ו-LitElement
המקבילה ב-Lit לרכיב של סוג ב-React היא LitElement, והמושג 'מאפיינים תגובתיים' ב-Lit הוא שילוב של ה-props והמצב ב-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 של ה-decorator
@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
);
ב-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. מצב ומחזור חיים
בקטע הזה נסביר על המצב ועל מחזור החיים של Lit.
מדינה
הקונספט של Lit ל-Reactive Properties הוא שילוב של מצב התגובה והאביזרים ב-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.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
דומה לשילוב של שתי הפונקציות החוזרות של מחזור החיים 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
של React, שינויים במאפיינים תגובתיים ב-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
(באנגלית,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 is similar to
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 מספקים דרך לרכיבי פונקציות "להצמיד" למצב. יש לכך כמה יתרונות.
- הם מפשטים את השימוש החוזר בלוגיקה של שמירת מצב
- עזרה לפיצול רכיב לפונקציות קטנות יותר
בנוסף, ההתמקדות ברכיבים מבוססי-פונקציות פתרה בעיות מסוימות בתחביר מבוסס-הכיתות של React, כמו:
- צריך להעביר את
props
מ-constructor
אלsuper
- אתחול לא מסודר של מאפיינים ב-
constructor
- זו הייתה הסיבה שצוות React ציין בזמנו, אבל היא נפתרה ב-ES2019
- בעיות שנגרמו על ידי
this
כבר לא מתייחסות לרכיב
מושגי React hooks ב-Lit
כפי שצוין בקטע רכיבים ו-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
בקרי תגובה
המושגים העיקריים שמאחורי ה-Hooks קיימים ב-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
, מחזור החיים של הבקר יקרא במקביל למחזור החיים של המארח. לדוגמה, חפשו את הדוגמה לשעון בקטע מצב ומחזור חיים:
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
ואת השיטות 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);
כדי להשתמש ב-Controller, צריך ליצור מופע של ה-Controller על ידי העברת הפניה למארח של ה-Controller (שהוא הרכיב <my-element>
), ואז להשתמש ב-Controller בשיטה 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) מאפשרות הרכבה בכך שהן מאפשרות להציב רכיבים בתוך רכיב.
ב-React, צאצאים עוברים בירושה דרך רכיבי פרופרטי. חריץ ברירת המחדל הוא props.children
, והפונקציה render
מגדירה את המיקום של חריץ ברירת המחדל. לדוגמה:
const MyArticle = (props) => {
return <article>{props.children}</article>;
};
חשוב לזכור ש-props.children
הם רכיבי React ולא רכיבי HTML.
ב-Lit, צאצאים מורכבים מפונקציית העיבוד עם רכיבים של משבצות. שימו לב שהילדים לא עוברים בירושה באותו אופן כמו ב-React. ב-Lit, צאצאים הם רכיבי HTMLElement שמצורפים לחריצים. הקובץ המצורף הזה נקרא הקרנה.
@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>
`;
}
}
הקצאת ילדים למשבצות
ב-React, רכיבים צאצאים מוקצים לחריצים באמצעות המאפיינים של הרכיב. בדוגמה הבאה, אלמנטים של React מועברים ל-props 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. Refs
מדי פעם, ייתכן שמפתח יצטרך לגשת ל-API של HTMLElement.
בקטע הזה תלמדו איך לקבל הפניות לרכיבים ב-Lit.
הפניות ל-React
רכיב React עובר תרגום ל-JavaScript (transpilation) לסדרה של קריאות פונקציה שיוצרות DOM וירטואלי כשהן מופעלות. ה-DOM הווירטואלי הזה מפורש על ידי ReactDOM ומעבד את HTMLElements.
ב-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 תגדיר את 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
באמצעות כלי העיצוב@query
(יצירת getter שלHTMLInputElement
). - מצהיר ומצרף התקשרות חזרה לאירוע קליק בשם
onButtonClick
. - התמקדות בקלט בלחיצה על הלחצן
ב-JavaScript, ה-decorators @query
ו-@queryAll
מבצעים את הפונקציות querySelector
ו-querySelectorAll
, בהתאמה. זהו JavaScript המקביל ל-@query('input') inputEl!: HTMLInputElement;
get inputEl() {
return this.renderRoot.querySelector('input');
}
אחרי שרכיב Lit מבצע commit של התבנית של השיטה render
לשורש של my-element
, מעטר @query
יאפשר עכשיו ל-inputEl
להחזיר את רכיב input
הראשון שנמצא בשורש העיבוד. הפונקציה תחזיר את הערך null
אם ה-@query
לא יכול למצוא את הרכיב שצוין.
אם היו כמה רכיבי input
ברמה הבסיסית של הרינדור, @queryAll
היה מחזיר רשימה של צמתים.
10. מצב תיווך
בקטע הזה נסביר איך לתווך בין המצבים בין רכיבים ב-Lit.
רכיבים לשימוש חוזר
React מחקה צינורות עיבוד נתונים של עיבוד פונקציונלי עם זרימת נתונים מלמעלה למטה. הורים מספקים לילדים מצב באמצעות אביזרים. ילדים מתקשרים עם ההורים באמצעות קריאה חוזרת (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
. - יצירת פונקציית קריאה חוזרת שמוסיפה מספר למצב
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 מותאמים אישית, כי הם יכולים לחדור להיקף הסגנון של Lit.
תגי סגנון
אפשר גם פשוט להטביע תגי <style>
בתבניות. הדפדפן יבטל את הכפילויות של תגי הסגנון האלה, אך על ידי הצבתם בתבניות שלך, הם ינותחו לכל מופע של רכיב, ולא לכל כיתה כמו במקרה של התבנית המתויגת css
. בנוסף, ביטול הכפילויות של CSSResult
בדפדפן מהיר הרבה יותר.
תגי קישור
אפשר גם להשתמש ב-<link rel="stylesheet">
בתבנית כדי להגדיר סגנונות, אבל גם זה לא מומלץ כי זה עלול לגרום להצגה קצרה של תוכן ללא סגנון (FOUC).
12. נושאים מתקדמים (אופציונלי)
JSX ויצירת תבניות
DOM מופעל ו-DOM וירטואלי
Lit-html לא כולל 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 לקישור, לניתוח ולאחסון ב-cache של התבניות, בלי הרבה זמן עיבוד נוסף לצורך השוואה בין גרסאות (diff) בסביבת זמן הריצה.
ההתנהגות המובנית הזו בדפדפן של ייצוגי מילים של תבניות מתויגות נותנת ל-Lit יתרון משמעותי בביצועים. רוב ה-DOM הווירטואליים הרגילים מבצעים את רוב העבודה ב-JavaScript. עם זאת, רוב פעולות ההשוואה של ביטויים מילוליים של תבניות מתויגות מתבצעות ב-C++ של הדפדפן.
אם אתם רוצים להתחיל להשתמש בליטרלים של תבניות עם תגים של HTML באמצעות React או Preact, צוות Lit ממליץ על הספרייה htm
.
עם זאת, כמו באתר Google Codelabs ובכמה עורכי קוד דיגיטליים, תבחינו שהדגשת תחביר מילולי של תבנית מתויגת אינה נפוצה מאוד. סביבות פיתוח משולבות (IDE) מסוימות ועריכות טקסט תומכים בהן כברירת מחדל, כמו Atom ו-GitHub's codeblock highlighter. צוות Lit עובד גם בשיתוף פעולה הדוק עם הקהילה כדי לתחזק פרויקטים כמו lit-plugin
, שהוא פלאגין ל-VS Code שמוסיף סימון תחביר, בדיקת סוגים ו-IntelliSense לפרויקטים של Lit.
Lit ו-JSX + DOM React
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 בפועל – מאפיינים, מאפיינים, מאזינים לאירועים וכו'.
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 של תגובת ריבה (39.4kb).
אירועים
ב-React נעשה שימוש במערכת אירועים סינתטית. כלומר, צריך להגדיר כל אירוע שישמש בכל רכיב, ולספק ערך השווה ל-event listener לכל סוג של צומת. כתוצאה מכך, ל-JSX אין שיטה להגדרת מאזין אירועים לאירוע מותאם אישית, והמפתחים צריכים להשתמש ב-ref
ואז להחיל מאזין באופן אימפרטיבי. כשמשלבים ספריות שלא נלקחות בחשבון בתגובה ל-React, נוצר חוויית מפתח משנית. כתוצאה מכך, צריך לכתוב wrapper שספציפי ל-React.
Lit-html ניגש ישירות ל-DOM ומשתמש באירועים נייטיב, כך שאפשר להוסיף פונקציות event listener רק ב-@event-name=${eventNameListener}
. פירוש הדבר הוא שמתבצע פחות ניתוח של זמן ריצה להוספת פונקציות event listener וגם להפעלה של אירועי הפעלה.
רכיבים ואביזרים
רכיבים ורכיבים מותאמים אישית של React
מתחת לפני השטח, LitElement משתמש ברכיבים מותאמים אישית כדי לארוז את הרכיבים שלו. רכיבים מותאמים אישית גורמים לפשרות מסוימות בין רכיבי React כשמדובר בחלוקה לרכיבים (מידע נוסף על מצב ומחזור חיים זמין בקטע מצב ומחזור חיים).
אלה כמה מהיתרונות של רכיבי Custom Elements כמערכת רכיבים:
- מקורי לדפדפן ולא דורש כלים
- התאמה לכל ממשק API של דפדפן מ-
innerHTML
ו-document.createElement
עדquerySelector
- בדרך כלל אפשר להשתמש בהם במסגרות שונות
- ניתן להירשם באופן מדורג ב-
customElements.define
וב-DOM "hydrate"
יש חסרונות של רכיבים מותאמים אישית בהשוואה לרכיבי תגובה:
- אי אפשר ליצור רכיב בהתאמה אישית בלי להגדיר כיתה (לכן אין רכיבים פונקציונליים כמו 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
- הערך המקביל ב-Lit הוא
shouldUpdate
- בוצעה קריאה ברינדור הראשון, בניגוד ל-React
- פונקציה דומה ל-
shouldComponentUpdate
של React
getSnapshotBeforeUpdate
ב-Lit, הערך של getSnapshotBeforeUpdate
דומה גם ל-update
וגם ל-willUpdate
willUpdate
- התקשרת לפני
update
- בניגוד ל-
getSnapshotBeforeUpdate
, הפונקציהwillUpdate
נקראת לפניrender
- שינויים במאפיינים תגובתיים ב-
willUpdate
לא מפעילים מחדש את מחזור העדכונים - מקום טוב לחשב בו ערכי נכסים שתלויים בנכסים אחרים ושנעשה בהם שימוש בשאר תהליך העדכון
- ה-method הזה נקרא בשרת ב-SSR, לכן לא מומלץ לגשת ל-DOM כאן
update
- הקריאה מתבצעת אחרי
willUpdate
- בניגוד ל-
getSnapshotBeforeUpdate
, הפונקציהupdate
נקראת לפניrender
- שינויים במאפיינים הראקטיביים ב-
update
לא יפעילו מחדש את מחזור העדכון אם הם שונו לפני הקריאה ל-super.update
- מקום טוב לקלוט מידע מה-DOM שמקיף את הרכיב לפני שהפלט המעובד מחויב ל-DOM
- השיטה הזו לא נקראת בשרת ב-SSR
קריאות חוזרות אחרות במחזור החיים של Lit
יש כמה אירועי קריאה חוזרת בזמן חיים שלא הוזכרו בקטע הקודם כי אין להם מקבילים ב-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
ה-method הזה קורא ל-callbacks של מחזור החיים של העדכון. בדרך כלל אין צורך בכך, למעט במקרים נדירים שבהם צריך לבצע את העדכון באופן סינכרוני או לצורך תזמון מותאם אישית.
hasUpdated
הערך של המאפיין הזה הוא true
אם הרכיב עודכן לפחות פעם אחת.
isConnected
חלק ממפרט הרכיבים המותאמים אישית, המאפיין הזה יהיה true
אם הרכיב מצורף כרגע לעץ המסמך הראשי.
המחשה של מחזור החיים של עדכוני Lit
מחזור החיים של העדכון כולל 3 חלקים:
- עדכון מראש
- עדכון
- אחרי עדכון
עדכון מקדים
אחרי requestUpdate
ממתין עדכון מתוזמן.
עדכון
אחרי העדכון
Hooks
למה קטעי הוק (hooks)
נוספו קטעי הוק (hooks) ל-React לצורך תרחישים לדוגמה של רכיבי פונקציה פשוטים שדרשו מצב נדרש. במקרים פשוטים רבים, רכיבי פונקציה עם קטעי הוק נוטים להיות פשוטים וקריאים יותר מרכיבי המחלקה המקבילים. עם זאת, כשרוצים להוסיף עדכוני מצב אסינכרונים וכן להעביר נתונים בין וו hooks או אפקטים, דפוס ה-hooks בדרך כלל לא מספיק, ופתרון מבוסס-כיתה כמו בקרי תגובה נוטה להניב תוצאות טובות יותר.
מתגים ובקרים לבקשות API
מקובל לכתוב הוק שמבקש נתונים מ-API. לדוגמה, רכיב הפונקציה הזה ב-React שמבצע את הפעולות הבאות:
index.tsx
- עיבוד הטקסט
- הצגת התשובה של
useAPI
- מזהה משתמש + שם משתמש
- הודעת שגיאה
- 404 כשמגיעים למשתמש 11 (כחלק מהעיצוב)
- שגיאת ביטול אם אחזור ה-API בוטל
- טוען הודעה
- עיבוד של לחצן פעולה
- המשתמש הבא: שמושך את ה-API של המשתמש הבא
- ביטול: ביטול האחזור של ה-API והצגת שגיאה
useApi.tsx
- הגדרת הוק
useApi
בהתאמה אישית - אחזור אסינכרוני של אובייקט משתמש מ-API
- אזכורים:
- שם משתמש
- האם האחזור נטען
- הודעות שגיאה
- פונקציית קריאה חוזרת (callback) כדי לבטל את האחזור
- ביטול אחזור של נתונים שנמצאים בתהליך אחזור אם ההתקן פורק
- הגדרת הוק
הטמעה של Lit + Reactive Controller
המסקנות:
- בקרי תגובה מיידית דומים בעיקר ל-hooks מותאמים אישית.
- העברת נתונים שלא ניתן להציג אותם בין פונקציות קריאה חוזרת (callbacks) ואפקטים
- React משתמשת ב-
useRef
כדי להעביר נתונים ביןuseEffect
לביןuseCallback
- Lit משתמש במאפיין כיתה פרטי
- ב-React, בעצם מחקים את ההתנהגות של מאפיין כיתה פרטי
- React משתמשת ב-
בנוסף, אם אתם מאוד אוהבים את התחביר של רכיב פונקציית 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
. בדוגמה הבאה, ה-assignedNodes של המיקום הראשון שנמצא ב-shadowRoot
יירשמו ביומן במסוף ב-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>
`;
}
}
Refs
יצירת קובצי עזר
גם Lit וגם React חושפים הפניה ל-HTMLElement אחרי קריאה לפונקציות render
שלהן. עם זאת, כדאי לבדוק איך React ו-Lit יוצרים את ה-DOM שמוחזר מאוחר יותר באמצעות מעטר @query
של Lit או הפניה של React.
React הוא צינור עיבוד נתונים פונקציונלי שיוצר רכיבי React ולא רכיבי HTMLElement. מכיוון שמצהירים על Ref לפני ש-HTMLElement מנוהל, מוקצה מקום בזיכרון. לכן הערך null
הוא הערך הראשוני של ההפניה, כי רכיב ה-DOM בפועל עדיין לא נוצר (או עבר רינדור), כלומר useRef(null)
.
אחרי ש-ReactDOM ממיר רכיב React ל-HTMLElement, הוא מחפש מאפיין שנקרא ref
ב-ReactComponent. אם ההפניה קיימת, ReactDOM ממוקם את ההפניה של HTMLElement אל ref.current
.
LitElement משתמש בפונקציית התג של התבנית html
מ-lit-html כדי ליצור Template Element מתחת לפני השטח. LitElement חותם את תוכן התבנית ב-DOM DOM של רכיב מותאם אישית לאחר העיבוד. ה-DOM של הצללית הוא עץ DOM עם היקף שמוקף על ידי שורש הצל. לאחר מכן, ה-decorator @query
יוצר פונקציית getter לנכס, שמבצעת למעשה this.shadowRoot.querySelector
ברמה הבסיסית (root) של ההיקף.
שאילתה של מספר רכיבים
בדוגמה הבאה, ה-decorator @queryAll
יחזיר את שני הקטעים ברמה הבסיסית (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, אפשר להצהיר על רכיב קריאה שיבצע את אותה מטרה:
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 עצמו. React עושה כמיטב יכולתה כדי לא להסתמך על מצב שמסופק על ידי רכיבים. ה-DOM הוא פשוט השפעה של תהליך הרינדור.
מצב חיצוני
אפשר להשתמש ב-Redux, ב-MobX או בכל ספרייה אחרת לניהול מדינה לצד Lit.
רכיבי ה-Lite נוצרים ברמת הדפדפן. כל ספרייה שקיימת גם ברמת הדפדפן זמינה ל-Lit. נוצרו הרבה ספריות מדהימות שמנצלות מערכות קיימות לניהול מצב ב-Lit.
סדרה של Vaadin שמסבירה איך להשתמש ב-Redux ברכיב Lit.
כדאי לעיין ב-lit-mobx מ-Adobe כדי לראות איך אפשר להשתמש ב-MobX ב-Lit באתרים גדולים.
כדאי גם לעיין ב-Apollo Elements כדי לראות איך מפתחים כוללים את GraphQL ברכיבי האינטרנט שלהם.
Lit פועל עם תכונות הדפדפן המקוריות, ואפשר להשתמש ברכיב Lit ברוב הפתרונות לניהול מצב ברמת הדפדפן.
עיצוב
Shadow 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>`
}
}
בחירת עיצוב
שורשים מוצללים מתקשים במידה מסוימת בעיצוב הקונבנציונלי, שבדרך כלל הוא גישות לתגי סגנון מלמעלה למטה. הדרך המקובלת לטיפול בנושא עיצוב באמצעות רכיבי Web שמשתמשים ב-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;
}
אם עיצוב מלמעלה למטה הוא חובה ואין לכם אפשרות לחשוף סגנונות, תמיד אפשר להשבית DOM של צל על ידי החלפה של createRenderRoot
כדי להחזיר this
. פעולה זו תבצע עיבוד של תבנית הרכיבים לאלמנט המותאם אישית עצמו במקום לשורש הצללית שמצורף לאלמנט המותאם אישית. אם תבחרו באפשרות הזו, לא תוכלו להשתמש באנקפסולציית סגנונות, באנקפסולציית DOM ובסמלי מיקום (slots).
ייצור
IE 11
אם אתם צריכים לתמוך בדפדפנים ישנים יותר כמו IE 11, תצטרכו לטעון כמה polyfills שיוצאים עוד כ-33kb. מידע נוסף זמין כאן.
חבילות מותנות
צוות Lit ממליץ להציג שתי חבילות שונות, אחת ל-IE 11 ואחת לדפדפנים מודרניים. יש לכך כמה יתרונות:
- הצגת ES 6 מהירה יותר ותתאים לרוב הלקוחות שלכם
- טרנספיילציה של ES 5 מגדילה באופן משמעותי את גודל החבילה
- חבילות מותנות מאפשרות לך ליהנות משני העולמות
- תמיכה ב-IE 11
- אין האטה בדפדפנים מודרניים
כאן תוכלו למצוא מידע נוסף על יצירת חבילה שמוצגת באופן מותנה באתר התיעוד שלנו.