EduFlow

الوحدة 3 · التنقّل والـ State

التوجيه المبني على الملفات في Expo Router

18 دقراءة 4 داملأ الفراغات

الدرس في لمحة

حوّل الملفات داخل src/app إلى شاشات Trailhead باستخدام Expo Router، وغلّفها بـ stack أصلي عبر ملف layout، وتنقّل بينها باستخدام Link وكائن router.

  1. 1الملفات هي المسارات
  2. 2الـ layouts تغلّف ما يجاورها
  3. 3التنقّل بين الشاشات
  4. 4الـ stack ليس سجلّ المتصفّح
  5. 5شاشة «غير موجود» ولماذا ما زالت الـ URLs مهمّة

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

على الويب، يُظهر شريط العنوان مكانك، ويمشي زرّ الرجوع عبر السجلّ. أما على الهاتف فلا يرى المستخدمون أيًّا منهما: يرون شاشات تنزلق من اليمين، وسهم رجوع أو إيماءة سحب، وtabs في الأسفل. وتحت السطح، ما زال Expo Router يعطي كل شاشة URL، وهذا ما يجعل الـ deep links والتنقّل المضبوط بالأنواع ونسخة الويب ممكنة. تحصل على النموذجين معًا.

الملفات هي المسارات

يبني Expo Router نظام التنقّل من الملفات الموجودة في src/app. مسار الملف هو المسار (route):

Text
src/app/
  _layout.tsx          wraps everything below in a navigator
  index.tsx            /
  about.tsx            /about
  settings/
    _layout.tsx        wraps the settings screens
    index.tsx          /settings
    units.tsx          /settings/units
  +not-found.tsx       any URL that doesn't match

كل ملف مسار يصدّر component بشكل افتراضي (default export). وهذا كل ما تعنيه الشاشة:

src/app/about.tsx
import { Text, View } from 'react-native';

export default function About() {
  return (
    <View style={{ flex: 1, padding: 16 }}>
      <Text>Trailhead keeps a private log of your hikes.</Text>
    </View>
  );
}

ولأن كل ملف في المجلّد يصبح مسارًا، أبقِ كل ما عداه في مكان آخر: src/components، وsrc/lib، وsrc/state. component صفّ حُفظ خطأً في src/app يصبح شاشة يستطيع أي أحد الوصول إليها عبر deep link.

الـ layouts تغلّف ما يجاورها

يرسم ملف _layout.tsx أداة تنقّل حول المسارات الموجودة في مجلّده. يستخدم الـ root layout في Trailhead Stack، وهو أداة التنقّل الأصلية التي تضيف الشاشات وتُزيلها بحركات المنصّة، مع السحب للرجوع على iOS وإيماءة الرجوع على مستوى النظام في Android:

src/app/_layout.tsx
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <Stack screenOptions={{ headerTintColor: '#2f6f4e' }}>
      <Stack.Screen name="index" options={{ title: 'Trailhead' }} />
      <Stack.Screen name="about" options={{ title: 'About' }} />
    </Stack>
  );
}

تنطبق screenOptions على كل شاشة في الـ stack؛ وكل Stack.Screen يخصّص شاشة واحدة، تُطابَق باسم مسارها name. لست مضطرًا إلى إدراج كل مسار: المسارات غير المدرجة تعمل مع ذلك بالخيارات الافتراضية.

بضعة خيارات للترويسة تغطّي معظم الاحتياجات: title، وheaderShown: false للشاشات التي ترسم شريطها العلوي بنفسها (مثل الخريطة بملء الشاشة من الدرس السابق)، وheaderBackTitle للتسمية بجوار سهم الرجوع في iOS، وheaderRight لزرّ مثل “Edit”. وعلى iOS يعطيك headerLargeTitle: true العنوان الكبير القابل للطي الذي تراه في تطبيقات Apple نفسها، وهو يناسب قائمة رئيسية مثل سجلّ الرحلات.

وحين يعتمد العنوان على البيانات (اسم الرحلة مثلًا)، ارسم <Stack.Screen options={{ title: hike.name }} /> داخل الشاشة نفسها. لا يرسم شيئًا مرئيًا؛ بل يضبط ترويسة الشاشة التي هو فيها.

الملفات index.tsx وsettings/index.tsx وsettings/units.tsx تقابل العناوين / و/settings و/settings/units؛ والتنقّل يضيف كلًّا منها إلى stack، والرجوع يُزيل الشاشة العليا. src/app/ index.tsx settings/index.tsx settings/units.tsx / /settings /settings/units / /settings /settings/units Stack (الأعلى هو الظاهر) push يضيف · back يُزيل
الملفات تصبح مسارات، والـ Stack في الـ layout يحمل الشاشات التي زرتها

التنقّل بين الشاشات

لكل ما ينقر عليه المستخدم، استخدم Link. يرسم نصًّا افتراضيًا؛ ومع asChild يسلّم التنقّل إلى الـ component القابل للضغط الخاص بك:

