الوحدة 2 · التخطيط والتنسيق
الـ StyleSheet في عالم بلا cascade
18 دقراءة 4 دبيئة تجريب الكود
الدرس في لمحة
نسّق Trailhead باستخدام StyleSheet ومصفوفات التنسيق والـ design tokens، مستخدمًا نقاطًا بلا وحدات وخصائص مفصّلة، في نظام بلا cascade ولا selectors ولا media queries.
- 1كائنات التنسيق وStyleSheet.create
- 2أرقام بلا وحدات
- 3مصفوفات التنسيق: الترتيب هو القاعدة الوحيدة
- 4الـ tokens بدلًا من الـ cascade
- 5لماذا غياب الـ cascade ميزة
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
في أول مرة تنسّق فيها شاشة في React Native، ستمدّ يدك إلى أشياء غير موجودة: اسم class، و:hover، وmedia query، وfont-family على body ليرثه كل شيء. لا يملك React Native أيًّا منها. ما يملكه بدلًا من ذلك أصغر وأكثر قابلية للتوقّع: كائنات عادية، تُطبَّق مباشرة على الـ component الذي يحتاج إليها، بترتيب تتحكّم فيه أنت.
كائنات التنسيق وStyleSheet.create
التنسيق (style) كائن JavaScript بمفاتيح camelCase. يمكنك تمريره مباشرة، لكن معظم الكود يجمع التنسيقات باستخدام StyleSheet.create، الذي يمنحك الإكمال التلقائي وفحص الأنواع ومكانًا واحدًا تقرأ فيه تنسيقات الـ component:
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). وهذا يحوّل التنسيق الشرطي إلى سطر واحد:
<View style={[styles.card, isFavorite && styles.favorite, isSelected && styles.selected]} />لا توجد specificity تفكّر فيها، ولا !important، ولا تنسيق يصل من أب على بُعد ثلاثة ملفات. إن بدت بطاقة خاطئة، فالسبب في تلك المصفوفة.
استخدم القاعدة نفسها لتجعل الـ components الخاصة بك قابلة للتعديل. اقبل prop باسم style وضعه في الآخر:
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 (قيم التصميم المشتركة) تستورده كلها.
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 من تعديله دون نسخه.