Lit للمطوّرين في React

1. مقدمة

ما هو تطبيق Lit؟

Lit هي مكتبة بسيطة لإنشاء مكونات ويب سريعة وخفيفة الوزن تعمل في أي إطار عمل أو بدون أي إطار عمل على الإطلاق. باستخدام Lit، يمكنك إنشاء تطبيقات ومكوّنات وأنظمة تصميم قابلة للمشاركة وغير ذلك.

أهداف الدورة التعليمية

كيفية ترجمة العديد من مفاهيم React إلى Lit، مثل:

  • ‫JSX وإنشاء النماذج
  • المكوّنات والدعائم
  • الحالة ومراحل النشاط
  • عناصر الجذب
  • الأطفال
  • المراجع
  • حالة التوسّط

ما ستنشئه

في نهاية هذا الدرس العملي، ستتمكّن من تحويل مفاهيم مكوّنات React إلى نظائرها في Lit.

المتطلبات

  • أحدث إصدار من Chrome أو Safari أو Firefox أو Edge
  • معرفة بلغات HTML وCSS وJavaScript وأدوات مطوّري البرامج في Chrome
  • معرفة React
  • (متقدّم) إذا كنت تريد الحصول على أفضل تجربة تطوير، نزِّل VS Code. ستحتاج أيضًا إلى lit-plugin لبرنامج VS Code وNPM.

2. ‫Lit مقابل React

تتشابه المفاهيم والقدرات الأساسية في Lit مع تلك المتوفّرة في React من نواحٍ عديدة، ولكن تتضمّن Lit أيضًا بعض الاختلافات والميزات الرئيسية:

إنّها صغيرة

حجم Lit صغير جدًا: يبلغ حوالي 5 كيلوبايت بعد تصغيره وضغطه باستخدام gzip، مقارنةً بأكثر من 40 كيلوبايت في React وReactDOM.

رسم بياني شريطي يعرض حجم الحِزمة بعد تصغيرها وضغطها بالكيلو بايت. يبلغ حجم Lit bar‏ 5 كيلوبايت، بينما يبلغ حجم React + React DOM‏ 42.2 كيلوبايت

إنّه سريع

في مقاييس الأداء العامة التي تقارن نظام النماذج في Lit، أي lit-html، بنظام VDOM في React، يتبيّن أنّ lit-html أسرع بنسبة تتراوح بين %8 و%10 من React في أسوأ الحالات، وأسرع بنسبة %50 أو أكثر في حالات الاستخدام الأكثر شيوعًا.

تضيف LitElement (صنف أساسي لمكوّنات Lit) الحد الأدنى من الحمل الزائد إلى lit-html، ولكنها تتفوق على أداء React بنسبة تتراوح بين %16 و%30 عند مقارنة ميزات المكوّنات، مثل استخدام الذاكرة وأوقات التفاعل وبدء التشغيل.

رسم بياني شريطي مجمّع للأداء يقارن بين Lit وReact بالمللي ثانية (كلما كان الرقم أصغر كان الأداء أفضل)

لا تحتاج إلى إنشاء إصدار

بفضل ميزات المتصفّح الجديدة، مثل وحدات ES وعبارات النموذج الموسومة، لا يتطلّب تشغيل Lit تجميعًا. وهذا يعني أنّه يمكن إعداد بيئات التطوير باستخدام علامة نص برمجي + متصفّح + خادم، وستكون جاهزًا للبدء.

باستخدام وحدات ES وشبكات توصيل المحتوى الحديثة مثل Skypack أو UNPKG، قد لا تحتاج حتى إلى NPM للبدء.

مع ذلك، إذا أردت ذلك، سيظل بإمكانك إنشاء رمز Lit وتحسينه. كانت عملية توحيد جهود المطوّرين الأخيرة بشأن وحدات ECMAScript الأصلية مفيدةً لـ Lit، إذ تعتمد Lit على JavaScript العادية ولا تحتاج إلى واجهات سطر أوامر أو معالجة إصدارات خاصة بإطار العمل.

غير مرتبط بإطار عمل معيّن

تستند مكوّنات Lit إلى مجموعة من معايير الويب تُعرف باسم "مكوّنات الويب". وهذا يعني أنّ إنشاء مكوّن في Lit سيتوافق مع الأُطر الحالية والمستقبلية. إذا كان المتصفّح يتيح عناصر HTML، سيتيح أيضًا مكوّنات الويب.

المشاكل الوحيدة المتعلّقة بالتوافق بين الأُطر هي عندما تكون الأُطر محدودة الدعم لنظام DOM. ‫React هو أحد هذه الأُطر، ولكنّه يتيح استخدام "مفاتيح الهروب" من خلال Refs، ولا تقدّم Refs في React تجربة جيّدة للمطوّرين.

يعمل فريق Lit على مشروع تجريبي يُسمى @lit-labs/react، وهو يحلّل تلقائيًا مكوّنات Lit وينشئ برنامج تضمين React حتى لا تضطر إلى استخدام المراجع.

بالإضافة إلى ذلك، ستعرض لك أداة Custom Elements Everywhere الأُطر والمكتبات التي تتوافق بشكل جيد مع العناصر المخصّصة.

توفير دعم كامل للغة TypeScript

على الرغم من إمكانية كتابة كل رموز Lit البرمجية بلغة JavaScript، تمت كتابة Lit بلغة TypeScript وينصح فريق Lit المطوّرين باستخدام TypeScript أيضًا.

تعاون فريق Lit مع منتدى Lit للمساعدة في الحفاظ على المشاريع التي تتيح التحقّق من أنواع TypeScript واستخدام ميزة "الإكمال التلقائي" في نماذج Lit أثناء التطوير ومدّة التصميم باستخدام lit-analyzer وlit-plugin.

لقطة شاشة لبيئة تطوير متكاملة (IDE) تعرض عملية تحقّق غير سليمة من النوع عند ضبط القيمة المنطقية المحدّدة على رقم

لقطة شاشة لبيئة تطوير متكاملة (IDE) تعرض اقتراحات IntelliSense

أدوات المطوّرين مدمجة في المتصفّح

مكوّنات Lit هي مجرد عناصر HTML في DOM. وهذا يعني أنّه ليس عليك تثبيت أي أدوات أو إضافات للمتصفّح من أجل فحص المكوّنات.

يمكنك ببساطة فتح أدوات المطوّرين واختيار عنصر واستكشاف خصائصه أو حالته.

