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

1. مقدمة

ما هو Lit؟

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

المُعطيات

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

  • لغة JSX والنماذج
  • المكوّنات والعناصر
  • الحالة ودورة الحياة
  • Hooks
  • أطفال
  • المراجع
  • حالة التوسط

ما الذي ستقوم ببنائه

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

المتطلبات

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

2. Lit مقابل React

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

إنه صغير

أداة Lit صغيرة جدًا: إذ يتم تصغيرها إلى حوالي 5 كيلوبايت وضغطها بتنسيق gzip مقارنةً بـ React + ReactDOM الذي يزيد عن 40 كيلوبايت.

رسم بياني شريطي لحجم الحِزمة المُصغّر والمضغوط بالكيلو بايت حجم شريط الإضاءة هو 5 كيلوبايت وحجم React + React DOM هو 42.2 كيلوبايت.

سريعة

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

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

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

لا يحتاج إلى إصدار

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

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

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

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

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

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

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

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

دعم TypeScript من الدرجة الأولى

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

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

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

لقطة شاشة لواجهة تطوير برامج برمجية تعرِض اقتراحات IntelliSense

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

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

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

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

تم تصميمه مع الأخذ في الاعتبار العرض من جهة الخادم (SSR).

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

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

تكلفة الاشتراك منخفضة

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

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

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

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

هناك طريقتان لتنفيذ هذا الدليل التعليمي حول الرموز البرمجية:

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

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

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

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

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

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

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

استكشاف واجهة مستخدم ساحة الألعاب المُضاءة

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

تُبرز لقطة شاشة واجهة المستخدم في "المنصة" في "المنصة" الأقسام التي ستستخدمها في هذا الدرس التطبيقي حول الترميز.

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

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

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

  • التحقّق من نوع النموذج
  • ميزة "اقتراحات الذكاء الاصطناعي" للنماذج والإكمال التلقائي

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

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

البنية الأساسية

في React، يمكنك عرض عالم JSX hello على النحو التالي:

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 لتجميع عناصر متعددة في قوالبها.

في 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
);

يمكن أن تقبل TemplateResults المُضاءة المصفوفات والسلاسل وTemplateResults الأخرى، بالإضافة إلى التوجيهات.

كتدريب، جرِّب تحويل رمز React البرمجي التالي إلى Lit:

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

ReactDOM.render(
  element,
  mountNode
);

الإجابة:

import {html, render} from 'lit';

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

render(
  element,
  mountNode
);

تمرير عناصر العرض وضبطها

من أكبر الاختلافات بين بنية JSX وبنية Lit هي بنية ربط البيانات. على سبيل المثال، خذ إدخال React هذا مع عمليات الربط:

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

ReactDOM.render(
  element,
  mountNode
);

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

  • يتم الضبط على متغيّر محدَّد (خطأ في هذه الحالة)
  • تُعين الفئة على 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، لن يؤدي ذلك إلى ضبط عنصر الإدخال ليكون للقراءة فقط لأنّه يتّبع التنفيذ الأصلي وسلوك الإدخال.

بنية ربط سمة Lit

html`<my-element ?attribute-name=${booleanVar}>`;
  • البادئة ? هي بنية الربط لتبديل سمة على عنصر.
  • ما يعادل inputRef.toggleAttribute('attribute-name', booleanVar)
  • مفيد للعناصر التي تستخدم disabled كـ disabled="false" لا تزال تقرأها دالة 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)
  • يصلح للقيم الأساسية وأدوات اختيار قواعد الأنماط وأدوات اختيار الاستعلام

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

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

ReactDOM.render(
  element,
  mountNode
);

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

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

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

import {html, render} from 'lit';

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

render(
  element,
  mountNode
);

في مثال Lit، هناك أداة معالجة تمّت إضافتها إلى الحدث click باستخدام @click.

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

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

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

5- المكوّنات والعناصر

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

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

مكافئ Lit لمكوّن فئة React هو LitElement، ومفهوم 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" ولكنه يتم تشغيله محليًا باستخدام JavaScript.
render() {
  return html`<h1>Hello, ${this.name}</h1>`
}
  • يتم استدعاء هذه العملية عند تغيير أي خاصية تفاعلية.
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
  ...
}
  • يؤدي هذا إلى ربط اسم علامة عنصر HTML بتعريف الفئة
  • وفقًا لمعيار العناصر المخصصة، يجب أن يحتوي اسم العلامة على واصلة (-)
  • تشير "this" في LitElement إلى مثيل العنصر المخصص (<welcome-banner> في هذه الحالة)
customElements.define('welcome-banner', WelcomeBanner);
  • هذا هو العنصر المكافئ في JavaScript لعنصر الزخرفة @customElement في TypeScript.
<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- الحالة ومراحل النشاط

ستتعرف في هذا القسم على حالة ليت ودورة حياتها.

