الوحدة 3 · التنقّل والـ State
الـ modals والمسارات المحميّة والـ deep links
17 دقراءة 4 دبيئة تجريب الكود
الدرس في لمحة
اعرض نموذج إضافة رحلة في Trailhead بوصفه modal، وأبقِ المستخدمين غير المسجّلين على شاشة تسجيل الدخول باستخدام Stack.Protected، وافتح رحلات محدّدة من الـ deep links، مع فهم كيف تُطابَق الـ URLs مع ملفات المسارات.
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
ثلاثة مواقف لا تناسب نمط «أضف شاشة» الذي استخدمته حتى الآن. تسجيل رحلة مهمّة قصيرة يبدؤها المستخدم وينهيها، فيجب أن تبدو منفصلة عن التصفّح. وبعض الشاشات يجب ألا توجد إلا للمستخدمين المسجّلين. ويجب أن ينفتح Trailhead على الرحلة الصحيحة حين ينقر أحدهم على رابط في تذكير أو رسالة. يتعامل Expo Router مع الثلاثة في ملف الـ layout.
نموذج إضافة رحلة بوصفه modal
الـ modal شاشة تُعرض فوق السياق الحالي، وتنزلق عادةً من الأسفل. على iOS تظهر بطاقةً تبقى الشاشة السابقة مرئية خلفها، ويستطيع المستخدمون سحبها إلى الأسفل للإلغاء. اجعل النموذج modal بضبط طريقة عرضه في الـ Stack الجذري:
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="hikes/[id]" options={{ title: 'Hike' }} />
<Stack.Screen name="new-hike" options={{ presentation: 'modal', title: 'Log a hike' }} />
</Stack>يبقى فتحه بـ router.push('/new-hike') (أو Link)؛ وإغلاقه بعد الحفظ بـ router.back()، الذي يُزيل الـ modal كأي شاشة. لا يملك Android ورقة على شكل بطاقة افتراضيًا، فيظهر الـ modal صفحةً بملء الشاشة بحركة مختلفة؛ لذا أعطِ النموذج زرّ Cancel ظاهرًا في headerLeft ليكون للمنصّتين مخرج واضح.
حين يحوي النموذج مدخلات غير محفوظة، فإن سحبه بعيدًا يُضيّع ما كتبه المستخدم. في النماذج الطويلة، اطلب التأكيد قبل التجاهل باستخدام Alert عند Cancel؛ أما في نموذج قصير مثل نموذج Trailhead، فمسوّدة تُعاد كتابتها بسرعة مقايضة مقبولة.
المسارات المحميّة
يزامن Trailhead الرحلات مع حساب، لذا تتطلّب معظم الشاشات تسجيل الدخول. يغلّف Stack.Protected المسارات بشرط:
import { Stack } from 'expo-router';
import { useSession } from '../state/session';
export default function RootLayout() {
const { isSignedIn, isLoading } = useSession();
if (isLoading) return null; // the splash screen is still showing
return (
<Stack>
<Stack.Protected guard={!isSignedIn}>
<Stack.Screen name="sign-in" options={{ headerShown: false }} />
</Stack.Protected>
<Stack.Protected guard={isSignedIn}>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="hikes/[id]" options={{ title: 'Hike' }} />
<Stack.Screen name="new-hike" options={{ presentation: 'modal', title: 'Log a hike' }} />
</Stack.Protected>
</Stack>
);
}حين يكون guard قيمته false، تصبح تلك المسارات غير متاحة: التنقّل إليها، بما في ذلك من deep link، يرسل المستخدم إلى أول شاشة متاحة بدلًا منها، وتُحذف أي مدخلات لها في السجلّ. تسجيل الدخول يقلب قيمة isSignedIn، فيُعاد الـ render للـ layout، وتظهر الشاشات المحميّة دون أن تستدعي router على الإطلاق. وتسجيل الخروج يفعل العكس، فيمسح أيضًا الـ back stack، فلا تستطيع إيماءة الرجوع كشف شاشة خاصة بالمستخدم المسجّل.
useSession هو context مثل الذي بنيته قبل درسين. تغطّي isLoading اللحظة عند بدء التشغيل التي يكون فيها Trailhead ما زال يقرأ الـ token المحفوظ من التخزين الآمن (القسم 4)؛ ورسم لا شيء بينما شاشة البداية ظاهرة يتجنّب وميض صفحة تسجيل الدخول للمستخدمين المسجّلين أصلًا.
الـ deep links
الـ deep link (الرابط العميق) يفتح التطبيق على شاشة محدّدة. أعطِ Trailhead مخطّط URL (scheme) في app.json:
{
"expo": {
"name": "Trailhead",
"scheme": "trailhead"
}
}الآن يفتح trailhead://hikes/42 التطبيق ويرسم /hikes/42 بالـ Stack نفسه الذي بنيته، بما في ذلك زرّ رجوع إلى السجلّ. تحصل على هذا مجانًا من التوجيه المبني على الملفات: كل مسار له URL أصلًا. المخطّطات جزء من الإعدادات الأصلية للتطبيق، لذا اختبرها في development build؛ فلـ Expo Go مخطّطه الخاص. ومن الطرفية:
npx uri-scheme open trailhead://hikes/42 --ios
npx uri-scheme open trailhead://hikes/42 --androidللمخطّطات المخصّصة نقطة ضعف: أي تطبيق يستطيع ادّعاء trailhead://. وللروابط التي تشاركها على الويب، استخدم الـ universal links (على iOS) والـ App Links (على Android)، التي تستخدم نطاق https:// الخاص بك وتتطلّب استضافة ملف تحقّق صغير عليه. يشرح دليل الروابط في Expo الاثنين خطوة بخطوة.
كيف يختار الـ URL مساره
حين يصل رابط، يقارن Expo Router مساره بكل ملف مسار. ومعرفة القواعد تُزيل التخمين:
| القاعدة | مثال |
|---|---|
المجموعات مثل (tabs) لا تضيف جزءًا |
(tabs)/stats.tsx يطابق /stats |
index لا يضيف جزءًا |
settings/index.tsx يطابق /settings |
[name] يطابق جزءًا واحدًا بالضبط |
hikes/[id].tsx يطابق /hikes/42 |
[...name] يطابق جزءًا متبقّيًا أو أكثر، بوصفها مصفوفة |
trails/[...path].tsx يطابق /trails/alps/mont-blanc |
| الثابت يتغلّب على الديناميكي، والديناميكي على الشامل | /hikes/new يختار hikes/new.tsx بدلًا من hikes/[id].tsx |
| الـ query string تصبح params إضافية | /hikes/42?units=mi يضيف units |
هذا الترتيب هو سبب قدرتك على إضافة hikes/new.tsx بجوار hikes/[id].tsx بأمان. وفي التمرين ستكتب أداة المطابقة هذه بنفسك، وهي أسرع طريقة لكي لا تفاجئك مرة أخرى أبدًا.
بهذا يكتمل التنقّل. أما القسم 4 فيصل Trailhead بالعالم الخارجي: خادم، وتخزين الجهاز، ومستشعراته.
أهم النقاط
- شاشة Stack مع
presentation: 'modal'تنزلق إلى الأعلى كورقة على iOS؛ وأغلقها باستخدامrouter.back()بعد الحفظ. - يُخفي
Stack.Protectedمعguardالمسارات حين يكون الشرط false، فلا يستطيع المستخدمون غير المسجّلين الوصول إليها، حتى من deep link. - الـ
schemeفي app.json يجعلtrailhead://hikes/42يفتح المسار/hikes/[id]؛ والمخطّطات المخصّصة تحتاج إلى development build، لا إلى Expo Go. - عند مطابقة URL، لا تضيف المجموعات ولا
indexأي جزء، والأجزاء الثابتة تتغلّب على الديناميكية، و[...rest]يلتقط كل ما تبقّى.