صورة لأدوات مطوّري البرامج في Chrome تعرض $0 returns <mwc-textfield> و$0.value returns hello world و$0.outlined returns true و{$0} تعرض توسيع السمة

تم تصميمها مع مراعاة العرض على جهة الخادم (SSR)

تم تصميم Lit 2 مع مراعاة إمكانية استخدام SSR. في وقت كتابة هذا الدرس التطبيقي حول الترميز، لم يطرح فريق Lit بعد أدوات SSR في شكل ثابت، ولكنّه كان ينشر المكوّنات المعروضة من جهة الخادم في جميع منتجات Google واختبر SSR في تطبيقات React. يتوقّع فريق Lit طرح هذه الأدوات خارجيًا على GitHub قريبًا.

في الوقت الحالي، يمكنك متابعة مستوى تقدّم فريق Lit هنا.

تتطلّب الحدّ الأدنى من الاستثمار

لا يتطلّب استخدام Lit التزامًا كبيرًا. يمكنك إنشاء مكوّنات في Lit وإضافتها إلى مشروعك الحالي. إذا لم تعجبك، لن تحتاج إلى تحويل التطبيق بأكمله في وقت واحد لأنّ مكوّنات الويب تعمل في أُطر أخرى.

هل أنشأت تطبيقًا كاملاً باستخدام Lit وتريد الانتقال إلى إطار عمل آخر؟ حسنًا، يمكنك بعد ذلك وضع تطبيق Lit الحالي داخل إطار العمل الجديد ونقل أي شيء تريده إلى مكوّنات إطار العمل الجديد.

بالإضافة إلى ذلك، تتيح العديد من الأُطر الحديثة إمكانية إخراج المحتوى في مكونات الويب، ما يعني أنّه يمكن عادةً تضمينها في عنصر Lit.

3- إعداد "الملعب" واستكشافه

يمكنك إجراء هذا الدرس العملي بطريقتَين:

  • يمكنك إجراء ذلك بالكامل على الإنترنت، في المتصفّح
  • (متقدّم) يمكنك إجراء ذلك على جهازك المحلي باستخدام VS Code

الوصول إلى الرمز

خلال درس تطبيقي حول الترميز، ستظهر روابط إلى ساحة تجارب Lit على النحو التالي:

ساحة اللعب هي بيئة اختبار للرموز البرمجية تعمل بالكامل في المتصفّح. يمكنه تجميع ملفات TypeScript وJavaScript وتشغيلها، ويمكنه أيضًا حل عمليات الاستيراد تلقائيًا إلى وحدات node، على سبيل المثال:

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

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

يمكنك إكمال البرنامج التعليمي بالكامل في ساحة تجارب Lit، باستخدام نقاط التحقّق هذه كنقاط بداية. إذا كنت تستخدم VS Code، يمكنك استخدام نقاط التحقّق هذه لتنزيل رمز البداية لأي خطوة، بالإضافة إلى استخدامها للتحقّق من عملك.

استكشاف واجهة مستخدم "ساحة الألعاب المضيئة"

تم تصنيف شريط علامات التبويب الخاص بأداة اختيار الملفات على أنّه القسم 1، وقسم تعديل الرموز البرمجية على أنّه القسم 2، ومعاينة الناتج على أنّه القسم 3، وزر إعادة تحميل المعاينة على أنّه القسم 4.

تُبرز لقطة شاشة لواجهة مستخدم Lit playground الأقسام التي ستستخدمها في هذا الدرس البرمجي.

  1. أداة اختيار الملفات لاحظ زرّ الإضافة...
  2. محرّر الملفات
  3. معاينة الرمز البرمجي
  4. زر إعادة التحميل
  5. زر التنزيل

إعداد VS Code (متقدّم)

في ما يلي مزايا استخدام إعداد VS Code هذا:

  • التحقّق من نوع النموذج
  • ميزة IntelliSense والإكمال التلقائي للنماذج

إذا كان لديك NPM وVS Code (مع المكوّن الإضافي lit-plugin) مثبّتَين مسبقًا وتعرف كيفية استخدام هذه البيئة، يمكنك ببساطة تنزيل هذه المشاريع وبدء استخدامها من خلال اتّباع الخطوات التالية:

  • انقر على زر التنزيل
  • استخراج محتوى ملف tar إلى دليل
  • (في حال استخدام TypeScript) يمكنك إعداد ملف tsconfig سريع يعرض وحدات es وes2015+
  • ثبِّت خادم تطوير يمكنه تحليل محدّدات وحدات البرامج المجردة (ينصح فريق Lit باستخدام ‎@web/dev-server)
  • شغِّل خادم التطوير وافتح المتصفّح (إذا كنت تستخدم ‎ @web/dev-server، يمكنك استخدام npx web-dev-server --node-resolve --watch --open)
    • إذا كنت تستخدم المثال package.json استخدِم npm run dev

4. ‫JSX وإنشاء النماذج

في هذا القسم، ستتعرّف على أساسيات إنشاء النماذج في 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
);

في المثال أعلاه، هناك عنصران ومتغيّر "الاسم" مضمّن. في Lit، يمكنك إجراء ما يلي:

import {html, render} from 'lit';

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

render(
  element,
  mountNode
);

يُرجى العِلم أنّ نماذج Lit لا تحتاج إلى React Fragment لتجميع عناصر متعدّدة في نماذجها.

في Lit، يتم تضمين النماذج في html نموذج موسوم LITeral، وهو المكان الذي استمدّت منه Lit اسمها.

قيم النموذج

