از Web Component تا Lit Element

۱. مقدمه

آخرین به‌روزرسانی: 2021-08-10

اجزای وب

کامپوننت‌های وب مجموعه‌ای از APIهای پلتفرم وب هستند که به شما امکان می‌دهند تگ‌های HTML سفارشی، قابل استفاده مجدد و کپسوله‌سازی شده جدیدی را برای استفاده در صفحات وب و برنامه‌های وب ایجاد کنید. کامپوننت‌ها و ویجت‌های سفارشی ساخته شده بر اساس استانداردهای کامپوننت وب در مرورگرهای مدرن کار می‌کنند و می‌توانند با هر کتابخانه یا چارچوب جاوا اسکریپتی که با HTML کار می‌کند، مورد استفاده قرار گیرند.

لیت چیست؟

Lit یک کتابخانه ساده برای ساخت کامپوننت‌های وب سریع و سبک است که در هر فریم‌ورکی یا بدون هیچ فریم‌ورکی کار می‌کنند. با Lit می‌توانید کامپوننت‌ها، برنامه‌ها، سیستم‌های طراحی و موارد دیگر را با قابلیت اشتراک‌گذاری بسازید.

Lit رابط‌های برنامه‌نویسی کاربردی (API) را برای ساده‌سازی وظایف رایج کامپوننت‌های وب مانند مدیریت ویژگی‌ها، صفات و رندرینگ ارائه می‌دهد.

آنچه یاد خواهید گرفت

  • کامپوننت وب چیست؟
  • مفاهیم اجزای وب
  • نحوه ساخت یک کامپوننت وب
  • lit-html و LiteElement چیستند؟
  • کاری که Lit روی یک کامپوننت وب انجام می‌دهد

آنچه خواهید ساخت

  • یک کامپوننت وب با قابلیت لایک/تشویق ساده
  • یک تایید/رد موافق برای کامپوننت وب مبتنی بر Lit

آنچه نیاز دارید

  • هر مرورگر مدرن به‌روز شده‌ای (کروم، سافاری، فایرفاکس، کرومیوم اج). کامپوننت‌های وب در همه مرورگرهای مدرن کار می‌کنند و پلی‌فیل‌ها برای مایکروسافت اینترنت اکسپلورر ۱۱ و مایکروسافت اج غیر کرومیوم در دسترس هستند.
  • آشنایی با HTML، CSS، جاوا اسکریپت و ابزارهای توسعه کروم

۲. آماده شدن و گشت و گذار در زمین بازی

دسترسی به کد

در سراسر آزمایشگاه کد، لینک‌هایی به زمین بازی Lit مانند این وجود خواهد داشت:

این محیط یک محیط کدنویسی است که به طور کامل در مرورگر شما اجرا می‌شود. این محیط می‌تواند فایل‌های TypeScript و JavaScript را کامپایل و اجرا کند و همچنین می‌تواند به طور خودکار importها را به ماژول‌های node حل کند.

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

// after
import './my-file.js';
import 'https://unpkg.com/lit?module';

شما می‌توانید کل آموزش را در زمین بازی Lit انجام دهید و از این نقاط کنترل به عنوان نقاط شروع استفاده کنید. اگر از VS Code استفاده می‌کنید، می‌توانید از این نقاط کنترل برای دانلود کد شروع هر مرحله و همچنین برای بررسی کار خود استفاده کنید.

بررسی رابط کاربری زمین بازی روشن

نوار تب انتخابگر فایل با عنوان بخش ۱، بخش ویرایش کد با عنوان بخش ۲، پیش‌نمایش خروجی با عنوان بخش ۳ و دکمه‌ی پیش‌نمایش بارگذاری مجدد با عنوان بخش ۴ نامگذاری شده است.

تصویر رابط کاربری زمین بازی Lit بخش‌هایی را که در این آزمایشگاه کد استفاده خواهید کرد، برجسته می‌کند.

  1. انتخابگر فایل. به دکمه بعلاوه توجه کنید...
  2. ویرایشگر فایل.
  3. پیش‌نمایش کد.
  4. دکمه بارگذاری مجدد.
  5. دکمه دانلود.

تنظیمات VS Code (پیشرفته)

در اینجا مزایای استفاده از این تنظیمات VS Code آورده شده است:

  • بررسی نوع الگو
  • هوش مصنوعی قالب و تکمیل خودکار