src/app/index.tsx
import { Link } from 'expo-router';
import { Pressable, Text, View } from 'react-native';

export default function HikeLog() {
  return (
    <View style={{ flex: 1, padding: 16, gap: 12 }}>
      <Link href="/about">About Trailhead</Link>
      <Link href="/settings" asChild>
        <Pressable accessibilityRole="button">
          <Text>Settings</Text>
        </Pressable>
      </Link>
    </View>
  );
}

ومن الكود، بعد حفظ أو تسجيل دخول، استخدم الكائن router:

TSX
import { router } from 'expo-router';

router.push('/settings/units'); // add a screen on top
router.back();                  // pop the top screen
router.replace('/');            // swap the current screen; no way back to it

push هو الانتقال الأمامي المعتاد. وreplace للمسارات التي لا معنى للعودة فيها: بعد الشاشات التعريفية، وبعد تسجيل الدخول، وبعد تأكيد «تم الحفظ!». أما back فهو ما يفعله سهم الرجوع في الترويسة وإيماءة السحب أصلًا؛ استدعه بنفسك بعد حفظ نموذج ليعود المستخدم إلى حيث جاء. وهناك أيضًا router.navigate، الذي يذهب إلى مسار، وإن كان ذلك المسار موجودًا في الـ stack أصلًا، يعود إليه بدلًا من إضافة نسخة مكرّرة.

الـ stack ليس سجلّ المتصفّح

من المغري أن تعامل الـ stack كسجلّ المتصفّح، لكن هناك فرقًا واحدًا مهمًّا: كل شاشة في الـ stack تبقى مركّبة. حين تضيف شاشة تفاصيل الرحلة، يحتفظ السجلّ تحتها بالـ state وموضع التمرير، وتستمر الـ effects الخاصة به في العمل. ولهذا يكون الرجوع فوريًا، ولهذا أيضًا لا يُعاد تشغيل useEffect في الشاشة حين تعود إليها. وحين تحتاج إلى تحديث البيانات في كل مرة تعود فيها الشاشة إلى الواجهة، يوفّر Expo Router الـ hook useFocusEffect، الذي يعمل حين تحصل الشاشة على التركيز (focus) وينظّف حين تفقده.

شاشة «غير موجود» ولماذا ما زالت الـ URLs مهمّة

يُرسم +not-found.tsx لأي URL لا يطابق مسارًا، وهذا يحدث على الجوّال غالبًا عبر deep links من رسائل بريد قديمة أو من شاشة غُيّر اسمها. أعطه رسالة ودودة وLink يعود إلى الرئيسية.

والتفكير بالـ URLs يؤتي ثماره لاحقًا في هذا القسم: يستطيع إشعار أن يفتح /hikes/42 مباشرة، ويستطيع رابط في رسالة أن يفتح التطبيق على الشاشة الصحيحة، وتعمل المسارات نفسها على الويب إن بنيت Trailhead له يومًا.

في الدرس التالي ستضيف tabs في أسفل التطبيق، وشاشة تفاصيل لكل رحلة، وتمرّر معرّف الرحلة عبر الـ URL.

أهم النقاط

  • كل ملف داخل src/app هو مسار (route)، ومساره هو الـ URL؛ أبقِ الـ components والـ hooks والأدوات المساعدة خارج ذلك المجلّد.
  • يغلّف ملف _layout.tsx المسارات المجاورة له بأداة تنقّل (navigator)، مثل Stack أصلي.
  • استخدم <Link href> للتنقّل الذي ينقر عليه المستخدم، واستخدم router.push وrouter.replace وrouter.back من الكود.
  • push يضيف شاشة إلى الـ stack، وback يُزيلها، وreplace يستبدل الشاشة الحالية فلا يستطيع المستخدم العودة إليها.

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

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

أضفت src/app/HikeRow.tsx ليحوي component صفّ قائمة قابلًا لإعادة الاستخدام. ماذا يحدث؟

السؤال 2 من 3

بعد أن ينهي المستخدم الشاشات التعريفية (onboarding)، ينتقل Trailhead إلى سجلّ الرحلات. يجب ألا يعيده زرّ الرجوع إلى الشاشات التعريفية. أيّ استدعاء يناسب؟

السؤال 3 من 3

أين يوضع عنوان الترويسة لشاشة /settings؟

تدريب

املأ الفراغات

اربط عناوين Trailhead بالملفات

املأ أسماء الملفات (دون .tsx) ليرسم Expo Router كل URL من الملف الصحيح، ويغلّف شاشات الإعدادات بـ layout خاص بها، ويعرض شاشة ودودة للعناوين غير المعروفة. ثم اختر دالة router التي تعود من /settings/units إلى /settings بعد الحفظ دون تكديس شاشة مكرّرة.

# URL file that renders it / src/app/.tsx /settings src/app/settings/.tsx /settings/units src/app/settings/.tsx (Stack around settings/*) src/app/settings/.tsx (any unknown URL) src/app/.tsx # In code, after the units form saves, return to /settings: router.();