يمكن أن تقبل نماذج Lit نماذج Lit أخرى، تُعرف باسم TemplateResult. على سبيل المثال، ضع name بين علامات الخط المائل (<i>) ثم ضعها بين علامات حرفية للنموذج ملاحظة: تأكَّد من استخدام حرف الفاصلة العليا المائلة (`) وليس حرف علامة الاقتباس المفردة (').

import {html, render} from 'lit';

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

render(
  element,
  mountNode
);

يمكن أن تقبل عناصر Lit TemplateResult المصفوفات والسلاسل وعناصر 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
);

في المثال أعلاه، تم تحديد إدخال ينفّذ ما يلي:

  • يتم ضبط القيمة "غير مفعَّل" على متغيّر محدّد (false في هذه الحالة)
  • يضبط هذا الإجراء الفئة على static-class بالإضافة إلى متغيّر ("static-class my-class" في هذه الحالة)
  • تحديد قيمة تلقائية

في Lit، يمكنك إجراء ما يلي:

import {html, render} from 'lit';

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

render(
  element,
  mountNode
);

في مثال Lit، تتم إضافة ربط منطقي لتبديل السمة disabled.

بعد ذلك، هناك ربط مباشر بالسمة class بدلاً من className. يمكن إضافة عمليات ربط متعددة إلى السمة class، ما لم تكن تستخدم التوجيه classMap الذي يمثّل أداة مساعدة تعريفية لتبديل الفئات.

أخيرًا، يتم ضبط السمة value على الإدخال. على عكس React، لن يؤدي ذلك إلى ضبط عنصر الإدخال على أنّه للقراءة فقط، لأنّه يتّبع التنفيذ والسلوك الأصليين للإدخال.

بنية ربط السمة الحرفية

html`<my-element ?attribute-name=${booleanVar}>`;
  • البادئة ? هي بنية الربط لتفعيل سمة في عنصر أو إيقافها
  • تعادل inputRef.toggleAttribute('attribute-name', booleanVar)
  • مفيدة للعناصر التي تستخدم disabled لأنّ DOM يظل يقرأ disabled="false" على أنّه صحيح لأنّ inputElement.hasAttribute('disabled') === true
html`<my-element .property-name=${anyVar}>`;
  • البادئة . هي صيغة الربط لضبط إحدى خصائص العنصر
  • تعادل inputRef.propertyName = anyVar
  • مناسبة لنقل البيانات المعقّدة، مثل الكائنات أو المصفوفات أو الفئات
html`<my-element attribute-name=${stringVar}>`;
  • يربط بسمة عنصر
  • تعادل inputRef.setAttribute('attribute-name', stringVar)
  • مناسب للقيم الأساسية وأدوات اختيار قواعد الأنماط وquerySelectors

معالجات التمرير

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

ReactDOM.render(
  element,
  mountNode
);

في المثال أعلاه، تم تحديد إدخال ينفّذ ما يلي:

  • تسجيل الكلمة "click" عند النقر على الإدخال
  • تسجيل قيمة الإدخال عندما يكتب المستخدم حرفًا

في Lit، يمكنك إجراء ما يلي:

import {html, render} from 'lit';

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

render(
  element,
  mountNode
);

في مثال Lit، تمت إضافة متتبِّع إلى الحدث click باستخدام @click.

بعد ذلك، بدلاً من استخدام onChange، هناك ربط بحدث input الأصلي<input> لأنّ حدث change الأصلي لا يتم إطلاقه إلا على blur (تجرّد React من هذه الأحداث).

بنية معالج أحداث Lit

html`<my-element @event-name=${() => {...}}></my-element>`;
  • البادئة @ هي صيغة الربط لمتتبِّع الأحداث
  • تعادل inputRef.addEventListener('event-name', ...)
  • يستخدم أسماء أحداث DOM الأصلية

5- المكوّنات والدعائم

في هذا القسم، سنتعرّف على مكوّنات وفئات Lit ووظائفها. سنتناول State وHooks بمزيد من التفاصيل في الأقسام اللاحقة.

مكوّنات الصف وLitElement

إنّ العنصر LitElement هو المكافئ في Lit لعنصر فئة React، كما أنّ مفهوم "السمات التفاعلية" في 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 = '';
  • تحدّد هذه السمة خاصية تفاعلية عامة، وهي جزء من واجهة برمجة التطبيقات العامة للمكوّن.
  • تعرض السمة (تلقائيًا) بالإضافة إلى خاصية في المكوّن
  • تحدّد هذه السمة كيفية ترجمة سمة المكوّن (وهي عبارة عن سلاسل) إلى قيمة
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 بتعريف فئة
  • بسبب معيار Custom Elements، يجب أن يتضمّن اسم العلامة شرطة (-)
  • يشير 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 تفسيرًا مطابقًا تمامًا لمكوّن الدالة لأنّه لا يستخدم 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 هو مزيج من حالة 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، ولكنّها قابلة للتغيير
  • واجهة برمجة تطبيقات عامة يمكن للمستهلكين الوصول إليها وضبطها

حالة تفاعلية داخلية

// 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 مزيجًا من عمليات معاودة الاتصال بدورة الحياة 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 بواسطة المكوّن
  • على عكس React، ستؤدي التغييرات التي يتم إجراؤها على الخصائص التفاعلية في firstUpdated إلى إعادة العرض، على الرغم من أنّ المتصفّح عادةً ما يجمع التغييرات في الإطار نفسه.componentDidMount إذا كانت هذه التغييرات لا تتطلّب الوصول إلى DOM الأساسي، يجب عادةً وضعها في willUpdate

connectedCallback

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

// Lit
connectedCallback() {
  super.connectedCallback();
  this.window.addEventListener('resize', this.boundOnResize);
}
  • يتم استدعاء هذه الطريقة كلما تم إدراج العنصر المخصّص في شجرة نموذج العناصر في المستند.
  • على عكس مكوّنات React، عندما يتم فصل العناصر المخصّصة عن واجهة Document Object Model ‏(DOM)، لا يتم إتلافها، وبالتالي يمكن "ربطها" عدة مرات
    • لن يتم استدعاء "firstUpdated" مرة أخرى
  • مفيد لإعادة تهيئة DOM أو إعادة ربط أدوات معالجة الأحداث التي تم تنظيفها عند قطع الاتصال
  • ملاحظة: قد يتم استدعاء connectedCallback قبل firstUpdated، لذا قد لا يكون نموذج المستند متاحًا عند الاستدعاء الأول.

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 (باستخدام صيغة الماضي من كلمة "تعديل" باللغة الإنجليزية)
  • على عكس 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 داخل الجذر مرتبطًا بالشجرة الفرعية للجذر
  • مفيدة لتنظيف أدوات معالجة الأحداث والمراجع التي تتضمّن أخطاءً، ما يتيح للمتصفّح جمع البيانات غير المرغوب فيها من المكوّن

تمرين

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.

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

بعد ذلك، يأتي تنفيذ componentDidMount. مرة أخرى، فإنّ مقياس Lit التناظري هو مزيج من firstUpdated وconnectedCallback. في حالة هذا المكوّن، لا يتطلّب استدعاء tick باستخدام setInterval الوصول إلى DOM داخل الجذر. بالإضافة إلى ذلك، سيتم محو الفاصل الزمني عند إزالة العنصر من شجرة المستندات، لذا إذا تمت إعادة ربطه، يجب بدء الفاصل الزمني مرة أخرى. وبالتالي، فإنّ connectedCallback هو الخيار الأفضل هنا.

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

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

  ...
}

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

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

أخيرًا، نظِّف الفترة الفاصلة حتى لا يتم تنفيذ العلامة بعد فصل العنصر عن شجرة المستند.

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

عند وضع كل ذلك معًا، يجب أن يبدو على النحو التالي:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

7. عناصر الجذب

في هذا القسم، سنتعرّف على كيفية تحويل مفاهيم React Hook إلى Lit.

مفاهيم خطّافات React

توفّر خطافات React طريقة لربط مكوّنات الدوال بالحالة. وهناك عدة مزايا لذلك.

  • تساعد في تبسيط إعادة استخدام المنطق ذي الحالة
  • المساعدة في تقسيم أحد المكوّنات إلى دوال أصغر

بالإضافة إلى ذلك، ساهم التركيز على المكوّنات المستندة إلى الدوال في حلّ بعض المشاكل المتعلّقة ببنية React المستندة إلى الفئات، مثل:

  • يجب تمرير props من constructor إلى super
  • الإعداد غير المنظَّم للسمات في constructor
    • كان هذا أحد الأسباب التي ذكرها فريق React في ذلك الوقت، ولكن تم حلّها باستخدام ES2019.
  • المشاكل التي تسبّبت فيها this ولم تعُد تشير إلى المكوّن

مفاهيم خطافات React في Lit

كما هو موضّح في قسم المكوّنات والخصائص، لا توفّر 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 في الغالبية العظمى من الحالات إلى مرجع العنصر المخصّص
  • يمكن الآن إنشاء مثيلات لخصائص الفئات كعناصر في الفئة. يؤدي ذلك إلى تنظيف عمليات التنفيذ المستندة إلى الدالة الإنشائية

Reactive Controllers

تتوفّر المفاهيم الأساسية التي تستند إليها 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 استخدامه في معظم الحالات الأساسية.

الآن، عليك ترجمة عمليات معاودة الاتصال بدورة حياة React إلى عمليات معاودة الاتصال بوحدة التحكّم. باختصار:

  • 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);

لاستخدام أداة التحكّم، عليك إنشاء مثيل لها من خلال تمرير مرجع إلى مضيف أداة التحكّم (وهو مكوّن <my-element>)، ثم استخدام أداة التحكّم في طريقة render.

تشغيل عمليات إعادة العرض في وحدة التحكّم

لاحظ أنّه سيتم عرض الوقت، ولكن لن يتم تعديله. ويرجع ذلك إلى أنّ وحدة التحكّم تضبط التاريخ كل ثانية، ولكن المضيف لا يحدّثه. ويرجع ذلك إلى أنّ date يتغيّر في الفئة ClockController وليس في المكوّن بعد الآن. هذا يعني أنّه بعد ضبط date على وحدة التحكّم، يجب إخبار المضيف بتنفيذ دورة حياة التحديث باستخدام host.requestUpdate().

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

من المفترض أن يبدأ العدّ التنازلي الآن.

للحصول على مقارنة أكثر تفصيلاً لحالات الاستخدام الشائعة مع خطافات، يُرجى الاطّلاع على قسم مواضيع متقدّمة - خطافات.

8. الأطفال

في هذا القسم، ستتعرّف على كيفية استخدام الفتحات لإدارة العناصر التابعة في Lit.

مساحات إعلانية وأطفال

تتيح لك الخانات إنشاء التركيب من خلال السماح لك بتضمين المكوّنات.

في React، يتم توريث العناصر الفرعية من خلال props. الفتحة التلقائية هي 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>
   `;
  }
}