اگر NPM و VS Code (به همراه افزونه lit-plugin ) را از قبل نصب کرده‌اید و می‌دانید که چگونه از آن محیط استفاده کنید، می‌توانید به سادگی این پروژه‌ها را با انجام موارد زیر دانلود و شروع کنید:

  • دکمه دانلود را فشار دهید
  • محتویات فایل tar را در یک دایرکتوری استخراج کنید
  • یک سرور توسعه‌دهنده نصب کنید که بتواند مشخصات ماژول‌های خالی را حل کند (تیم Lit سرور @web/dev-server را توصیه می‌کند).
    • در اینجا یک مثال package.json آورده شده است.
  • سرور توسعه را اجرا کنید و مرورگر خود را باز کنید (اگر @web/dev-server استفاده می‌کنید، می‌توانید npx web-dev-server --node-resolve --watch --open استفاده کنید).
    • اگر از مثال package.json استفاده می‌کنید npm run serve استفاده کنید.

۳. تعریف یک عنصر سفارشی

عناصر سفارشی

کامپوننت‌های وب مجموعه‌ای از ۴ API وب بومی هستند. آن‌ها عبارتند از:

  • ماژول‌های ES
  • عناصر سفارشی
  • سایه DOM
  • قالب‌های HTML

شما قبلاً از مشخصات ماژول‌های ES استفاده کرده‌اید، که به شما امکان می‌دهد ماژول‌های جاوا اسکریپت با importها و exportهایی ایجاد کنید که با <script type="module"> در صفحه بارگذاری می‌شوند.

تعریف یک عنصر سفارشی

مشخصات عناصر سفارشی به کاربران اجازه می‌دهد عناصر HTML خود را با استفاده از جاوا اسکریپت تعریف کنند. نام‌ها باید شامل یک خط فاصله ( - ) باشند تا از عناصر مرورگر بومی متمایز شوند. فایل index.js را پاک کنید و یک کلاس عنصر سفارشی تعریف کنید:

ایندکس.js

class RatingElement extends HTMLElement {}

customElements.define('rating-element', RatingElement);

یک عنصر سفارشی با مرتبط کردن یک کلاس که HTMLElement با یک نام تگ خط فاصله دار بسط می‌دهد، تعریف می‌شود. فراخوانی customElements.define به مرورگر می‌گوید که کلاس RatingElement با tagName 'rating-element' مرتبط کند. این بدان معناست که هر عنصری در سند شما با نام <rating-element> با این کلاس مرتبط خواهد شد.

یک <rating-element> در بدنه سند قرار دهید و ببینید چه چیزی رندر می‌شود.

فهرست.html

<body>
 <rating-element></rating-element>
</body>

حالا، با نگاه به خروجی، خواهید دید که هیچ چیزی رندر نشده است. این انتظار می‌رفت، زیرا شما به مرورگر نگفته‌اید که چگونه <rating-element> را رندر کند. می‌توانید با انتخاب <rating-element> در انتخابگر عنصر Chrome Dev Tools و فراخوانی آن در کنسول، تأیید کنید که تعریف عنصر سفارشی با موفقیت انجام شده است:

$0.constructor

که باید خروجی آن این باشد:

class RatingElement extends HTMLElement {}

چرخه عمر عنصر سفارشی

عناصر سفارشی با مجموعه‌ای از قلاب‌های چرخه عمر ارائه می‌شوند. آن‌ها عبارتند از:

  • constructor
  • connectedCallback
  • disconnectedCallback
  • attributeChangedCallback
  • adoptedCallback

constructor زمانی فراخوانی می‌شود که عنصر برای اولین بار ایجاد می‌شود: برای مثال، با فراخوانی document.createElement('rating-element') یا new RatingElement() . سازنده (constructor) مکان خوبی برای تنظیم عنصر شماست، اما معمولاً انجام دستکاری‌های DOM در سازنده به دلایل عملکرد "boot-up" عنصر، یک عمل بد تلقی می‌شود.

متد connectedCallback زمانی فراخوانی می‌شود که عنصر سفارشی به DOM متصل می‌شود. این معمولاً جایی است که دستکاری‌های اولیه DOM اتفاق می‌افتد.

تابع disconnectedCallback پس از حذف عنصر سفارشی از DOM فراخوانی می‌شود.

زمانی که هر یک از ویژگی‌های مشخص شده توسط کاربر تغییر کند attributeChangedCallback(attrName, oldValue, newValue) فراخوانی می‌شود.

تابع adoptedCallback زمانی فراخوانی می‌شود که عنصر سفارشی از documentFragment دیگری از طریق adoptNode مانند HTMLTemplateElement به سند اصلی منتقل شود.

رندر DOM

حالا، به عنصر سفارشی برگردید و مقداری DOM به آن اختصاص دهید. محتوای عنصر را هنگام اتصال به DOM تنظیم کنید:

ایندکس.js

class RatingElement extends HTMLElement {
 constructor() {
   super();
   this.rating = 0;
 }
 connectedCallback() {
   this.innerHTML = `
     <style>
       rating-element {
         display: inline-flex;
         align-items: center;
       }
       rating-element button {
         background: transparent;
         border: none;
         cursor: pointer;
       }
     </style>
     <button class="thumb_down" >
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
     </button>
     <span class="rating">${this.rating}</span>
     <button class="thumb_up">
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
     </button>
   `;
 }
}