الحالة

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

connectedCallback

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

// Lit
connectedCallback() {
  super.connectedCallback();
  this.window.addEventListener('resize', this.boundOnResize);
}
  • يتمّ استدعاؤه عند إدراج العنصر المخصّص في شجرة DOM.
  • على عكس مكونات React، عند فصل العناصر المخصصة عن DOM، لا يتم إتلافها وبالتالي يمكن "الاتصال" بها عدة مرات
    • لن يتم الاتصال بـ "firstUpdated" مرة أخرى
  • يكون مفيدًا لإعادة بدء DOM أو إعادة إرفاق أدوات معالجة الأحداث التي تمّت إزالتها عند انقطاع الاتصال.
  • ملاحظة: قد يتم استدعاء 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 (باستخدام مصطلح "تحديث") باللغة الإنجليزية.
  • على عكس React، يتمّ أيضًا استدعاء updated عند العرض الأوّلي.
  • مشابهة في الوظيفة لدالة componentDidUpdate في React

componentWillUnmount

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

// Lit
disconnectedCallback() {
  super.disconnectedCallback();
  this.window.removeEventListener('resize', this.boundOnResize);
}
  • بيانات Lit المكافئة تشبه 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')
);

في المثال أعلاه، هناك ساعة بسيطة تؤدي ما يلي:

  • يتم عرض "مرحبًا بالجميع! "الوقت الآن" ثم يعرض الوقت
  • سيتم تحديث الساعة كل ثانية
  • عند إلغاء تثبيته، يتم محو الفاصل الزمني الذي يستدعي علامة التجزئة

ابدأ أولاً بتعريف فئة المكوِّن:

// 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. مرة أخرى، يجمع مقياس 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- Hooks

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

مفاهيم أدوات React الإضافية

توفّر أدوات React Hooks طريقة لمكونات الدوالّ "للتعلّق" بالحالة. وهناك العديد من الفوائد وراء ذلك.

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

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

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

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

أدوات التحكّم التفاعلية

تتوفّر المفاهيم الأساسية وراء عناصر الخطاطيف في 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')
);

في المثال أعلاه، هناك ساعة بسيطة تؤدي ما يلي:

  • يعرض "مرحبًا بالعالم! "الوقت الآن" ثم يعرض الوقت
  • سيتم تحديث الساعة كل ثانية
  • عند إزالة الجهاز، يتم محو الفاصل الزمني الذي يُستخدَم للإشارة إلى العلامة

بناء سقّالة المكونات

ابدأ أولاً بتعريف فئة المكوِّن وأضِف الدالة 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.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. المراجع

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

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

مراجع React

.