فترات زمنية متعددة

في React، تكون إضافة خانات متعدّدة مماثلة تمامًا لتوريث المزيد من الخصائص.

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

وبالمثل، يؤدي إضافة المزيد من عناصر <slot> إلى إنشاء المزيد من الخانات في Lit. تم تحديد خانات متعدّدة باستخدام السمة name: <slot name="slot-name">. يتيح ذلك للأطفال تحديد الفترة الزمنية التي سيتم تخصيصها لهم.

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

محتوى الوحدة الإعلانية التلقائي

ستعرض الخانات الشجرة الفرعية عندما لا تكون هناك أي عقدة معروضة في تلك الخانة. عندما يتم عرض العُقد في موضع، لن يعرض هذا الموضع الشجرة الفرعية الخاصة به، بل سيعرض العُقد المعروضة.

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

تحديد الأطفال في خانات

في React، يتم تعيين العناصر الثانوية إلى الخانات من خلال خصائص أحد المكوّنات. في المثال أدناه، يتم تمرير عناصر React إلى السمتَين headerChildren وsectionChildren.

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

في Lit، يتم تعيين العناصر الثانوية إلى الخانات باستخدام السمة slot.

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

إذا لم تكن هناك خانة تلقائية (مثل <slot>) ولم تكن هناك خانة تحتوي على السمة name (مثل <slot name="foo">) التي تتطابق مع السمة slot للعناصر الثانوية للعنصر المخصّص (مثل <div slot="foo">)، لن يتم عرض هذه العقدة ولن تظهر.

9- المراجع

في بعض الأحيان، قد يحتاج المطوِّر إلى الوصول إلى واجهة برمجة التطبيقات الخاصة بعنصر HTMLElement.

في هذا القسم، ستتعرّف على كيفية الحصول على مراجع العناصر في Lit.

مراجع React

يتم تحويل أحد مكونات 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>
 );
};

في المثال أعلاه، سينفّذ مكوّن React ما يلي:

  • عرض حقل إدخال نص فارغ وزر يتضمّن نصًا
  • تركيز الإدخال عند النقر على الزر

بعد العرض الأولي، سيضبط React قيمة inputRef.current على HTMLInputElement الذي تم إنشاؤه من خلال السمة ref.

ميزة "المراجع" مع @query

تعمل Lit بالقرب من المتصفّح وتوفّر تجريدًا بسيطًا جدًا لميزات المتصفّح الأصلية.

