الـWidget الاحترافي ليس صندوقًا جميلًا نضعه في الصفحة وخلاص. هو مكوّن صغير له وظيفة واضحة، وواجهة يمكن فهمها، وحالات تحميل وخطأ ونجاح، وعقد استخدام لا يربك الموقع الذي سيستضيفه. في هذا الدليل سنبني طريقة تفكير عملية تصلح لودجت حالة، أو بطاقة أسعار، أو مشغل ملف، أو لوحة إحصائيات، ثم نحولها إلى مكوّن يمكن تضمينه في أكثر من مشروع من غير أن تتسرب فوضى الـCSS والـJavaScript إلى بقية الصفحة.
1. ما الذي يجعل الـWidget احترافيًا؟
الودجت الجيد يحل مشكلة واحدة في مساحة محدودة. لا يحتاج المستخدم إلى قراءة وثيقة طويلة كي يعرف ماذا يفعل، ولا يحتاج المطور إلى تعديل عشرة ملفات كي يغير عنوانًا أو يمرر قيمة جديدة. الاحتراف هنا يجمع بين أربعة أشياء: هدف واضح، API صغيرة، تصميم قابل للتكيف، وسلوك يمكن اختباره.
| المحور | قرار احترافي | علامة خطر |
|---|---|---|
| الوظيفة | مهمة واحدة قابلة للشرح في جملة | الودجت يحاول أن يكون صفحة كاملة |
| البيانات | خصائص وأحداث موثقة | قراءة عشوائية من global variables |
| التصميم | يعمل في عرض ضيق وعريض | أبعاد ثابتة تكسر الصفحة |
| التجربة | loading وerror وempty وsuccess واضحة | مساحة بيضاء أو رسالة غامضة |
| الدمج | لا يفرض CSS أو مكتبات على الموقع | أنماط عامة مثل `.button` تتسرب للخارج |
2. اكتب مواصفة الودجت قبل رسمه
قبل فتح Figma أو كتابة class، اكتب مواصفة قصيرة. سمِّ المستخدم، واللحظة التي سيستخدم فيها الودجت، والنتيجة التي يجب أن يراها. مثال: “يعرض حالة خدمة من endpoint عام، ويخبر المستخدم بآخر تحديث، ويعطي رسالة قابلة للفهم إذا تعذر الاتصال”. هذه الجملة تمنعك من إضافة زر لا يخدم الهدف أو تحميل بيانات لا يحتاجها المستخدم.
ورقة المواصفة في سبع خانات
- اسم الودجت وهدفه في جملة واحدة.
- خصائص الإدخال: نص، رقم، رابط، أو JSON محدود الشكل.
- الأحداث التي قد يحتاج الموقع الأب إلى استقبالها.
- الحالات المرئية: loading، success، empty، error، disabled.
- الحد الأدنى للعرض والحد الأقصى المعقول له.
- مصدر البيانات، وهل يحتاج إذنًا أو مفتاحًا أو لا.
- ما الذي لن يفعله الإصدار الأول.
3. صمّم API صغيرة لا تحوّل الودجت إلى تطبيق كامل
إذا استخدمت Web Components، فالمكوّن يمكن تعريفه كعنصر HTML مخصص وتسجيله في المتصفح عبر customElements.define() [1]. اجعل أسماء الخصائص قليلة ومفهومة، وميّز بين البيانات التي يقرأها الودجت والأحداث التي يطلقها. لا تجعل الموقع الأب يعبث بعناصر داخلية لا تضمن بقاءها.
<ignis-status
endpoint="/api/status"
label="حالة الخدمة"
refresh="60000">
</ignis-status>
في المثال السابق، العقد واضحة: الودجت يملك endpoint وlabel وrefresh، أما الموقع الأب فلا يحتاج معرفة أسماء عناصره الداخلية. لو احتاج الأب إلى معرفة النتيجة، أطلق حدثًا مخصصًا مثل status-change مع بيانات صغيرة وآمنة بدل تمرير كائن ضخم أو كشف تفاصيل التنفيذ.
4. اختر بنية قابلة لإعادة الاستخدام
Web Components تجمع تقنيات متعاونة: Custom Elements لتعريف العنصر، Shadow DOM لعزل الشجرة والأنماط، وHTML templates وslots لإعادة استخدام البنية وإتاحة نقاط تركيب. توضح MDN أن Shadow DOM يحد من تصادم المعرفات والأنماط بين المكوّن والصفحة المضيفة [1]، لكنه ليس بديلًا عن تصميم دلالي أو وصول جيد.
const template = document.createElement('template');
template.innerHTML = `
<style>
:host { display:block; font: 500 0.95rem system-ui; }
.card { padding:1rem; border:1px solid #ddd; border-radius:14px; }
[part="value"] { font-size:1.6rem; }
</style>
<article class="card" aria-live="polite">
<h2 part="label"></h2>
<p part="value">—</p>
<p part="message" hidden></p>
</article>`;
استخدم :host لتحديد السلوك العام، وخصائص مثل part إذا كنت تريد السماح بتخصيص محدود من الخارج. لا تعتمد على selectors عامة داخل Shadow DOM، ولا تفترض أن الموقع المضيف يستخدم نفس نظام الألوان أو نفس الخط.
5. ابنِ دورة حياة واضحة
يحتاج المكوّن إلى مكان واحد لإنشاء الـShadow Root، ومكان واحد لبدء التشغيل، ودالة render صغيرة تعكس الحالة الحالية. في Custom Elements تُستخدم connectedCallback() عند إدخال العنصر إلى الصفحة، ويمكن استخدام attributeChangedCallback() لمراقبة خصائص معلنة [2]. لا تبدأ طلبات الشبكة من constructor قبل أن يصبح العنصر متصلًا.
class IgnisStatus extends HTMLElement {
static observedAttributes = ['endpoint', 'label'];
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.append(template.content.cloneNode(true));
this.state = { status: 'idle', value: null, message: '' };
}
connectedCallback() {
this.render();
this.load();
}
attributeChangedCallback() {
if (this.isConnected) this.load();
}
}
الفكرة الأهم هي أن الحالة تكون مصدر الحقيقة. لا تغيّر عشرة عناصر من أماكن مختلفة ثم تحاول معرفة ما حدث؛ حدّث state واحدة، وبعدها ارسم الواجهة. هذا يجعل اختبار loading وerror أسهل، ويمنع بقاء رسالة قديمة بعد نجاح طلب جديد.
6. صمّم الحالات قبل تفاصيل الألوان
أغلب الودجتات تبدو جيدة في success فقط، ثم تنهار عندما ينقطع الإنترنت أو لا توجد بيانات. ارسم لكل حالة نصًا وارتفاعًا متوقعًا. الحالة الفارغة ليست خطأ؛ هي نتيجة صحيحة تحتاج شرحًا. أما الخطأ فيجب أن يوضح ما يستطيع المستخدم فعله، مثل المحاولة مرة أخرى، من غير كشف stack trace.
| الحالة | ما يراه المستخدم | قرار التنفيذ |
|---|---|---|
| Loading | عنوان ثابت ومؤشر هادئ | لا تغيّر ارتفاع البطاقة بعشوائية |
| Success | القيمة ووقت التحديث | استخدم تنسيقًا مفهومًا للأرقام والتاريخ |
| Empty | لا توجد نتائج مع سبب مختصر | لا تعرض قيمة وهمية كأنها حقيقة |
| Error | رسالة مفهومة وزر إعادة المحاولة | سجّل التفاصيل للمطور فقط |
| Disabled | سبب عدم التفاعل | لا تجعل اللون هو الإشارة الوحيدة |
7. اجعل التصميم متجاوبًا من داخل الودجت
صمّم الودجت كجزء قد يعيش في sidebar أو داخل بطاقة أو على شاشة هاتف. استخدم وحدات مرنة ولفّ النص الطويل، ولا تضع width: 600px كحل سريع. إذا احتجت breakpoint، اجعله مرتبطًا بعرض المكوّن لا بعرض الشاشة متى كان ذلك ممكنًا، واختبره داخل حاوية ضيقة فعلًا.
احفظ تسلسلًا بصريًا ثابتًا: label صغير، value واضح، message مساعد، ثم action عند الحاجة. لا تضف الظلال والحدود لكل عنصر؛ يكفي سطح واحد وحدود هادئة وتباين يوضح ما يمكن الضغط عليه. لو كان الودجت جزءًا من نظام تصميم، عرّف custom properties مثل --ignis-accent بدل نسخ ألوان المشروع المضيف داخل كل سطر.
8. الوصول ليس إضافة لاحقة
الودجت التفاعلي يجب أن يعمل بلوحة المفاتيح وقارئ الشاشة. استخدم HTML دلاليًا قبل ARIA، واجعل زر إعادة المحاولة زرًا حقيقيًا، وامنح كل معلومة اسمًا واضحًا. توصي مراجع الوصول إلى الودجتات بأن تكون الحالة المعلنة مفهومة دون الاعتماد على اللون أو الحركة [5] [6]. اجعل focus ظاهرًا، ولا تحبس المستخدم داخل Shadow DOM من غير سبب.
استخدم aria-live="polite" فقط للمعلومة التي تتغير ويحتاج المستخدم إلى معرفتها، ولا تجعل كل إعادة رسم إعلانًا صوتيًا. إذا كان الودجت يحتوي dialog أو listbox أو tabs، فلا تخترع سلوكًا من الذاكرة؛ راجع نمط الوصول المناسب واختبر التنقل بالأسهم وEscape وTab.
9. البيانات والأمان والخصوصية
لا تضع أسرارًا داخل Widget يعمل في المتصفح؛ أي قيمة تصل إلى JavaScript في العميل يمكن للمستخدم رؤيتها. استخدم endpoint عام محدود أو اجعل الطلب يمر عبر خادمك عندما تكون هناك صلاحيات. تحقّق من شكل البيانات القادمة قبل عرضها، واستخدم textContent للنصوص بدل إدخال HTML غير موثوق.
function readStatus(payload) {
if (!payload || typeof payload !== 'object') {
throw new Error('Invalid status response');
}
return {
value: String(payload.value ?? 'غير متاح'),
updatedAt: Number.isFinite(payload.updatedAt)
? payload.updatedAt
: null
};
}
وثّق ما يتم إرساله وأين يتم تخزينه. إذا كان الودجت يجمع بريدًا أو معرفًا أو سلوكًا، فاجعل ذلك واضحًا، ووفّر طريقة تمنع التحميل قبل الموافقة عندما يكون ذلك مطلوبًا. الودجت الصغير ليس إعفاءً من مسؤولية البيانات.
10. الأداء: اجعل العمل عند الحاجة
لا تبدأ أكثر من طلب عند تغيير attribute، ولا تترك timer يعمل بعد إزالة العنصر. خزّن مؤقت التحديث، وألغِ الطلب السابق عند بدء طلب جديد باستخدام AbortController. حمّل الكود عند الحاجة إذا كان الودجت نادر الظهور، ولا تضع مكتبة كاملة من أجل وظيفة يمكن تنفيذها بواجهة المتصفح.
async load() {
this.controller?.abort();
this.controller = new AbortController();
this.setState({ status: 'loading' });
try {
const response = await fetch(this.endpoint, {
signal: this.controller.signal,
headers: { Accept: 'application/json' }
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
this.setState({ status: 'success', data: readStatus(await response.json()) });
} catch (error) {
if (error.name !== 'AbortError') this.setState({ status: 'error' });
}
}
قِس بدل التخمين: حجم JavaScript، عدد الطلبات، وقت ظهور المحتوى، وما إذا كان الودجت يسبب layout shift. لا تجعل animation مستمرة أو polling سريعًا لمجرد أن البطاقة تبدو حية؛ حدث البيانات حسب الحاجة وبحدود معقولة.
11. اختبر الودجت في صفحة غريبة عنه
الاختبار الحقيقي ليس داخل demo الذي صممته. أنشئ صفحة اختبار فيها CSS عدواني، خط مختلف، اتجاه RTL، حاوية 240px، شبكة عالية الكثافة، وبطء شبكة. جرّب أكثر من نسخة من الودجت في الصفحة نفسها، لأن المتغيرات العامة والـIDs الثابتة تظهر عيوبها عند التكرار.
| الاختبار | السؤال | النجاح |
|---|---|---|
| Keyboard | هل يمكن الوصول لكل action؟ | ترتيب focus منطقي ومؤشر ظاهر |
| Screen reader | هل الحالة مفهومة؟ | العنوان والقيمة والتغيير مسموعون بوضوح |
| Network | ماذا يحدث مع 500 أو timeout؟ | error مفهومة وإعادة المحاولة تعمل |
| Responsive | هل ينهار داخل 240px؟ | لا قص أفقي ولا نص خارج البطاقة |
| Multiple instances | هل تختلط الحالات؟ | كل نسخة مستقلة عن الأخرى |
| Reduced motion | هل تحترم إعداد المستخدم؟ | لا حركة ضرورية عند طلب تقليلها |
12. طريقة نشر وتوثيق الودجت
اكتب README يبدأ بمثال نسخ ولصق، ثم جدول الخصائص والأحداث، ثم الحالات والمشاكل المعروفة. اذكر دعم المتصفحات، ومتطلبات endpoint، وحجم الحزمة، وطريقة تغيير الألوان. لا تجعل المستخدم يبحث داخل المصدر ليعرف أن refresh بالمللي ثانية أو أن تغيير endpoint يعيد التحميل.
عند إصدار نسخة جديدة، اكتب changelog مختصرًا، واختبر المثال الموجود في README نفسه. إذا كسرت اسم attribute أو شكل event، ارفع major version أو وفر فترة توافق. المكوّن القابل لإعادة الاستخدام ينجح عندما يستطيع مطور آخر استخدامه من الوثيقة، لا عندما ينجح عند صاحبه فقط.
خطة تنفيذ من يومين
| الفترة | المهمة | الناتج |
|---|---|---|
| الساعات 1–2 | المواصفة والـAPI والحالات | ورقة عقد ورسمة بسيطة |
| الساعات 3–5 | Markup دلالي ونسخة static | واجهة قابلة للفحص دون بيانات حقيقية |
| الساعات 6–8 | Custom Element وShadow DOM | نسخة يمكن تضمينها في صفحة ثانية |
| اليوم الثاني صباحًا | البيانات والأخطاء والأداء | loading/error/retry مع إلغاء الطلبات |
| اليوم الثاني مساءً | الوصول والاختبار والتوثيق | README وdemo وchecklist إصدار |
مراجع رسمية
استخدم هذه المراجع عند تنفيذ التفاصيل أو عند اختلاف سلوك المتصفح؛ المواصفات والواجهات تتطور، لذلك لا تعتمد على مثال قديم من منشور مجهول.