يتم تحويل مكوِّن React إلى سلسلة من استدعاءات الدوال التي تنشئ DOM افتراضي عند استدعائه. تفسر ReactDOM نموذج DOM الافتراضي هذا ويعرض عناصر 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 في Lit هو HTMLElement الذي يعرضه الزخرفيان @query و@queryAll.

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

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

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

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

  • تحدِّد خاصيّة في MyElement باستخدام زخرفة @query (إنشاء دالة للحصول على 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.
  • لعرض زر يتضمن "خطوة +" أو "-خطوة" كتصنيف له
  • تعديل المكوّن الرئيسي من خلال استدعاء 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.
  • يتم استخدام addToCounter من قِبل "CounterButton" لتعديل "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.

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 المميّز بعلامة. بالإضافة إلى ذلك، تتم إزالة تكرار متصفِّح CSSResult بشكل أسرع بكثير.

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

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

JSX والنماذج

Lit وVirtual 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 من هذه الميزات بدون الحاجة إلى الكثير من عمليات المقارنة أثناء التشغيل.

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

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

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

Lit وJSX + React DOM

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

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

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

إلى ما يلي:

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

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

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

الفعاليات

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

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

المكوّنات والعناصر

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

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

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

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

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

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

اعتمدت Lit على العناصر المخصّصة بدلاً من نظام العناصر المخصّصة لأنّ العناصر المخصّصة مدمجة في المتصفّح، ويعتقد فريق Lit أنّ مزايا استخدام إطار عمل متعدّد تفوق المزايا التي يوفّرها نظام العناصر المجمّعة. في الواقع، تغلّبت جهود فريق Lit في مساحة lit-ssr على المشاكل الرئيسية المتعلّقة بتسجيل JavaScript. بالإضافة إلى ذلك، تستفيد بعض الشركات، مثل GitHub، من ميزة التسجيل غير الفوري للعناصر المخصّصة لتحسين الصفحات تدريجيًا باستخدام ميزات اختيارية.

تمرير البيانات إلى العناصر المخصّصة

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

على سبيل المثال، في ما يلي تعريف LitElement التالي:

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

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

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

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

يتمّ تحديد خاصيّة تفاعلية بدائية num ستحوّل قيمة سلسلة السمة إلى number، ثمّ يتمّ تقديم بنية بيانات معقّدة باستخدام attribute:false التي توقِف معالجة سمة Lit.

في ما يلي كيفية تمرير البيانات إلى هذا العنصر المخصّص:

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

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

الحالة ودورة الحياة

طلبات الاستدعاء الأخرى في مراحل نشاط React

static getDerivedStateFromProps

ليس هناك مكافئ في "الإضاءة" حيث إن كل من الأدوات والحالة هما نفس خصائص الفئة

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

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

خصائص وطُرق دورة الحياة الأخرى

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

updateComplete

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

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

getUpdateComplete

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

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

performUpdate

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

hasUpdated

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

isConnected

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

العرض المرئي لمراحل نشاط تعديل Lit

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

  • التحديث المُسبَق
  • تعديل
  • بعد التحديث

التحديث المُسبَق

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

بعد requestUpdate، يتم انتظار تحديث مجدول.

تعديل

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

تعديل المحتوى بعد نشره

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

Hooks

أهمية العبارات الافتتاحية

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

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

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

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

في ما يلي تنفيذ وحدة التحكّم التفاعلية + وحدات التحكّم بالإضاءة.

الخلاصات:

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

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

أطفال

الشريحة التلقائية

عند عدم توفير السمة slot لعناصر HTML، يتم تعيينها إلى الخانة التلقائية بدون اسم. في المثال أدناه، سيضع MyApp فقرة واحدة في خانة مُسمّاة. سيتم ضبط الفقرة الأخرى تلقائيًا على الخانة غير المُسمّاة".

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

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

تحديثات الخانة

عندما تتغير بنية العناصر التابعة للخانة، يتم تنشيط حدث slotchange. يمكن لمكوّن Lit ربط مستمع أحداث بحدث slotchange. في المثال أدناه، سيتم تسجيل assignedNodes في وحدة التحكّم على slotchange للفتحة الأولى التي تم العثور عليها في shadowRoot.

@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 وليس عناصر HTMLElement. لأنه يتم الإعلان عن المرجع قبل عرض HTMLElement، يتم تخصيص مسافة في الذاكرة. هذا هو السبب في أنّك ترى null كقيمة أولية للمرجع، لأنّ عنصر DOM الفعلي لم يتم إنشاؤه (أو عرضه) بعد، أي useRef(null).

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

يستخدم LitElement دالة علامة النموذج html من lit-html لإنشاء عنصر نموذج في المقدمة. يختم LitElement محتوى النموذج على shadow DOM للعنصر المخصّص بعد العرض. shadow DOM هو شجرة DOM ذات نطاق مُحاطة بجذر ظل. بعد ذلك، ينشئ أسلوب الزخرفة @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 (عناصر Apollo) لمعرفة كيف يضمِّن المطوّرون GraphQL في مكوّناتهم على الويب.

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

التصميم

نموذج DOM للظل

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

يقدّم Shadow DOM أيضًا مفاهيم وأدوات اختيار جديدة لمواصفات CSS:

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

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

أنماط المشاركة

تسهِّل Lit مشاركة الأنماط بين المكوّنات في شكل CSSTemplateResults من خلال علامات نموذج css. على سبيل المثال:

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

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

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

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

المظهر

تشكّل جذور الظلّ تحديًا بسيطًا لأسلوب وضع السمات التقليدية الذي يعتمد عادةً على علامات الأنماط من الأعلى إلى الأسفل. الطريقة التقليدية لمعالجة السمات باستخدام مكونات الويب التي تستخدم Shadow DOM هي عرض واجهة برمجة تطبيقات للنمط عبر خصائص CSS المخصصة. على سبيل المثال، هذا هو النمط الذي يستخدمه Material Design:

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

بعد ذلك، سيغيّر المستخدم مظهر الموقع الإلكتروني من خلال تطبيق قيم السمات المخصّصة:

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

إذا كان من الضروري استخدام المظاهر من أعلى إلى أسفل ولا يمكنك عرض الأنماط، من الممكن دائمًا إيقاف Shadow DOM من خلال إلغاء createRenderRoot لعرض this، ما سيؤدي بعد ذلك إلى عرض نموذج المكوّنات إلى العنصر المخصّص نفسه بدلاً من جذر الظل المرتبط بالعنصر المخصّص. في هذه الحالة، ستفقد: تغليف الأنماط، وتغليف DOM، والخانات.

الإنتاج

الإصدار 11 من IE

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

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

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

  • إنّ عرض ES 6 أسرع وسيناسب معظم عملائك.
  • تم قراءة الفيديو ES 5 بهدف زيادة حجم الحِزمة بشكل كبير.
  • تمنحك الحزم الشرطية أفضل ما في الأمرين
    • دعم IE 11
    • لا بطء في المتصفحات الحديثة

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