EduFlow

الوحدة 2 · التخطيط والتنسيق

الـ StyleSheet في عالم بلا cascade

18 دقراءة 4 دبيئة تجريب الكود

الدرس في لمحة

نسّق Trailhead باستخدام StyleSheet ومصفوفات التنسيق والـ design tokens، مستخدمًا نقاطًا بلا وحدات وخصائص مفصّلة، في نظام بلا cascade ولا selectors ولا media queries.

  1. 1كائنات التنسيق وStyleSheet.create
  2. 2أرقام بلا وحدات
  3. 3مصفوفات التنسيق: الترتيب هو القاعدة الوحيدة
  4. 4الـ tokens بدلًا من الـ cascade
  5. 5لماذا غياب الـ cascade ميزة

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

في أول مرة تنسّق فيها شاشة في React Native، ستمدّ يدك إلى أشياء غير موجودة: اسم class، و:hover، وmedia query، وfont-family على body ليرثه كل شيء. لا يملك React Native أيًّا منها. ما يملكه بدلًا من ذلك أصغر وأكثر قابلية للتوقّع: كائنات عادية، تُطبَّق مباشرة على الـ component الذي يحتاج إليها، بترتيب تتحكّم فيه أنت.

كائنات التنسيق وStyleSheet.create

التنسيق (style) كائن JavaScript بمفاتيح camelCase. يمكنك تمريره مباشرة، لكن معظم الكود يجمع التنسيقات باستخدام StyleSheet.create، الذي يمنحك الإكمال التلقائي وفحص الأنواع ومكانًا واحدًا تقرأ فيه تنسيقات الـ component:

src/components/HikeCard.tsx
import { StyleSheet, Text, View } from 'react-native';

export function HikeCard({ name, distanceKm }: { name: string; distanceKm: number }) {
  return (
    <View style={styles.card}>
      <Text style={styles.title}>{name}</Text>
      <Text style={styles.meta}>{distanceKm} km</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  card: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    paddingVertical: 12,
    paddingHorizontal: 16,
    borderWidth: 1,
    borderColor: '#e2e8e4',
  },
  title: { fontSize: 18, fontWeight: '600', color: '#1d2a22' },
  meta: { fontSize: 14, color: '#5c6b62', marginTop: 4 },
});

تعريف styles أسفل الـ component عُرف متّبع، وهو عُرف جيد: الـ JSX يأتي أولًا حين تفتح الملف.

هل StyleSheet.create أسرع من الكائنات المباشرة (inline)؟ ليس بأي قدر ستقيسه اليوم. الكلفة الحقيقية للكائنات المباشرة هي سهولة القراءة، وإنشاء كائنات جديدة في كل render، وهذا لا يهمّ إلا حين تمرّر التنسيقات إلى أبناء مغلّفين بـ memo. القاعدة المعقولة: التنسيقات الثابتة في StyleSheet.create، والقيم القليلة التي تعتمد على الـ props أو الـ state تُكتب مباشرة أو في مصفوفة التنسيق بجوارها.

أرقام بلا وحدات

borderRadius: 12 تعني 12 نقطة مستقلّة عن الكثافة (density-independent points)، لا 12 بكسلًا فعليًا. الزاوية ذات الـ 12 نقطة تبدو بالحجم نفسه على هاتف Android رخيص وعلى iPhone بكثافة بكسلات أعلى بثلاث مرات؛ فالمنصّة تتولّى الضرب نيابةً عنك. النسب المئوية تعمل كنصوص (width: '50%')، وهذا كل شيء: لا px ولا rem ولا em ولا vh.

والاختصارات التي تأخذ عدّة قيم غير موجودة أيضًا. بدلًا من margin: '8px 16px' تكتب marginVertical: 8, marginHorizontal: 16. والنمط نفسه يشمل الحشو (paddingTop، paddingHorizontal)، والحدود (borderTopWidth، borderBottomColor)، والزوايا (borderTopLeftRadius). أما الاختصارات ذات القيمة الواحدة مثل margin: 8 وborderRadius: 12 فتعمل كما تتوقّع.

يدعم React Native الحديث أيضًا عدّة خصائص يفتقدها مطوّرو الويب: gap وrowGap وcolumnGap للمسافات بين الأبناء، وboxShadow بنصّ يشبه CSS، وfilter. أما الدروس القديمة فتستخدم props منفصلة هي shadowColor/shadowOffset لـ iOS وelevation لـ Android، وما زلت ستراها في قواعد الكود الموجودة.

مصفوفات التنسيق: الترتيب هو القاعدة الوحيدة

يقبل الـ prop المسمّى style مصفوفة. يدمجها React Native من اليسار إلى اليمين، فتفوز المفاتيح اللاحقة، وتُتخطّى القيم الزائفة (false وnull وundefined). وهذا يحوّل التنسيق الشرطي إلى سطر واحد:

TSX
<View style={[styles.card, isFavorite && styles.favorite, isSelected && styles.selected]} />

لا توجد specificity تفكّر فيها، ولا !important، ولا تنسيق يصل من أب على بُعد ثلاثة ملفات. إن بدت بطاقة خاطئة، فالسبب في تلك المصفوفة.

استخدم القاعدة نفسها لتجعل الـ components الخاصة بك قابلة للتعديل. اقبل prop باسم style وضعه في الآخر:

TSX
import type { StyleProp, ViewStyle } from 'react-native';

type Props = { name: string; distanceKm: number; style?: StyleProp<ViewStyle> };