customElements.define('rating-element', RatingElement);

در constructor ، یک ویژگی نمونه به نام rating روی عنصر ذخیره می‌کنید. در connectedCallback ، فرزندان DOM را به <rating-element> اضافه می‌کنید تا امتیاز فعلی را به همراه دکمه‌های thumbs up و thumbs down نمایش دهند.

۴. سایه DOM

چرا Shadow DOM؟

در مرحله قبل، متوجه خواهید شد که انتخابگرهای موجود در تگ style که وارد کرده‌اید، هر عنصر رتبه‌بندی در صفحه و همچنین هر دکمه‌ای را انتخاب می‌کنند. این ممکن است منجر به نشت استایل‌ها از عنصر و انتخاب گره‌های دیگری شود که ممکن است قصد استایل‌دهی به آنها را نداشته باشید. علاوه بر این، استایل‌های دیگر خارج از این عنصر سفارشی ممکن است ناخواسته گره‌های داخل عنصر سفارشی شما را استایل‌دهی کنند. به عنوان مثال، سعی کنید یک تگ style را در بالای سند اصلی قرار دهید:

فهرست.html

<!DOCTYPE html>
<html>
 <head>
   <script src="./index.js" type="module"></script>
   <style>
     span {
       border: 1px solid red;
     }
   </style>
 </head>
 <body>
   <rating-element></rating-element>
 </body>
</html>

خروجی شما باید یک کادر حاشیه قرمز در اطراف محدوده برای رتبه‌بندی داشته باشد. این یک مورد جزئی است، اما عدم کپسوله‌سازی DOM ممکن است منجر به مشکلات بزرگتری برای برنامه‌های پیچیده‌تر شود. اینجاست که Shadow DOM وارد عمل می‌شود.

اتصال یک ریشه سایه

یک Shadow Root به عنصر اضافه کنید و DOM را درون آن ریشه رندر کنید:

ایندکس.js

class RatingElement extends HTMLElement {
 constructor() {
   super();
   this.rating = 0;
 }
 connectedCallback() {
   const shadowRoot = this.attachShadow({mode: 'open'});

   shadowRoot.innerHTML = `
     <style>
       :host {
         display: inline-flex;
         align-items: center;
       }
       button {
         background: transparent;
         border: none;
         cursor: pointer;
       }
     </style>
     <button class="thumb_down" >
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
     </button>
     <span class="rating">${this.rating}</span>
     <button class="thumb_up">
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
     </button>
   `;
 }
}

customElements.define('rating-element', RatingElement);

وقتی صفحه را رفرش می‌کنید، متوجه خواهید شد که استایل‌های موجود در سند اصلی دیگر نمی‌توانند گره‌های داخل Shadow Root را انتخاب کنند.

چطور این کار را انجام دادید؟ در connectedCallback شما this.attachShadow را فراخوانی کردید که یک ریشه سایه را به یک عنصر متصل می‌کند. حالت open به این معنی است که محتوای سایه قابل بررسی است و ریشه سایه را از طریق this.shadowRoot نیز قابل دسترسی می‌کند. به کامپوننت وب در inspector کروم نیز نگاهی بیندازید:

درخت دامنه در بازرس کروم. یک <rating-element> با a#shadow-root (open) به عنوان فرزند آن وجود دارد و DOM از قبل درون آن shadowroot قرار دارد.

اکنون باید یک ریشه سایه قابل گسترش ببینید که محتویات را در خود جای داده است. هر چیزی که درون آن ریشه سایه قرار دارد، Shadow DOM نامیده می‌شود. اگر عنصر امتیازدهی را در Chrome Dev Tools انتخاب کنید و $0.children را فراخوانی کنید، متوجه خواهید شد که هیچ فرزندی را برنمی‌گرداند. دلیل این امر این است که Shadow DOM به عنوان بخشی از همان درخت DOM به عنوان فرزندان مستقیم در نظر گرفته نمی‌شود، بلکه Shadow Tree است .

DOM سبک

یک آزمایش: یک گره را به عنوان فرزند مستقیم <rating-element> اضافه کنید:

فهرست.html

<rating-element>
 <div>
   This is the light DOM!
 </div>
</rating-element>

صفحه را رفرش کنید، خواهید دید که این گره DOM جدید در Light DOM این عنصر سفارشی در صفحه نمایش داده نمی‌شود. دلیل این امر این است که Shadow DOM دارای ویژگی‌هایی برای کنترل نحوه نمایش گره‌های Light DOM در shadow dom از طریق عناصر <slot> است.

۵. قالب‌های HTML

چرا قالب‌ها

