تصميم أنظمة الواجهات الأمامية
هندسة مكتبة المكونات ونظام التصميم
"صمم مكتبة مكونات" هو سؤال تصميم أنظمة يختبر تصميم API وأنماط التركيب وإمكانية الوصول والتفكير في قابلية التوسع. يظهر في الشركات التي تبني منصات UI مشتركة.
مبادئ تصميم API
أفضل مكتبات المكونات تتبع هذه الأنماط:
المكونات المُركّبة
بدلاً من مكون واحد ضخم بعشرات الخصائص، قسّم إلى قطع قابلة للتركيب:
// سيء: مكون ضخم ثقيل الخصائص
<Select
options={options}
label="البلد"
placeholder="اختر..."
isSearchable
isMulti
onSearch={handleSearch}
renderOption={renderOption}
renderValue={renderValue}
/>
// جيد: نمط المكونات المُركّبة
<Select onValueChange={setCountry}>
<Select.Trigger>
<Select.Value placeholder="اختر بلدًا..." />
</Select.Trigger>
<Select.Content>
<Select.Search placeholder="ابحث عن البلدان..." />
<Select.Group label="شائعة">
<Select.Item value="us">الولايات المتحدة</Select.Item>
<Select.Item value="uk">المملكة المتحدة</Select.Item>
</Select.Group>
<Select.Group label="جميع البلدان">
{countries.map(c => (
<Select.Item key={c.code} value={c.code}>{c.name}</Select.Item>
))}
</Select.Group>
</Select.Content>
</Select>
لماذا تفوز المكونات المُركّبة:
- المستخدمون يُركّبون فقط ما يحتاجون
- كل مكون فرعي لديه API مُركّز
- سهل إضافة عناصر مخصصة بين الأجزاء
- الحالة مُشاركة عبر React Context داخليًا
خصائص العرض ونمط الفتحات
امنح المستهلكين التحكم في العرض:
// مكون بدون واجهة: يتعامل مع المنطق، المستهلك يتعامل مع العرض
function Combobox<T>({ items, onSelect, children }: {
items: T[];
onSelect: (item: T) => void;
children: (props: {
inputProps: InputHTMLAttributes<HTMLInputElement>;
listProps: HTMLAttributes<HTMLUListElement>;
getItemProps: (item: T, index: number) => HTMLAttributes<HTMLLIElement>;
isOpen: boolean;
highlightedIndex: number;
}) => ReactNode;
}) {
// جميع منطق لوحة المفاتيح والتركيز والتحديد مُعالج داخليًا
// المستهلك يقرر كيفية عرض كل جزء
}
// الاستخدام
<Combobox items={users} onSelect={handleSelect}>
{({ inputProps, listProps, getItemProps, isOpen, highlightedIndex }) => (
<div className="my-custom-combobox">
<input {...inputProps} className="custom-input" />
{isOpen && (
<ul {...listProps} className="custom-dropdown">
{users.map((user, i) => (
<li
{...getItemProps(user, i)}
key={user.id}
className={i === highlightedIndex ? 'active' : ''}
>
<Avatar src={user.avatar} /> {user.name}
</li>
))}
</ul>
)}
</div>
)}
</Combobox>
التركيب فوق التكوين
// نظام التصميم يوفر بدائيات
<Card>
<Card.Header>
<Card.Title>ملخص الطلب</Card.Title>
<Card.Description>راجع عناصرك</Card.Description>
</Card.Header>
<Card.Content>
<ItemList items={cartItems} />
</Card.Content>
<Card.Footer>
<Button variant="outline">إلغاء</Button>
<Button>تأكيد الطلب</Button>
</Card.Footer>
</Card>
نُهج التنسيق
| النهج | المزايا | العيوب | الأفضل لـ |
|---|---|---|---|
| متغيرات CSS | بدون تكلفة وقت تشغيل، أصلية، تعمل مع SSR | منطق محدود (بدون حسابات شرطية) | معظم أنظمة التصميم |
| CSS-in-JS (styled-components، Emotion) | تنسيقات ديناميكية، أنماط مترافقة | حمل وقت التشغيل، تعقيد SSR | تنسيق ديناميكي عالي |
| Tailwind CSS | أولوية الأدوات المساعدة، حزمة صغيرة، تكرار سريع | ترميز مُطوّل، منحنى تعلم | التطوير السريع |
متغيرات CSS (الافتراضي الموصى به)
/* رموز التصميم كمتغيرات CSS */
:root {
--color-primary: #2563eb;
--color-primary-hover: #1d4ed8;
--color-background: #ffffff;
--color-text: #111827;
--radius-md: 8px;
--space-4: 16px;
--font-sans: 'Inter', system-ui, sans-serif;
}
/* تجاوز الوضع الداكن */
[data-theme="dark"] {
--color-primary: #60a5fa;
--color-primary-hover: #93bbfd;
--color-background: #0f172a;
--color-text: #f1f5f9;
}
// مُبدّل التنسيق بسيط
function ThemeToggle() {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
return (
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
تبديل التنسيق
</button>
);
}
إمكانية الوصول مُدمجة
كل مكون في نظام التصميم يجب أن يتعامل مع إمكانية الوصول بشكل افتراضي. لا يجب على المستهلكين إضافة سمات ARIA يدويًا.
التنقل بلوحة المفاتيح
function Tabs({ children, defaultValue }: TabsProps) {
const [activeTab, setActiveTab] = useState(defaultValue);
const tabRefs = useRef<Map<string, HTMLButtonElement>>(new Map());
function handleKeyDown(e: React.KeyboardEvent) {
const tabs = Array.from(tabRefs.current.keys());
const currentIndex = tabs.indexOf(activeTab);
let nextIndex: number;
switch (e.key) {
case 'ArrowRight':
nextIndex = (currentIndex + 1) % tabs.length;
break;
case 'ArrowLeft':
nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
break;
case 'Home':
nextIndex = 0;
break;
case 'End':
nextIndex = tabs.length - 1;
break;
default:
return;
}
e.preventDefault();
const nextTab = tabs[nextIndex];
setActiveTab(nextTab);
tabRefs.current.get(nextTab)?.focus();
}
return (
<div role="tablist" onKeyDown={handleKeyDown}>
{/* أزرار التبويب مع role="tab"، aria-selected، aria-controls */}
{/* لوحات التبويب مع role="tabpanel"، aria-labelledby */}
</div>
);
}
إدارة التركيز
// الحوار يحبس التركيز عند الفتح
function Dialog({ isOpen, onClose, children }: DialogProps) {
const dialogRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!isOpen) return;
const dialog = dialogRef.current;
if (!dialog) return;
// تخزين العنصر المُركّز سابقًا
const previouslyFocused = document.activeElement as HTMLElement;
// تركيز أول عنصر قابل للتركيز في الحوار
const firstFocusable = dialog.querySelector<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
firstFocusable?.focus();
// استعادة التركيز عند إغلاق الحوار
return () => previouslyFocused?.focus();
}, [isOpen]);
if (!isOpen) return null;
return (
<div role="dialog" aria-modal="true" ref={dialogRef}>
{children}
</div>
);
}
الإصدارات والتغييرات الكاسرة
إصدارات نظام التصميم تتبع الإصدار الدلالي (semver):
| نوع التغيير | زيادة الإصدار | مثال |
|---|---|---|
| إصلاح خطأ، تعديل تنسيق | تصحيح (1.0.X) | إصلاح لون التمرير للزر |
| مكون جديد، خاصية جديدة | ثانوي (1.X.0) | إضافة مكون <Skeleton> |
| خاصية محذوفة، مكون مُعاد تسميته | رئيسي (X.0.0) | إعادة تسمية <Input> إلى <TextField> |
استراتيجية الترحيل للتغييرات الكاسرة:
- الإهمال أولاً: أضف تحذيرات console قبل الحذف بإصدار ثانوي واحد
- وفّر codemod (jscodeshift) لأتمتة الترحيل
- حافظ على API القديم كاسم مستعار لإصدار رئيسي واحد
- وثّق كل تغيير كاسر مع أمثلة قبل/بعد
إزالة الشجرة وحجم الحزمة
يجب على المستهلكين الدفع فقط مقابل ما يستوردونه:
// سيء: تصدير البرميل يُجبر المُحزم على تضمين كل شيء
import { Button, Card, Dialog, Table, Tabs } from '@mylib/components';
// جيد: نقاط دخول فردية
import { Button } from '@mylib/components/button';
import { Card } from '@mylib/components/card';
كيفية تمكين إزالة الشجرة:
- اضبط
"sideEffects": falseفيpackage.json - استخدم التصديرات المُسماة، وليس التصديرات الافتراضية
- تجنب الآثار الجانبية على المستوى الأعلى في الوحدات
- وفّر بناءات ESM و CJS
- استخدم حقل
exportsفيpackage.jsonلنقاط دخول كل مكون
{
"name": "@mylib/components",
"sideEffects": false,
"exports": {
"./button": {
"import": "./dist/button/index.mjs",
"require": "./dist/button/index.cjs"
},
"./card": {
"import": "./dist/card/index.mjs",
"require": "./dist/card/index.cjs"
}
}
}
التوثيق و Storybook
نظام تصميم بدون توثيق هو نظام تصميم لا يستخدمه أحد.
التوثيق الأساسي لكل مكون:
- ساحة لعب تفاعلية (قصص Storybook)
- جدول الخصائص مع الأنواع والقيم الافتراضية
- أمثلة استخدام للأنماط الشائعة
- ملاحظات إمكانية الوصول (اختصارات لوحة المفاتيح، سلوك قارئ الشاشة)
- إرشادات مرئية افعل/لا تفعل
نصيحة للمقابلات: عند طلب تصميم مكتبة مكونات، ابدأ بـ API المستهلك. أظهر كيف سيستخدم المطورون مكوناتك قبل مناقشة التنفيذ. هذا يُظهر تفكير المنتج، وليس الهندسة فقط.
هذا يُكمل وحدة تصميم الأنظمة. خذ الاختبار لاختبار معرفتك، ثم تمرّن مع مختبر نظام الإشعارات. :::