تصميم أنظمة الواجهات الأمامية

هندسة مكتبة المكونات ونظام التصميم

4 دقيقة للقراءة

"صمم مكتبة مكونات" هو سؤال تصميم أنظمة يختبر تصميم 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>

استراتيجية الترحيل للتغييرات الكاسرة:

  1. الإهمال أولاً: أضف تحذيرات console قبل الحذف بإصدار ثانوي واحد
  2. وفّر codemod (jscodeshift) لأتمتة الترحيل
  3. حافظ على API القديم كاسم مستعار لإصدار رئيسي واحد
  4. وثّق كل تغيير كاسر مع أمثلة قبل/بعد

إزالة الشجرة وحجم الحزمة

يجب على المستهلكين الدفع فقط مقابل ما يستوردونه:

// سيء: تصدير البرميل يُجبر المُحزم على تضمين كل شيء
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 المستهلك. أظهر كيف سيستخدم المطورون مكوناتك قبل مناقشة التنفيذ. هذا يُظهر تفكير المنتج، وليس الهندسة فقط.

هذا يُكمل وحدة تصميم الأنظمة. خذ الاختبار لاختبار معرفتك، ثم تمرّن مع مختبر نظام الإشعارات. :::

اختبار

الوحدة 4: تصميم أنظمة الواجهات الأمامية

خذ الاختبار