استفاده از innerHTML و رشته‌های تحت‌اللفظی قالب بدون پاکسازی ممکن است باعث ایجاد مشکلات امنیتی در تزریق اسکریپت شود. روش‌های گذشته شامل استفاده از DocumentFragment s بوده‌اند، اما این روش‌ها با مشکلات دیگری مانند بارگذاری تصاویر و اجرای اسکریپت‌ها هنگام تعریف قالب‌ها و همچنین ایجاد موانعی برای قابلیت استفاده مجدد نیز همراه هستند. اینجاست که عنصر <template> وارد عمل می‌شود؛ قالب‌ها DOM بی‌اثر، روشی بسیار کارآمد برای کلون کردن گره‌ها و قالب‌بندی قابل استفاده مجدد را ارائه می‌دهند.

استفاده از قالب‌ها

در مرحله بعد، کامپوننت را به استفاده از قالب‌های HTML تغییر دهید:

فهرست.html

<body>
 <template id="rating-element-template">
   <style>
     :host {
       display: inline-flex;
       align-items: center;
     }
     button {
       background: transparent;
       border: none;
       cursor: pointer;
     }
   </style>
   <button class="thumb_down" >
     <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
   </button>
   <span class="rating"></span>
   <button class="thumb_up">
     <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
   </button>
 </template>

 <rating-element>
   <div>
     This is the light DOM!
   </div>
 </rating-element>
</body>

در اینجا محتوای DOM را به یک تگ قالب در DOM سند اصلی منتقل کردید. اکنون تعریف عنصر سفارشی را refactor کنید:

ایندکس.js

class RatingElement extends HTMLElement {
 constructor() {
   super();
   this.rating = 0;
 }
 connectedCallback() {
   const shadowRoot = this.attachShadow({mode: 'open'});
   const templateContent = document.getElementById('rating-element-template').content;
   const clonedContent = templateContent.cloneNode(true);
   shadowRoot.appendChild(clonedContent);

   this.shadowRoot.querySelector('.rating').innerText = this.rating;
 }
}

customElements.define('rating-element', RatingElement);

برای استفاده از این عنصر الگو، شما الگو را جستجو می‌کنید، محتویات آن را دریافت می‌کنید و آن گره‌ها را با templateContent.cloneNode کلون می‌کنید که در آن آرگومان true یک کلون عمیق انجام می‌دهد. سپس dom را با داده‌ها مقداردهی اولیه می‌کنید.

تبریک می‌گویم، شما اکنون یک کامپوننت وب دارید! متأسفانه هنوز هیچ کاری انجام نمی‌دهد، بنابراین در مرحله بعد، برخی قابلیت‌ها را اضافه کنید.

۶. افزودن قابلیت‌ها

اتصال املاک

در حال حاضر، تنها راه برای تنظیم امتیاز روی عنصر rating، ساخت عنصر، تنظیم ویژگی rating روی شیء و سپس قرار دادن آن در صفحه است. متأسفانه، عناصر HTML بومی معمولاً اینگونه کار نمی‌کنند. عناصر HTML بومی معمولاً با تغییرات ویژگی و ویژگی به‌روزرسانی می‌شوند.

با اضافه کردن خطوط زیر، کاری کنید که عنصر سفارشی، هنگام تغییر ویژگی rating نما را به‌روزرسانی کند:

ایندکس.js

constructor() {
  super();
  this._rating = 0;
}

set rating(value) {
  this._rating = value;
  if (!this.shadowRoot) {
    return;
  }

  const ratingEl = this.shadowRoot.querySelector('.rating');
  if (ratingEl) {
    ratingEl.innerText = this._rating;
  }
}

get rating() {
  return this._rating;
}

شما یک setter و getter برای ویژگی rating اضافه می‌کنید و سپس متن عنصر rating را در صورت موجود بودن به‌روزرسانی می‌کنید. این بدان معناست که اگر ویژگی rating را روی عنصر تنظیم کنید، view به‌روزرسانی می‌شود؛ آن را در کنسول Dev Tools خود به سرعت آزمایش کنید!

مقیدسازی‌های ویژگی

حالا، وقتی ویژگی تغییر می‌کند، view را به‌روزرسانی کنید؛ این مشابه به‌روزرسانی view یک input هنگام تنظیم <input value="newValue"> است. خوشبختانه، چرخه حیات کامپوننت وب شامل attributeChangedCallback است. با اضافه کردن خطوط زیر، rating را به‌روزرسانی کنید:

ایندکس.js

static get observedAttributes() {
 return ['rating'];
}

attributeChangedCallback(attributeName, oldValue, newValue) {
 if (attributeName === 'rating') {
   const newRating = Number(newValue);
   this.rating = newRating;
 }
}

برای اینکه attributeChangedCallback فعال شود، باید یک getter استاتیک برای RatingElement.observedAttributes which defines the attributes to be observed for changes . سپس rating را به صورت اعلانی در DOM تنظیم کنید. آن را امتحان کنید:

فهرست.html

<rating-element rating="5"></rating-element>

اکنون رتبه‌بندی باید به صورت اعلانی به‌روزرسانی شود!

عملکرد دکمه

حالا تنها چیزی که کم داریم، عملکرد دکمه است. رفتار این کامپوننت باید به کاربر اجازه دهد تا یک امتیاز مثبت یا منفی ارائه دهد و بازخورد بصری به کاربر ارائه دهد. می‌توانید این کار را با چند شنونده رویداد و یک ویژگی بازتابنده پیاده‌سازی کنید، اما ابتدا استایل‌ها را برای ارائه بازخورد بصری با افزودن خطوط زیر به‌روزرسانی کنید:

فهرست.html

<style>
...

 :host([vote=up]) .thumb_up {
   fill: green;
 }
  :host([vote=down]) .thumb_down {
   fill: red;
 }
</style>

در Shadow DOM، انتخابگر :host به گره یا عنصر سفارشی که Shadow Root به آن متصل است اشاره دارد. در این حالت، اگر ویژگی vote "up" باشد، دکمه thumb-up سبز می‌شود، اما اگر vote "down", then it will turn the thumb-down button red . حال، منطق این کار را با ایجاد یک ویژگی/ویژگی منعکس‌کننده برای vote مشابه نحوه پیاده‌سازی rating پیاده‌سازی کنید. با setter و getter ویژگی شروع کنید:

ایندکس.js

constructor() {
  super();
  this._rating = 0;
  this._vote = null;
}

set vote(newValue) {
  const oldValue = this._vote;
  if (newValue === oldValue) {
    return;
  }
  if (newValue === 'up') {
    if (oldValue === 'down') {
      this.rating += 2;
    } else {
      this.rating += 1;
    }
  } else if (newValue === 'down') {
    if (oldValue === 'up') {
      this.rating -= 2;
    } else {
      this.rating -= 1;
    }
  }
  this._vote = newValue;
  this.setAttribute('vote', newValue);
}

get vote() {
  return this._vote;
}

شما ویژگی نمونه _vote را در constructor با null مقداردهی اولیه می‌کنید و در تنظیم‌کننده بررسی می‌کنید که آیا مقدار جدید متفاوت است یا خیر. در این صورت، امتیاز را بر اساس آن تنظیم می‌کنید و مهم‌تر از همه، ویژگی vote را با this.setAttribute به میزبان منعکس می‌کنید.

در مرحله بعد، اتصال ویژگی را تنظیم کنید:

ایندکس.js

static get observedAttributes() {
  return ['rating', 'vote'];
}

attributeChangedCallback(attributeName, oldValue, newValue) {
  if (attributeName === 'rating') {
    const newRating = Number(newValue);

    this.rating = newRating;
  } else if (attributeName === 'vote') {
    this.vote = newValue;
  }
}

باز هم، این همان فرآیندی است که با اتصال ویژگی rating طی کردید؛ شما vote به observedAttributes اضافه می‌کنید و ویژگی vote را در attributeChangedCallback تنظیم می‌کنید. و حالا در نهایت، چند شنونده رویداد کلیک اضافه می‌کنید تا به دکمه‌ها قابلیت بدهید!

ایندکس.js

constructor() {
 super();
 this._rating = 0;
 this._vote = null;
 this._boundOnUpClick = this._onUpClick.bind(this);
 this._boundOnDownClick = this._onDownClick.bind(this);
}

connectedCallback() {
  ...
  this.shadowRoot.querySelector('.thumb_up')
    .addEventListener('click', this._boundOnUpClick);
  this.shadowRoot.querySelector('.thumb_down')
    .addEventListener('click', this._boundOnDownClick);
}

disconnectedCallback() {
  this.shadowRoot.querySelector('.thumb_up')
    .removeEventListener('click', this._boundOnUpClick);
  this.shadowRoot.querySelector('.thumb_down')
    .removeEventListener('click', this._boundOnDownClick);
}

_onUpClick() {
  this.vote = 'up';
}

_onDownClick() {
  this.vote = 'down';
}

در constructor شما تعدادی شنونده‌ی کلیک را به عنصر متصل می‌کنید و ارجاعات را نگه می‌دارید. در connectedCallback به رویدادهای کلیک روی دکمه‌ها گوش می‌دهید. در disconnectedCallback این شنونده‌ها را پاک‌سازی می‌کنید و در خود شنونده‌های کلیک، به طور مناسب vote تنظیم می‌کنید.

تبریک می‌گویم، شما اکنون یک کامپوننت وب با امکانات کامل دارید؛ سعی کنید روی چند دکمه کلیک کنید! مشکل الان این است که فایل JS من اکنون به ۹۶ خط و فایل HTML من به ۴۳ خط رسیده است و کد برای چنین کامپوننت ساده‌ای کاملاً طولانی و ضروری است. اینجاست که پروژه Lit گوگل وارد عمل می‌شود!