إنّ العنصر المكافئ لـ refs في React هو 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 (إنشاء أداة جلب لـ HTMLInputElement).
  • تُعلن هذه السمة عن دالة ردّ الاتصال الخاصة بحدث ناتج عن النقر وتُرفقها، ويُطلق على هذه الدالة اسم onButtonClick.
  • يركّز الإدخال على النقرة على الزر

في JavaScript، تنفّذ أدوات التزيين @query و@queryAll الوظيفتَين querySelector وquerySelectorAll على التوالي. هذا هو المكافئ في JavaScript لـ @query('input') inputEl!: HTMLInputElement;

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

بعد أن يلتزم مكوّن Lit بنموذج طريقة render إلى جذر my-element، سيسمح الآن أداة التزيين @query لـ inputEl بعرض أول عنصر input تم العثور عليه في جذر العرض. سيعرض null إذا لم يتمكّن @query من العثور على العنصر المحدّد.

إذا كان هناك عناصر input متعددة في جذر العرض، ستعرض الدالة @queryAll قائمة بالعُقد.

10. حالة التوسّط

في هذا القسم، ستتعرّف على كيفية التوسّط في الحالة بين المكوّنات في Lit.

المكوّنات القابلة لإعادة الاستخدام

تحاكي React مسارات العرض الوظيفية مع تدفق البيانات من الأعلى إلى الأسفل. يقدّم الأهل الحالة للأطفال من خلال الدعائم. يتواصل الأطفال مع الوالدَين من خلال عمليات رد الاتصال التي يتم العثور عليها في الخصائص.

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


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

في المثال أعلاه، ينفّذ أحد مكوّنات React ما يلي:

  • تنشئ هذه الدالة تصنيفًا استنادًا إلى القيمة props.step.
  • تعرض هذه السمة زرًا يحمل العلامة ‎+step أو ‎-step
  • تعدّل المكوّن الأصل من خلال استدعاء props.addToCounter مع props.step كمعلَمة عند النقر

على الرغم من إمكانية تمرير عمليات رد الاتصال في Lit، إلا أنّ الأنماط التقليدية مختلفة. يمكن كتابة مكوّن React في المثال أعلاه كمكوّن Lit في المثال أدناه:

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

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

    this.dispatchEvent(event);
  }

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

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

في المثال أعلاه، سيفعل مكوّن Lit ما يلي:

  • إنشاء السمة التفاعلية step
  • إرسال حدث مخصّص باسم update-counter يحمل قيمة step للعنصر عند النقر

تنتقل أحداث المتصفح من العناصر الثانوية إلى العناصر الرئيسية. تسمح الأحداث للأطفال ببث أحداث التفاعل وتغييرات الحالة. تُمرِّر React الحالة بشكل أساسي في الاتجاه المعاكس، لذا من غير الشائع أن نرى مكوّنات React ترسل الأحداث وتستمع إليها بالطريقة نفسها التي تعمل بها مكوّنات Lit.

المكوّنات التي تحتفظ بحالتها

في React، من الشائع استخدام الخطّافات لإدارة الحالة. يمكن إنشاء MyCounter مكون من خلال إعادة استخدام CounterButton مكون. لاحظ كيف يتم تمرير addToCounter إلى كلتا نسختَي CounterButton.

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

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

يُجري المثال أعلاه ما يلي:

  • تنشئ هذه السمة حالة count.
  • تنشئ هذه السمة دالة ردّ نداء تضيف رقمًا إلى حالة count.
  • يستخدم CounterButton addToCounter لتعديل count بمقدار step في كل نقرة.

يمكن تحقيق عملية تنفيذ مماثلة لـ MyCounter في Lit. لاحظ كيف لا يتم تمرير addToCounter إلى counter-button. بدلاً من ذلك، يتم ربط دالة الرجوع كمتتبِّع الأحداث بالحدث @update-counter على عنصر رئيسي.

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

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

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

يُجري المثال أعلاه ما يلي:

  • تُنشئ هذه السمة خاصية تفاعلية باسم count سيتم تعديل المكوّن عند تغيير القيمة.
  • يربط هذا الحقل عملية رد الاتصال addToCounter بمتتبِّع الأحداث @update-counter.
  • تعدِّل count من خلال إضافة القيمة التي تم العثور عليها في detail.step للحدث update-counter
  • تضبط هذه السمة قيمة step الخاصة بـ counter-button من خلال السمة step

من الشائع أكثر استخدام خصائص تفاعلية في Lit لبث التغييرات من العناصر الرئيسية إلى العناصر الثانوية. وبالمثل، من الممارسات الجيدة استخدام نظام الأحداث في المتصفح لتمرير التفاصيل من الأسفل إلى الأعلى.

يتّبع هذا النهج أفضل الممارسات ويتوافق مع هدف Lit المتمثل في توفير دعم من عدّة منصات لمكوّنات الويب.

11. التصميم

في هذا القسم، سنتعرّف على كيفية إضافة أنماط في Lit.

التصميم

توفّر Lit طرقًا متعددة لتصميم العناصر بالإضافة إلى حلّ مضمّن.

الأنماط المضمّنة

يتيح Lit استخدام الأنماط المضمّنة بالإضافة إلى ربطها.

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

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

في المثال أعلاه، هناك عنوانان لكل منهما نمط مضمّن.

الآن، استورِد حدودًا واربطها من 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 tagged template literal.

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

