تصميم واجهات ويب تفاعلية بـ JavaScript باستخدام Web Components دون أطر عمل
مقدمة
أصبحت الواجهات التفاعلية جزءاً أساسياً من تطبيقات الويب الحديثة، إلا أن بناءها لا يقتضي دائماً الاعتماد على أطر عمل ضخمة أو مكتبات خارجية. يتيح تصميم واجهات ويب تفاعلية بـ JavaScript باستخدام Web Components دون أطر عمل الاستفادة من قدرات المتصفح الأصلية لإنشاء مكوّنات مستقلة، قابلة لإعادة الاستخدام، وسهلة الدمج في أي مشروع.
تمثل Web Components مجموعة من معايير الويب التي تساعد المطور على بناء عناصر واجهة مخصصة تعمل كما تعمل عناصر HTML القياسية. ويمكن مثلاً إنشاء عنصر باسم <task-card> أو <user-menu> واستخدامه في أي صفحة دون الحاجة إلى React أو Vue أو Angular. تعتمد هذه المنظومة بصورة رئيسية على العناصر المخصصة، وشجرة DOM الظلية، والقوالب، والأحداث المخصصة.
لا يعني ذلك أن أطر العمل عديمة الفائدة؛ فهي تقدم حلولاً متكاملة لإدارة الحالة والتوجيه وبناء المشاريع الكبيرة. لكن Web Components خيار عملي عندما تكون الحاجة إلى مكونات مستقلة، أو عند العمل داخل نظام متعدد التقنيات، أو عند تطوير مكتبة واجهات يمكن استهلاكها من مشاريع مختلفة.
لماذا تختار Web Components بدلاً من إطار عمل؟
الميزة الأولى لـ Web Components هي أنها معيار أصيل في المتصفح، وليست واجهة برمجية تملكها شركة أو مشروع واحد. لذلك يمكن للمكوّن الذي تبنيه اليوم أن يعمل في صفحة HTML تقليدية، أو ضمن تطبيق React، أو داخل مشروع Vue، أو حتى في نظام إدارة محتوى، ما دام المتصفح حديثاً.
تساعد هذه الطبيعة القياسية على تقليل الارتباط التقني بإطار عمل محدد. فعندما تتغير متطلبات المشروع أو تتبدل التقنية المستخدمة في فريق آخر، يبقى المكوّن قابلاً للنقل لأن واجهته الخارجية تعتمد على HTML وخصائص العناصر والأحداث. وهذا مهم خصوصاً في المؤسسات التي تعمل فيها فرق متعددة باستخدام تقنيات واجهات مختلفة.
كذلك تمنحك هذه المقاربة تحكماً مباشراً في DOM ودورة حياة العنصر، من دون طبقة تجريد إضافية. وفي المشاريع الصغيرة والمتوسطة، قد يؤدي ذلك إلى تقليل حجم الحزمة البرمجية وتسريع التحميل الأولي. ومع ذلك، ينبغي اتخاذ القرار بواقعية: إذا كان التطبيق يتضمن حالة عالمية معقدة، أو صفحات كثيرة، أو احتياجات توجيه متقدمة، فقد يكون إطار العمل أكثر إنتاجية.
من المهم أيضاً عدم التعامل مع Web Components كحزمة واحدة يجب استعمال كل أجزائها. يمكن البدء بالعناصر المخصصة فقط، ثم إضافة Shadow DOM حين تحتاج إلى عزل الأنماط أو البنية الداخلية. هذا التدرج يجعل تبني التقنية عملياً وآمناً داخل المشاريع القائمة.
الأسس التقنية: العناصر المخصصة وShadow DOM والقوالب
العنصر المخصص هو صنف JavaScript يرث عادة من HTMLElement، ثم يسجل في المتصفح عبر customElements.define(). يجب أن يحتوي اسم العنصر على شرطة، مثل product-card، لتجنب التعارض مع عناصر HTML القياسية الحالية أو المستقبلية.
أما Shadow DOM فهو شجرة DOM داخلية مرتبطة بالعنصر، تعزل تفاصيله البصرية والبنيوية عن الصفحة المحيطة. فعندما تضع قاعدة CSS للزر داخل المكوّن، لا تنتقل تلقائياً إلى أزرار الصفحة الأخرى. وبالعكس، تقل احتمالات أن تكسر أنماط الصفحة شكل المكوّن الداخلي.
يُستخدم العنصر template لتعريف بنية HTML خاملة لا تظهر في الصفحة حتى يتم استنساخها. وهو مفيد في المكونات التي تحتوي على ترميز متكرر أو بنية كبيرة، كما يحافظ على فصل معقول بين قالب الواجهة ومنطق JavaScript.
const template = document.createElement('template');
template.innerHTML = `
<style>
:host {
display: block;
font-family: sans-serif;
}
.card {
padding: 16px;
border: 1px solid #d8dce5;
border-radius: 10px;
background: #ffffff;
}
</style>
<article class="card">
<slot name="title"></slot>
<slot></slot>
</article>
`;
class InfoCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.appendChild(template.content.cloneNode(true));
}
}
customElements.define('info-card', InfoCard);
بعد التسجيل، يمكن استعمال المكوّن في HTML كما يأتي. تسمح عناصر slot بإدخال محتوى من الصفحة المضيفة إلى مواضع محددة داخل Shadow DOM، من دون التخلي عن عزل البنية الداخلية.
<info-card>
<h2 slot="title">تنبيه النظام</h2>
<p>تم حفظ الإعدادات بنجاح.</p>
</info-card>
بناء مكوّن تفاعلي عملي لإدارة المهام
لننشئ مكوّناً بسيطاً يمثل بطاقة مهمة. سيعرض عنوان المهمة، ويتيح للمستخدم تغيير حالتها إلى مكتملة. الهدف من المثال ليس إنشاء نظام إدارة مهام كامل، بل توضيح كيفية جمع القالب والأسلوب والسلوك داخل وحدة مستقلة.
في المُنشئ constructor ننشئ Shadow DOM ونبني الواجهة الداخلية. ثم نحتفظ بمراجع للعناصر التي سيتعامل معها المنطق، مثل العنوان والزر. ويُفضّل عدم تنفيذ عمليات تعتمد على وجود العنصر في الصفحة داخل المُنشئ، لأن العنصر قد لا يكون قد اتصل بـ DOM بعد.
class TaskCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
:host { display: block; margin: 12px 0; }
.task {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px;
border-radius: 8px;
background: #f6f8fb;
}
.done .title {
color: #667085;
text-decoration: line-through;
}
button {
border: 0;
border-radius: 6px;
padding: 8px 12px;
cursor: pointer;
background: #155eef;
color: white;
}
</style>
<section class="task">
<span class="title"></span>
<button type="button">إكمال</button>
</section>
`;
this.titleElement = this.shadowRoot.querySelector('.title');
this.button = this.shadowRoot.querySelector('button');
this.button.addEventListener('click', () => this.toggle());
}
connectedCallback() {
this.render();
}
get completed() {
return this.hasAttribute('completed');
}
toggle() {
this.toggleAttribute('completed', !this.completed);
this.render();
}
render() {
const title = this.getAttribute('title') || 'مهمة بلا عنوان';
const task = this.shadowRoot.querySelector('.task');
this.titleElement.textContent = title;
task.classList.toggle('done', this.completed);
this.button.textContent = this.completed ? 'إعادة الفتح' : 'إكمال';
}
}
customElements.define('task-card', TaskCard);
يمكن استخدامه بهذه الصورة:
<task-card title="مراجعة طلبات الدمج"></task-card>
<task-card title="إعداد تقرير الأداء" completed></task-card>
الخاصية completed ممثلة هنا كسمة HTML منطقية. وجودها يعني أن المهمة مكتملة، وغيابها يعني العكس. هذه صيغة مناسبة للحالات البسيطة لأنها تجعل حالة المكوّن قابلة للفهم مباشرة عند فحص HTML.
إدارة الحالة والتواصل عبر الأحداث المخصصة
ينبغي أن يحتفظ كل مكوّن بحالته المحلية بالقدر الضروري فقط. في مثال بطاقة المهمة، الحالة هي الاكتمال، ويمكن تمثيلها بسمة. أما البيانات التي يجب أن تتشاركها مكونات متعددة، مثل قائمة المهام الكاملة أو المستخدم الحالي، فمن الأفضل أن يديرها عنصر أب أو وحدة JavaScript مستقلة.
يُعد الحدث المخصص الوسيلة الأنسب لإبلاغ العالم الخارجي بما حدث داخل مكوّن مغلق البنية. بدلاً من أن يبحث العنصر الأب في Shadow DOM الخاص ببطاقة المهمة، تطلق البطاقة حدثاً يصف التغيير، ويقرر الأب كيف يتصرف.
toggle() {
this.toggleAttribute('completed', !this.completed);
this.render();
this.dispatchEvent(new CustomEvent('task-change', {
detail: {
title: this.getAttribute('title'),
completed: this.completed
},
bubbles: true,
composed: true
}));
}
تجعل القيمة bubbles: true الحدث يصعد في شجرة DOM، بينما تسمح composed: true له بعبور حدود Shadow DOM. وهذه النقطة ضرورية غالباً عند استخدام Shadow DOM، لأن الأحداث غير المركبة قد تبقى محصورة داخل الشجرة الظلية.
document.addEventListener('task-change', (event) => {
const { title, completed } = event.detail;
console.log(
`المهمة "${title}" أصبحت ${completed ? 'مكتملة' : 'مفتوحة'}`
);
});
بهذا التصميم يصبح المكوّن أقل اقتراناً بالتطبيق. فهو لا يعرف أين تحفظ المهام، ولا هل سيُرسل التغيير إلى خادم، ولا كيف ستظهر الرسائل للمستخدم؛ بل يعلن فقط عن حدوث تغيير. هذه الحدود الواضحة تسهّل الاختبار وإعادة الاستخدام.
دورة الحياة والخصائص والأنماط القابلة للتخصيص
توفر العناصر المخصصة توابع دورة حياة مهمة. يُستدعى connectedCallback() عند إدراج العنصر في الصفحة، وهو مكان مناسب للرسم الأولي أو إضافة مستمعات مرتبطة بالوثيقة. ويُستدعى disconnectedCallback() عند إزالة العنصر، ويجب فيه تنظيف المؤقتات أو مراقبي الأحداث الخارجيين لتجنب تسرب الذاكرة.
عندما تحتاج إلى مراقبة سمة معينة، عرّف مصفوفة observedAttributes ونفذ attributeChangedCallback(). يتيح ذلك تحديث الواجهة تلقائياً إذا غيّر كود خارجي قيمة السمة.
static get observedAttributes() {
return ['title', 'completed'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue && this.shadowRoot) {
this.render();
}
}
لا ينبغي الإفراط في إعادة رسم كل الشجرة الداخلية عند كل تغيير في تطبيقات كبيرة. في المكونات الصغيرة يكون ذلك مقبولاً، أما في الواجهات الأكثر تعقيداً فمن الأفضل تحديث العقد المتأثرة فقط، وحفظ مراجعها، وتقليل عمليات القراءة والكتابة المتتابعة في DOM.
أما تخصيص النمط، فيمكن دعمه عبر خصائص CSS المخصصة. فبدلاً من فرض لون ثابت داخل المكوّن، استخدم متغيراً مثل --task-accent مع قيمة احتياطية. وبذلك يتمكن المستهلك من تعديل مظهر المكوّن من الصفحة الخارجية دون كشف بنيته الداخلية.
button {
background: var(--task-accent, #155eef);
}
<task-card
title="اختبار واجهة الدفع"
style="--task-accent: #0f8a4b">
</task-card>
الإتاحة والاختبار وتنظيم المشروع دون مكتبات
التفاعلية لا تكتمل إذا كانت الواجهة غير قابلة للاستخدام بلوحة المفاتيح أو قارئات الشاشة. استخدم عناصر HTML الدلالية كلما أمكن؛ فالزر الحقيقي button يدعم التركيز والضغط بلوحة المفاتيح تلقائياً، وهو أفضل من تحويل عنصر div إلى زر عبر أحداث النقر فقط. أضف تسميات واضحة، ولا تعتمد على اللون وحده للتعبير عن الحالة، واحرص على ظهور مؤشر التركيز.
في جانب التنظيم، يمكن وضع كل مكوّن في ملف مستقل، مثل task-card.js، ثم استيراده من ملف التطبيق الرئيسي باستخدام وحدات ES Modules. يضمن ذلك عزلاً واضحاً للاعتماديات ويستفيد من أدوات البناء عند الحاجة، من دون فرض إطار عمل كامل.
<script type="module" src="/components/task-card.js"></script>
يُختبر المكوّن على مستويين: اختبار سلوكه الداخلي، مثل تبدل السمة عند النقر، واختبار عقده الخارجي، مثل إطلاق حدث task-change بالتفاصيل الصحيحة. كما يجب اختباره داخل صفحات حقيقية للتأكد من عدم تأثره بأنماط المشروع العامة، ومن عمله مع المحتوى العربي واتجاه الكتابة من اليمين إلى اليسار.
ومن الممارسات الجيدة توثيق خصائص كل مكوّن وسماته وأحداثه والفتحات المتاحة فيه. فالمكوّن الناجح ليس مجرد كتلة كود تعمل، بل واجهة برمجية صغيرة وواضحة يستطيع الآخرون استخدامها دون معرفة تفاصيل تنفيذها.
خاتمة
يوفر تصميم واجهات ويب تفاعلية باستخدام Web Components وJavaScript الأصلي مساراً مرناً لبناء وحدات واجهة مستقلة وقابلة للنقل بين المشاريع. تشكل العناصر المخصصة نقطة البداية، بينما يقدم Shadow DOM عزل البنية والأنماط، وتسمح الأحداث المخصصة بتواصل نظيف بين المكونات.
ابدأ بمكوّن صغير ذي مسؤولية واحدة، واجعل سماته وأحداثه واضحة، واهتم بالإتاحة والتنظيف ضمن دورة الحياة. بهذه المبادئ يمكن بناء مكتبة واجهات متينة دون أطر عمل، أو دمج مكوناتك لاحقاً داخل أي إطار عمل عند الحاجة.
تعليقات
إرسال تعليق