۷. لیت-اچ‌تی‌ام‌ال

ایست بازرسی کد

چرا lit-html

اول و مهمتر از همه، تگ <template> مفید و کارآمد است، اما با منطق کامپوننت بسته‌بندی نشده است، بنابراین توزیع قالب با بقیه منطق را دشوار می‌کند. علاوه بر این، نحوه استفاده از عناصر قالب ذاتاً به کد دستوری منجر می‌شود که در بسیاری از موارد، در مقایسه با الگوهای کدنویسی اعلانی، منجر به کدی با خوانایی کمتر می‌شود.

اینجاست که lit-html وارد می‌شود! Lit html سیستم رندر Lit است که به شما امکان می‌دهد قالب‌های HTML را در جاوا اسکریپت بنویسید، سپس آن قالب‌ها را به همراه داده‌ها برای ایجاد و به‌روزرسانی DOM به طور مؤثر رندر و دوباره رندر کنید. این شبیه به کتابخانه‌های محبوب JSX و VDOM است اما به صورت بومی در مرورگر اجرا می‌شود و در بسیاری از موارد بسیار کارآمدتر است.

استفاده از HTML لیت

در مرحله بعد، rating-element کامپوننت وب بومی را به قالب Lit منتقل کنید که از Tagged Template Literals استفاده می‌کند. Tagged Template Literals توابعی هستند که رشته‌های قالب را به عنوان آرگومان با سینتکس خاص دریافت می‌کنند. سپس Lit از عناصر قالب در زیر کاپوت استفاده می‌کند تا رندر سریع و همچنین برخی از ویژگی‌های پاکسازی برای امنیت را فراهم کند. با انتقال <template> در index.html به یک قالب Lit با اضافه کردن یک متد render() به کامپوننت وب شروع کنید:

ایندکس.js

// Dont forget to import from Lit!
import {render, html} from 'lit';

class RatingElement extends HTMLElement {
  ...
  render() {
    if (!this.shadowRoot) {
      return;
    }

    const template = html`
      <style>
        :host {
          display: inline-flex;
          align-items: center;
        }
        button {
          background: transparent;
          border: none;
          cursor: pointer;
        }

       :host([vote=up]) .thumb_up {
         fill: green;
       }

       :host([vote=down]) .thumb_down {
         fill: red;
       }
      </style>
      <button class="thumb_down">
        <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
      </button>
      <span class="rating">${this.rating}</span>
      <button class="thumb_up">
        <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
      </button>`;

    render(template, this.shadowRoot);
  }
}

همچنین می‌توانید قالب خود را از index.html حذف کنید. در این متد رندر، متغیری به نام template تعریف می‌کنید و تابع html template literal با برچسب html را فراخوانی می‌کنید. همچنین متوجه خواهید شد که با استفاده از سینتکس درون‌یابی template literal به صورت ${...} یک اتصال داده ساده درون عنصر span.rating انجام داده‌اید. این بدان معناست که در نهایت دیگر نیازی به به‌روزرسانی اجباری آن گره نخواهید داشت. علاوه بر این، متد lit render را فراخوانی می‌کنید که قالب را به صورت همزمان در ریشه shadow رندر می‌کند.

مهاجرت به سینتکس اعلانی

حالا که از شر عنصر <template> خلاص شده‌اید، کد را طوری تغییر دهید که به جای آن، متد render تازه تعریف شده را فراخوانی کند. می‌توانید با استفاده از اتصال شنونده رویداد lit برای پاک کردن کد شنونده شروع کنید:

ایندکس.js

<button
    class="thumb_down"
    @click=${() => {this.vote = 'down'}}>
...
<button
    class="thumb_up"
    @click=${() => {this.vote = 'up'}}>

قالب‌های Lit می‌توانند با سینتکس اتصال @EVENT_NAME یک شنونده رویداد به گره‌ای اضافه کنند که در این حالت، هر بار که روی این دکمه‌ها کلیک می‌شود، ویژگی vote به‌روزرسانی می‌شود.

در مرحله بعد، کد مقداردهی اولیه شنونده رویداد را در constructor و connectedCallback و disconnectedCallback پاک کنید:

ایندکس.js

constructor() {
  super();
  this._rating = 0;
  this._vote = null;
}

connectedCallback() {
  this.attachShadow({mode: 'open'});
  this.render();
}

// remove disonnectedCallback and _onUpClick and _onDownClick

شما توانستید منطق شنونده کلیک را از هر سه callback حذف کنید و حتی disconnectedCallback را به طور کامل حذف کنید! همچنین توانستید تمام کد مقداردهی اولیه DOM را از connectedCallback حذف کنید و آن را بسیار زیباتر جلوه دهید. این همچنین بدان معنی است که می‌توانید از شر متدهای شنونده _onUpClick و _onDownClick خلاص شوید!