const ORANGE = css`orange`;

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

      #purple {
        color: rebeccapurple;
      }
    `
  ];

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

يُجري المثال أعلاه ما يلي:

  • تعريف سلسلة حرفية لنموذج موسوم بلغة CSS مع ربط
  • تحديد ألوان عنصرَي h1 باستخدام المعرّفات

مزايا استخدام علامة النموذج css:

  • يتم تحليلها مرة واحدة لكل فئة مقابل مرة واحدة لكل مثيل
  • تمّ تنفيذها مع مراعاة إمكانية إعادة استخدام الوحدات
  • يمكن فصل الأنماط بسهولة في ملفات منفصلة
  • متوافق مع CSS Custom Properties polyfill

بالإضافة إلى ذلك، يُرجى الانتباه إلى العلامة <style> في index.html:

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

سيحدّد Lit نطاق أنماط مكوّناتك لتشمل عناصرها الأساسية. وهذا يعني أنّه لن يتم تسريب الأنماط إلى الداخل والخارج. لنقل الأنماط إلى المكوّنات، ينصح فريق Lit باستخدام خصائص CSS المخصّصة لأنّها يمكن أن تخترق نطاق نمط Lit.

علامات الأنماط

يمكن أيضًا تضمين علامات <style> في النماذج. سيزيل المتصفّح العلامات المكرّرة من هذه العلامات، ولكن من خلال وضعها في النماذج، سيتم تحليلها لكل مثيل من المكوّن بدلاً من كل فئة كما هو الحال مع النموذج الذي يحمل العلامة css. بالإضافة إلى ذلك، فإنّ عملية إزالة تكرار CSSResults في المتصفّح أسرع بكثير.

يمكن أيضًا استخدام <link rel="stylesheet"> في النموذج للأنماط، ولكن لا يُنصح بذلك أيضًا لأنّه قد يتسبّب في ظهور محتوى غير منمّق بشكل مفاجئ في البداية (FOUC).

12. المواضيع المتقدّمة (اختياري)

‫JSX وإنشاء النماذج

Lit & Virtual DOM

لا تتضمّن Lit-html نموذج DOM افتراضيًا تقليديًا يختلف عن كل عقدة فردية. بدلاً من ذلك، تستخدم هذه المكتبة ميزات الأداء المتأصلة في مواصفات tagged template literal في ES2015. إنّ Tagged template literals هي سلاسل template literal مع دوال علامات مرتبطة بها.

في ما يلي مثال على سلسلة نموذجية:

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 جديدة عند كل استدعاء لدالة العلامة، لأنّه يستخدم السلسلة الحرفية نفسها (أي في الموقع نفسه في شجرة بنية التجريد). وبالتالي، يمكن أن تستفيد ميزات ربط البيانات وتحليلها وتخزين النماذج مؤقتًا في Lit من هذه الميزات بدون تكبُّد الكثير من النفقات العامة المتعلقة بمقارنة البيانات في وقت التشغيل.

يمنح سلوك المتصفّح المضمّن هذا الخاص بسلاسل نماذج العلامات الحرفية Lit ميزة أداء كبيرة. تنفّذ معظم أنظمة DOM الافتراضية التقليدية معظم عملها في JavaScript. ومع ذلك، تنفّذ السلاسل الحرفية للنماذج الموسومة معظم عمليات المقارنة في C++ في المتصفّح.

إذا أردت البدء باستخدام عناصر HTML الحرفية المميّزة بعلامات مع React أو Preact، ينصح فريق Lit باستخدام مكتبة htm.

مع ذلك، وكما هو الحال مع موقع Google Codelabs وبعض أدوات تعديل الرموز البرمجية على الإنترنت، ستلاحظ أنّ تمييز بناء الجملة الحرفي للنماذج الموسومة ليس شائعًا جدًا. تتوافق بعض بيئات التطوير المتكاملة ومحرّرات النصوص معها تلقائيًا، مثل Atom وأداة تمييز كتل الرموز البرمجية في GitHub. يتعاون فريق Lit أيضًا بشكل وثيق مع المنتدى للحفاظ على المشاريع، مثل lit-plugin، وهو مكوّن إضافي لـ VS Code سيضيف تمييزًا للبنية والتحقّق من النوع وIntelliSense إلى مشاريع Lit.

‫Lit وJSX وReact DOM

لا يتم تشغيل JSX في المتصفّح، بل يتم استخدام معالج مسبق لتحويل JSX إلى استدعاءات وظائف JavaScript (عادةً من خلال Babel).

على سبيل المثال، سيحوّل Babel ما يلي:

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

إلى ما يلي:

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

بعد ذلك، تأخذ React DOM ناتج React وتحوّله إلى DOM الفعلي، أي الخصائص والسمات وأدوات معالجة الأحداث وكل ما يلزم.

تستخدم مكتبة 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 أقل من 5 كيلوبايت بعد تصغيره وضغطه باستخدام gzip، مقارنةً بحجم React (2.8 كيلوبايت) + react-dom (39.4 كيلوبايت) الذي يبلغ 40 كيلوبايت بعد تصغيره وضغطه باستخدام gzip.

الفعاليات

تستخدم React نظام أحداث اصطناعية. هذا يعني أنّه يجب أن تحدّد react-dom كل حدث سيتم استخدامه في كل مكوّن وأن توفّر متتبِّع أحداث مكافئًا بنظام الكتابة المختلطة لكل نوع من العُقد. نتيجةً لذلك، لا تتضمّن JSX طريقة لتحديد متتبِّع أحداث لحدث مخصّص، وعلى المطوّرين استخدام ref ثم تطبيق متتبِّع بشكل إلزامي. يؤدي ذلك إلى تقديم تجربة غير مرضية للمطوّرين عند دمج المكتبات التي لا تتوافق مع React، ما يؤدي إلى الحاجة إلى كتابة برنامج تضمين خاص بـ React.

يصل Lit-html مباشرةً إلى DOM ويستخدم الأحداث الأصلية، لذا فإنّ إضافة أدوات معالجة الأحداث سهلة مثل @event-name=${eventNameListener}. وهذا يعني أنّه يتم إجراء تحليل أقل لوقت التشغيل عند إضافة أدوات معالجة الأحداث وكذلك عند تشغيل الأحداث.

المكوّنات والدعائم

مكوّنات React والعناصر المخصّصة

تستخدم LitElement في الخلفية عناصر مخصّصة لتجميع مكوّناتها. تتضمّن العناصر المخصّصة بعض المفاضلات بين مكوّنات React عندما يتعلّق الأمر بتقسيم المكوّنات (تتم مناقشة الحالة ودورة الحياة بشكل أكبر في قسم الحالة ودورة الحياة).

في ما يلي بعض مزايا استخدام "العناصر المخصّصة" كنظام للمكوّنات:

  • مدمجة في المتصفّح ولا تتطلّب أي أدوات
  • التوافق مع جميع واجهات برمجة التطبيقات للمتصفحات من innerHTML وdocument.createElement إلى querySelector
  • يمكن استخدامها عادةً في مختلف الأُطر
  • يمكن تسجيلها بشكل غير مباشر باستخدام customElements.define و "إعادة ترطيب" نموذج المستند (DOM)

في ما يلي بعض عيوب "العناصر المخصّصة" مقارنةً بمكوّنات React:

  • لا يمكن إنشاء عنصر مخصّص بدون تحديد فئة (وبالتالي لا يمكن إنشاء مكونات وظيفية تشبه JSX)
  • يجب أن تحتوي على علامة إغلاق
    • ملاحظة: على الرغم من أنّ مورّدي المتصفّحات يفضّلون استخدام علامة الإغلاق الذاتي لتسهيل عمل المطوّرين، إلا أنّهم يندمون على مواصفات علامة الإغلاق الذاتي، ولهذا السبب لا تتضمّن المواصفات الأحدث علامات الإغلاق الذاتي.
  • إضافة عقدة إضافية إلى شجرة نموذج العناصر في المستند (DOM) قد تتسبّب في حدوث مشاكل في التنسيق
  • يجب التسجيل من خلال JavaScript

تستخدم Lit العناصر المخصّصة بدلاً من نظام العناصر المخصّصة لأنّ العناصر المخصّصة مدمجة في المتصفّح، ويرى فريق Lit أنّ المزايا المتوافقة مع جميع الأُطر تفوق المزايا التي توفّرها طبقة تجريد المكوّنات. في الواقع، تغلّب فريق Lit على المشاكل الرئيسية المتعلقة بتسجيل JavaScript من خلال جهوده في مجال lit-ssr. بالإضافة إلى ذلك، تستفيد بعض الشركات، مثل 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 لأنّ كلّاً من props وstate هما خصائص الفئة نفسها

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

هناك العديد من عمليات معاودة الاتصال بدورة الحياة التي لم يتم ذكرها في القسم السابق لأنّه لا يوجد ما يماثلها في React. وهي:

attributeChangedCallback

يتم استدعاؤه عند حدوث تغيير في أحد observedAttributes العناصر. يشكّل كل من observedAttributes وattributeChangedCallback جزءًا من مواصفات العناصر المخصّصة، وتنفّذهما مكتبة Lit بشكل غير مرئي لتوفير واجهة برمجة تطبيقات للسمات لعناصر Lit.

adoptedCallback

يتم استدعاء هذا الإجراء عندما يتم نقل المكوّن إلى مستند جديد، مثلاً من HTMLTemplateElement documentFragment إلى document الرئيسي. هذه الدالة هي أيضًا جزء من مواصفات العناصر المخصّصة، ويجب استخدامها فقط في حالات الاستخدام المتقدّمة عندما يغيّر المكوّن المستندات.

طُرق وسمات دورة الحياة الأخرى

هذه الطرق والخصائص هي عناصر فئة يمكنك استدعاؤها أو إلغاءها أو انتظارها للمساعدة في معالجة عملية دورة الحياة.

updateComplete

هذا هو Promise الذي يتم تنفيذه عند انتهاء العنصر من التحديث لأنّ دورات حياة التحديث والعرض غير متزامنة. مثال:

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

getUpdateComplete

هذه طريقة يجب إلغاؤها لتخصيص وقت حلّ updateComplete. يحدث ذلك عادةً عندما يعرض أحد المكوّنات مكوّنًا فرعيًا ويجب أن تكون دورات العرض متزامنة، مثلاً:

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

performUpdate

هذه الطريقة هي التي تستدعي عمليات معاودة الاتصال بدورة حياة التحديث. لا يُنصح باستخدام هذه الطريقة بشكل عام إلا في حالات نادرة يجب فيها إجراء التحديث بشكل متزامن أو عند استخدام جدولة مخصّصة.

hasUpdated

تكون قيمة هذه السمة true إذا تم تعديل المكوّن مرة واحدة على الأقل.

isConnected

هذه السمة هي جزء من مواصفات العناصر المخصّصة، وستكون قيمتها true إذا كان العنصر مرتبطًا حاليًا بشجرة المستند الرئيسية.

Lit Update Lifecycle Visualization

تتضمّن دورة حياة التحديث 3 أجزاء:

  • قبل التحديث
  • تعديل
  • بعد التحديث

Pre-Update

رسم بياني موجّه غير دوري للعُقد التي تتضمّن أسماء دوال رد الاتصال. الدالة الإنشائية لطلب التحديث. ‫@property إلى Property Setter وattributeChangedCallback إلى Property Setter يتم تغيير قيمة Property Setter إلى hasChanged، ثم يتم تغيير قيمة hasChanged إلى requestUpdate، وتشير requestUpdate إلى الرسم البياني التالي لدورة حياة التحديث.

بعد requestUpdate، من المتوقّع أن يتم طرح تحديث مُجدوَل.

تعديل

رسم بياني موجّه غير دوري للعُقد مع أسماء دوال رد الاتصال سهم من الصورة السابقة لنقاط مراحل النشاط قبل التحديث إلى performUpdate. يشير performUpdate إلى shouldUpdate. يشير shouldUpdate إلى كل من &quot;إكمال التحديث إذا كانت القيمة خطأ&quot; وwillUpdate. يشير willUpdate إلى update. يشير update إلى كل من render وإلى الرسم البياني التالي لمراحل النشاط بعد التحديث. يشير render أيضًا إلى الرسم البياني التالي لمراحل النشاط بعد التحديث.

Post-Update

رسم بياني موجّه غير دوري للعُقد مع أسماء دوال رد الاتصال سهم من الصورة السابقة لنقاط دورة حياة التحديث يشير إلى firstUpdated. سهم من firstUpdated إلى updated. سهم من updated إلى updateComplete.

عناصر الجذب

لماذا عناصر الجذب؟

تم تقديم Hooks في React لحالات استخدام بسيطة لمكوّنات الدوال التي تتطلّب حالة. في العديد من الحالات البسيطة، تكون مكوّنات الدوال التي تتضمّن خطافات أبسط بكثير وأسهل في القراءة من مكوّنات الفئات المقابلة. ومع ذلك، عند تقديم تعديلات غير متزامنة على الحالة بالإضافة إلى تمرير البيانات بين الخطافات أو التأثيرات، لا يكفي نمط الخطافات، ويميل الحل المستند إلى الفئات، مثل وحدات التحكّم التفاعلية، إلى التميّز.

خطافات وعناصر تحكّم طلبات البيانات من واجهة برمجة التطبيقات

من الشائع كتابة دالة ربط تطلب بيانات من واجهة برمجة تطبيقات. على سبيل المثال، لنأخذ مكوّن دالة React هذا الذي ينفّذ ما يلي:

  • index.tsx
    • عرض النص
    • عرض ردّ useAPI
      • رقم تعريف المستخدم + اسم المستخدم
      • رسالة الخطأ
        • 404 عندما يصل إلى المستخدم 11 (حسب التصميم)
        • عرض خطأ الإلغاء في حال تم إلغاء عملية جلب البيانات من واجهة برمجة التطبيقات
      • جارٍ تحميل الرسالة
    • تعرض هذه السمة زر إجراء
      • المستخدم التالي: لاسترداد بيانات المستخدم التالي من واجهة برمجة التطبيقات
      • إلغاء: يؤدي إلى إيقاف عملية استرجاع البيانات من واجهة برمجة التطبيقات وعرض خطأ
  • useApi.tsx
    • تحديد useApi خطاف مخصّص
    • سيتم استرداد كائن مستخدم بشكل غير متزامن من واجهة برمجة التطبيقات
    • الإصدارات:
      • اسم المستخدم
      • ما إذا كان يتم تحميل عملية الجلب
      • أي رسائل خطأ
      • دالة ردّ الاتصال لإيقاف عملية الجلب
    • إلغاء عمليات الجلب الجارية في حال تم إلغاء التحميل

إليك عملية تنفيذ Lit + Reactive Controller.

الخلاصات:

  • تشبه عناصر التحكّم التفاعلية إلى حد كبير الخطافات المخصّصة
  • تمرير بيانات غير قابلة للعرض بين الدوالّ الاحتياطية والتأثيرات
    • تستخدم React useRef لنقل البيانات بين useEffect وuseCallback
    • تستخدم Lit خاصية فئة خاصة
    • تتطابق React بشكل أساسي مع سلوك سمة فئة خاصة

بالإضافة إلى ذلك، إذا كنت تفضّل بنية مكوّنات دالة React مع الخطافات ولكن في بيئة 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 في وحدة التحكّم على slotchange باستخدام assignedNodes.

@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) الذي يتم عرضه لاحقًا من خلال أداة تزيين @query في Lit أو مرجع React.

‫React هي مسار وظيفي ينشئ "مكوّنات React" وليس HTMLElements. بما أنّه يتم تعريف Ref قبل عرض HTMLElement، يتم تخصيص مساحة في الذاكرة. لهذا السبب تظهر القيمة null كقيمة أولية لـ Ref، لأنّه لم يتم إنشاء (أو عرض) عنصر DOM الفعلي بعد، أي useRef(null).

بعد أن يحوّل ReactDOM أحد مكوّنات React إلى HTMLElement، يبحث عن سمة باسم ref في ReactComponent. إذا كان ذلك متاحًا، يضع ReactDOM مرجع HTMLElement في ref.current.

تستخدم LitElement دالة علامة النموذج html من lit-html لإنشاء عنصر نموذج في الخلفية. تطبع LitElement محتوى النموذج في shadow DOM لعنصر مخصّص بعد العرض. ‫shadow DOM هي شجرة نموذج العناصر في المستند ذات نطاق محدد يتم تغليفها بواسطة shadow root. ينشئ الديكور @query بعد ذلك أداة إرجاع القيمة للسمة تنفّذ بشكل أساسي this.shadowRoot.querySelector على الجذر ذي النطاق المحدود.

طلب عناصر متعدّدة

في المثال أدناه، ستعرض أداة التزيين @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 دالة جلب لـ 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، يتم استخدام عمليات معاودة الاتصال لأنّ React نفسه هو من يدير الحالة. تبذل React قصارى جهدها لعدم الاعتماد على الحالة التي توفّرها العناصر. إنّ نموذج DOM هو ببساطة نتيجة لعملية العرض.

الحالة الخارجية

يمكن استخدام Redux أو MobX أو أي مكتبة أخرى لإدارة الحالة إلى جانب Lit.

يتم إنشاء مكوّنات Lit في نطاق المتصفّح. لذلك، تتوفّر أي مكتبة متوفّرة أيضًا في نطاق المتصفّح في Lit. تم إنشاء العديد من المكتبات الرائعة للاستفادة من أنظمة إدارة الحالة الحالية في Lit.

إليك سلسلة من Vaadin تشرح كيفية الاستفادة من Redux في أحد مكونات Lit.

يمكنك الاطّلاع على lit-mobx من Adobe لمعرفة كيف يمكن لموقع إلكتروني واسع النطاق الاستفادة من MobX في Lit.

يمكنك أيضًا الاطّلاع على Apollo Elements لمعرفة كيف يدرج المطوّرون GraphQL في مكوّنات الويب.

تعمل Lit مع ميزات المتصفّح الأصلية، ويمكن استخدام معظم حلول إدارة الحالة في نطاق المتصفّح في أحد مكونات Lit.

التصميم

Shadow DOM

لتغليف الأنماط وDOM بشكل أصلي ضمن عنصر مخصّص، تستخدم Lit Shadow DOM. تنشئ Shadow 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 هي عرض واجهة برمجة تطبيقات للأنماط من خلال خصائص CSS المخصّصة. على سبيل المثال، هذا نمط يستخدمه التصميم المتعدد الأبعاد:

.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، ما سيؤدي بعد ذلك إلى عرض نموذج المكوّنات في العنصر المخصّص نفسه بدلاً من جذر Shadow المرفق بالعنصر المخصّص. سيؤدي ذلك إلى فقدان: تغليف الأنماط وتغليف DOM والفتحات.

الإنتاج

IE 11

إذا كنت بحاجة إلى توفير التوافق مع المتصفّحات القديمة، مثل IE 11، عليك تحميل بعض عمليات التعبئة التي تبلغ حوالي 33 كيلوبايت أخرى. يمكنك الاطّلاع على مزيد من المعلومات هنا.

الحِزم الشرطية

ينصح فريق Lit بعرض حزمتَين مختلفتَين، إحداهما لمتصفّح IE 11 والأخرى للمتصفّحات الحديثة. ويوفّر ذلك عدة مزايا:

  • عرض ES 6 أسرع وسيتيح الوصول إلى معظم عملائك
  • يؤدي تحويل ES 5 إلى زيادة كبيرة في حجم الحزمة
  • تمنحك الحِزم الشرطية أفضل ما في كلا الخيارين
    • التوافق مع الإصدار 11 من Internet Explorer
    • لا يحدث تباطؤ في المتصفّحات الحديثة

يمكنك الاطّلاع على مزيد من المعلومات حول كيفية إنشاء حِزمة يتم عرضها بشكل مشروط على موقع المستندات هنا.