export function HikeCard({ name, distanceKm, style }: Props) {
  return (
    <View style={[styles.card, style]}>
      {/* …title and meta as before */}
    </View>
  );
}

// A caller nudges one instance without forking the component:
<HikeCard name="Ridge Loop" distanceKm={8.4} style={{ marginBottom: 24 }} />

StyleProp<ViewStyle> هو الـ type الذي يقبل كائنًا أو مصفوفة أو قيمة زائفة، تمامًا مثل الـ components المدمجة.

الـ tokens بدلًا من الـ cascade

دون وراثة، يأتي الاتساق من مكانين: الـ components (بطاقة HikeCard واحدة، وزر PrimaryButton واحد، وAppText واحد)، وملف صغير من الـ tokens (قيم التصميم المشتركة) تستورده كلها.

src/theme.ts
export const colors = {
  bg: '#f6f8f6',
  surface: '#ffffff',
  text: '#1d2a22',
  textMuted: '#5c6b62',
  primary: '#2f6f4e',
  border: '#e2e8e4',
};

export const space = { xs: 4, sm: 8, md: 12, lg: 16, xl: 24 };
export const radius = { sm: 8, md: 12, lg: 20 };

الآن تقرأ البطاقة borderRadius: radius.md وpaddingHorizontal: space.lg، وأي تغيير من المصمّم يقع في ملف واحد.

لماذا غياب الـ cascade ميزة

في تطبيق ويب كبير، تأتي أصعب أخطاء التنسيق من قواعد لم تكتبها تصل إلى عناصر لم تتوقّعها. يُلغي React Native هذه الفئة كلها. شكل الـ component هو تنسيقه الخاص زائد ما يمرّره له من يستخدمه، ولا شيء غير ذلك. هذا يعني كتابة أكثر في البداية، وتصحيح أخطاء أقل بكثير لاحقًا. والفرق التي تريد سهولة تشبه الـ classes تضيف مكتبة مثل NativeWind (classes من Tailwind تُترجَم إلى كائنات تنسيق)، لكنها في الأسفل تبقى الكائنات نفسها.

في الدرس التالي سترتّب هذه الصناديق المنسّقة باستخدام flexbox، حيث تختلف القيم الافتراضية في React Native عن الويب بطرق تفسّر معظم لحظات «لماذا هذا العنصر منضغط؟».

أهم النقاط

  • التنسيقات كائنات JavaScript بمفاتيح camelCase وأرقام بلا وحدات، تُقاس بنقاط مستقلّة عن كثافة البكسلات.
  • اختصارات CSS متعدّدة القيم، مثل margin '8px 16px'، غير موجودة؛ استخدم marginVertical وmarginHorizontal.
  • في مصفوفة التنسيق تفوز العناصر اللاحقة وتُتخطّى القيم الزائفة (falsy)، وهذا يجعل التنسيق الشرطي سطرًا واحدًا.
  • إعادة الاستخدام تتم عبر الـ components وكائن من الـ tokens، لا عبر الـ selectors أو ملف تنسيق عام.
  • اقبل prop باسم style وضعه في آخر المصفوفة، ليتمكّن من يستخدم الـ component من تعديله دون نسخه.

قراءات إضافية

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

ما الذي يُرسَم مع style={[styles.card, isFavorite && styles.favorite, { padding: 8 }]} حين تكون isFavorite تساوي false وكلا التنسيقين يضبط padding؟

السؤال 2 من 3

أيّ كائن تنسيق صالح في React Native؟

السؤال 3 من 3

يحتاج Trailhead أن تستخدم كل البطاقات نصف قطر الزوايا والمسافات نفسها. ما الطريقة المعتادة؟

تدريب

بيئة تجريب الكود

اكتب دالة دمج التنسيقات بنفسك

يدمج React Native الـ prop المسمّى style بالطريقة نفسها في كل مرة: تُقرأ المصفوفات من اليسار إلى اليمين، وتفوز المفاتيح اللاحقة، وتُتخطّى القيم الزائفة، ويمكن أن تتداخل المصفوفات. اكتب flattenStyle(style) بحيث تتّبع هذه القواعد وتُرجع كائنًا عاديًا جديدًا واحدًا.

  • الكائن يُرجع نسخة منه.
  • القيمة الزائفة (false أو null أو undefined) أو المصفوفة الفارغة تُرجع {}.
  • يجب ألا تعدّل أبدًا كائنات التنسيق التي تمرّرها إليها.

الكود المبدئي قريب من الحل لكن فيه ثلاثة أخطاء. اعثر عليها.

// Merge a React Native style value into one plain object.
// A style can be an object, a falsy value, or an array of styles (arrays can nest).
function flattenStyle(style) {
  if (Array.isArray(style)) {
    return Object.assign(style[0], ...style.slice(1));
  }
  return style;
}

const card = { padding: 12, borderRadius: 12 };
console.log(flattenStyle([card, false, { padding: 8 }]));
جارٍ تحميل المحرّر…

Ctrl/⌘ + Enter للتشغيل · يعمل في بيئة معزولة داخل متصفّحك. لا شيء يغادر جهازك.

الـ console

(لا مخرجات)

الفحوص

  • الكائن المفرد يعود بالمفاتيح نفسها
  • العناصر اللاحقة تتغلّب على السابقة
  • تُتخطّى عناصر false وnull وundefined
  • تُسطَّح المصفوفات المتداخلة بالترتيب
  • لا تُعدَّل كائنات التنسيق المُدخلة أبدًا
  • المدخل الزائف والمصفوفات الفارغة تُرجع {}