در نهایت، تنظیم‌کننده‌های ویژگی را به‌روزرسانی کنید تا از متد render استفاده کنند تا dom بتواند هنگام تغییر ویژگی‌ها یا صفات به‌روزرسانی شود:

ایندکس.js

set rating(value) {
  this._rating = value;
  this.render();
}

...

set vote(newValue) {
  const oldValue = this._vote;
  if (newValue === oldValue) {
    return;
  }

  if (newValue === 'up') {
    if (oldValue === 'down') {
      this.rating += 2;
    } else {
      this.rating += 1;
    }
  } else if (newValue === 'down') {
    if (oldValue === 'up') {
      this.rating -= 2;
    } else {
      this.rating -= 1;
    }
  }

  this._vote = newValue;
  this.setAttribute('vote', newValue);
  // add render method
  this.render();
}

در اینجا، شما توانستید منطق به‌روزرسانی دامنه را از تنظیم‌کننده rating حذف کنید و یک فراخوانی برای render از تنظیم‌کننده vote اضافه کردید. اکنون قالب بسیار خواناتر است زیرا اکنون می‌توانید ببینید که اتصال‌ها و شنونده‌های رویداد کجا اعمال می‌شوند.

صفحه را رفرش کنید، و باید یک دکمه امتیازدهی فعال داشته باشید که وقتی روی گزینه رأی مثبت کلیک می‌کنید، باید به این شکل باشد!

اسلایدر امتیازدهی با انگشت شست بالا و پایین با مقدار ۶ و رنگ سبز برای انگشت شست بالا

۸. لایت المنت

چرا لایت المنت؟

هنوز برخی مشکلات در کد وجود دارد. اول اینکه، اگر ویژگی یا خاصیت vote را تغییر دهید، ممکن است ویژگی rating نیز تغییر کند که منجر به فراخوانی دو بار render می‌شود. با وجود اینکه فراخوانی‌های مکرر render اساساً بدون عملیات و کارآمد هستند، ماشین مجازی جاوا اسکریپت هنوز هم دو بار وقت خود را صرف فراخوانی همزمان آن تابع می‌کند. دوم اینکه، اضافه کردن ویژگی‌ها و خاصیت‌های جدید خسته‌کننده است زیرا به کد تکراری زیادی نیاز دارد. اینجاست که LitElement وارد عمل می‌شود!

LitElement کلاس پایه Lit برای ایجاد کامپوننت‌های وب سریع و سبک است که می‌توانند در فریم‌ورک‌ها و محیط‌ها مورد استفاده قرار گیرند. در مرحله بعد، نگاهی به کاری که LitElement می‌تواند با تغییر پیاده‌سازی برای استفاده از آن در rating-element برای ما انجام دهد، می‌اندازیم!

استفاده از LiteElement

با وارد کردن و زیرکلاس‌سازی کلاس پایه LitElement از پکیج lit شروع کنید:

ایندکس.js

import {LitElement, html, css} from 'lit';

class RatingElement extends LitElement {
// remove connectedCallback()
...

شما LitElement که کلاس پایه جدید برای rating-element است، وارد می‌کنید. در مرحله بعد، html import خود را نگه می‌دارید و در نهایت css که به ما امکان می‌دهد قالب‌های با برچسب css را برای محاسبات ریاضی css، قالب‌بندی و سایر ویژگی‌ها در زیر کاپوت تعریف کنیم.

سپس، استایل‌ها را از متد رندر به استایل‌شیت استاتیک Lit منتقل کنید:

ایندکس.js

class RatingElement extends LitElement {
  static get styles() {
    return css`
      :host {
        display: inline-flex;
        align-items: center;
      }
      button {
        background: transparent;
        border: none;
        cursor: pointer;
      }

      :host([vote=up]) .thumb_up {
        fill: green;
      }

      :host([vote=down]) .thumb_down {
        fill: red;
      }
    `;
  }
 ...

اینجا جایی است که بیشتر استایل‌ها در Lit قرار دارند. Lit این استایل‌ها را می‌گیرد و از ویژگی‌های مرورگر مانند Constructable Stylesheets برای ارائه زمان رندر سریع‌تر استفاده می‌کند و همچنین در صورت لزوم، آن را از طریق Web Components polyfill در مرورگرهای قدیمی‌تر عبور می‌دهد.

چرخه حیات

Lit مجموعه‌ای از متدهای فراخوانی چرخه عمر رندر را علاوه بر فراخوانی‌های کامپوننت وب بومی معرفی می‌کند. این فراخوانی‌ها زمانی فعال می‌شوند که ویژگی‌های Lit که تعریف شده‌اند تغییر کنند.

برای استفاده از این ویژگی، باید به صورت ایستا اعلام کنید که کدام ویژگی‌ها چرخه حیات رندر را فعال می‌کنند.

ایندکس.js

static get properties() {
  return {
    rating: {
      type: Number,
    },
    vote: {
      type: String,
      reflect: true,
    }
  };
}

// remove observedAttributes() and attributeChangedCallback()
// remove set rating() get rating()

در اینجا، شما تعریف می‌کنید که rating و vote چرخه حیات رندر LitElement را فعال می‌کنند و همچنین انواعی را تعریف می‌کنید که برای تبدیل ویژگی‌های رشته‌ای به ویژگی‌ها استفاده می‌شوند.

<user-profile .name=${this.user.name} .age=${this.user.age}>
  ${this.user.family.map(member => html`
        <family-member
             .name=${member.name}
             .relation=${member.relation}>
        </family-member>`)}
</user-profile>

علاوه بر این، پرچم reflect در ویژگی vote به طور خودکار ویژگی vote عنصر میزبان را که به صورت دستی در تنظیم‌کننده‌ی vote فعال کرده‌اید، به‌روزرسانی می‌کند.

حالا که بلوک ویژگی‌های استاتیک را دارید، می‌توانید تمام منطق به‌روزرسانی رندر ویژگی و ویژگی را حذف کنید. این یعنی می‌توانید متدهای زیر را حذف کنید:

  • connectedCallback
  • observedAttributes
  • attributeChangedCallback
  • rating (تنظیم‌کننده‌ها و دریافت‌کننده‌ها)
  • vote (تنظیم‌کننده‌ها و دریافت‌کننده‌ها اما منطق تغییر را از تنظیم‌کننده نگه دارید)

چیزی که نگه می‌دارید constructor و همچنین اضافه کردن یک متد جدید willUpdate برای چرخه عمر است:

ایندکس.js

constructor() {
  super();
  this.rating = 0;
  this.vote = null;
}

willUpdate(changedProps) {
  if (changedProps.has('vote')) {
    const newValue = this.vote;
    const oldValue = changedProps.get('vote');

    if (newValue === 'up') {
      if (oldValue === 'down') {
        this.rating += 2;
      } else {
        this.rating += 1;
      }
    } else if (newValue === 'down') {
      if (oldValue === 'up') {
        this.rating -= 2;
      } else {
        this.rating -= 1;
      }
    }
  }
}

// remove set vote() and get vote()

در اینجا، شما به سادگی rating و vote مقداردهی اولیه می‌کنید و منطق تنظیم‌کننده‌ی vote را به متد willUpdate منتقل می‌کنید. متد willUpdate قبل از render هر زمان که هر ویژگی به‌روزرسانی تغییر کند، فراخوانی می‌شود، زیرا LitElement تغییرات ویژگی را دسته‌ای می‌کند و رندر را ناهمزمان می‌کند. تغییرات در ویژگی‌های واکنشی (مانند this.rating ) در willUpdate باعث فراخوانی‌های غیرضروری چرخه‌ی render نمی‌شود.

در نهایت، render یک متد چرخه عمر LitElement است که ما را ملزم به بازگرداندن یک الگوی Lit می‌کند:

ایندکس.js

render() {
  return html`
    <button
        class="thumb_down"
        @click=${() => {this.vote = 'down'}}>
      <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
    </button>
    <span class="rating">${this.rating}</span>
    <button
        class="thumb_up"
        @click=${() => {this.vote = 'up'}}>
      <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
    </button>`;
}

دیگر لازم نیست ریشه سایه را بررسی کنید، و دیگر لازم نیست تابع render که قبلاً از بسته 'lit' وارد شده بود را فراخوانی کنید.

عنصر شما باید اکنون در پیش‌نمایش رندر شود؛ روی آن کلیک کنید!

۹. تبریک

تبریک می‌گویم، شما با موفقیت یک کامپوننت وب را از ابتدا ساختید و آن را به یک LiteElement تبدیل کردید!

Lit فوق‌العاده کوچک (کمتر از ۵ کیلوبایت فشرده شده + gzip شده)، فوق‌العاده سریع و کدنویسی با آن واقعاً سرگرم‌کننده است! می‌توانید کامپوننت‌هایی بسازید که توسط فریم‌ورک‌های دیگر استفاده شوند، یا می‌توانید با آن برنامه‌های کامل بسازید!

حالا می‌دانید که یک کامپوننت وب چیست، چگونه می‌توان آن را ساخت، و چگونه Lit ساخت آنها را آسان‌تر می‌کند!

ایست بازرسی کد

آیا می‌خواهید کد نهایی خود را با کد ما مقایسه کنید؟ آن را اینجا مقایسه کنید.

بعدش چی؟

به برخی از آزمایشگاه‌های کد دیگر نگاهی بیندازید!

مطالعه بیشتر

جامعه