← الدرس السابق المستوى 4 - الدرس 9 من 12

🧭 Angular Routing: بناء تطبيق SPA متعدد الصفحات باحترافية

تقدمك في المستوى الرابع 75%

🔀 لماذا نحتاج Routing في Angular؟

في التطبيقات الحقيقية، المستخدمين محتاجين يتنقلوا بين صفحات مختلفة: الصفحة الرئيسية، صفحة المنتجات، صفحة التفاصيل، صفحة تسجيل الدخول، إلخ. في الموقع العادي، كل صفحة بتكون ملف HTML مستقل وبتتحمل من السيرفر. لكن في Single Page Application (SPA) زي Angular، الصفحة الواحدة بتتحمل مرة واحدة، والتنقل بين "الصفحات" بيحصل داخلياً من غير ما نعيد تحميل الصفحة.

Angular Routing هو النظام اللي بيدير التنقل ده. بيغير الـ URL في المتصفح، بيحمل المكون المناسب، وبيحافظ على تاريخ التنقل (Back/Forward buttons شغالة عادي). ده بيخلي التطبيق سريع وسلس زي تطبيق الموبايل.

بنهاية هذا الدرس، هتكون قادر على:

💡 مثال من الحياة العملية: تخيّل تطبيق فيسبوك. لما تدخل على صفحة شخص، الـ URL بيتغير لـ /profile/ahmed، لكن الصفحة مش بترجع تتحمل من الأول. Angular Routing بيحمل مكون ProfileComponent ويمررله معامل ahmed، وكل ده بيحصل في جزء من الثانية.

⚡ إعداد Routing الأساسي

Angular بتوفر نظام Routing مدمج في الإطار. أول خطوة هي إنشاء ملف Routing يحتوي على تعريف كل المسارات (Routes).

الخطوة 1: إنشاء ملف app-routing.module.ts

// app-routing.module.ts - تعريف كل مسارات التطبيق import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; // استيراد المكونات اللي هنستخدمها في المسارات import { HomeComponent } from './pages/home/home.component'; import { AboutComponent } from './pages/about/about.component'; import { ContactComponent } from './pages/contact/contact.component'; import { NotFoundComponent } from './pages/not-found/not-found.component'; // ✅ تعريف المسارات - ترتيبهم مهم جداً! const routes: Routes = [ { path: '', component: HomeComponent }, // الصفحة الرئيسية (/) { path: 'about', component: AboutComponent }, // /about { path: 'contact', component: ContactComponent }, // /contact { path: '**', component: NotFoundComponent } // أي مسار تاني (404) - لازم يكون آخر واحد! ]; @NgModule({ imports: [RouterModule.forRoot(routes)], // forRoot للـ Root Module exports: [RouterModule] // تصدير RouterModule عشان المكونات تستخدمه }) export class AppRoutingModule { }

⚠️ مهم جداً: ترتيب المسارات

Angular بتفحص المسارات من فوق لتحت. لو حطيت path: '**' (wildcard) في الأول، كل المسارات هتروح لـ 404. لازم يكون آخر مسار في القائمة.

الخطوة 2: تسجيل Routing Module في AppModule

// app.module.ts import { AppRoutingModule } from './app-routing.module'; @NgModule({ imports: [ // ... imports أخرى AppRoutingModule // ✅ إضافة AppRoutingModule ], // ... }) export class AppModule { }

الخطوة 3: إضافة router-outlet في القالب الرئيسي

<!-- app.component.html - القالب الرئيسي --> <header> <nav class="navbar"> <a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}"> 🏠 الرئيسية </a> <a routerLink="/about" routerLinkActive="active"> ℹ️ من نحن </a> <a routerLink="/contact" routerLinkActive="active"> 📧 التواصل </a> </nav> </header> <!-- ✅ router-outlet: هنا بيظهر المكون اللي يتوافق مع المسار --> <main> <router-outlet></router-outlet> </main> <footer> <p>© 2026 تطبيقي</p> </footer>

الخطوة 4: تنسيق الرابط النشط

/* styles.css - تنسيق الرابط النشط */ a { text-decoration: none; color: #333; padding: 10px 20px; transition: all 0.3s; } /* ✅ routerLinkActive بيضيف class "active" للرابط النشط */ a.active { color: #ff5722; font-weight: bold; border-bottom: 2px solid #ff5722; } a:hover { color: #ff5722; }
📌 الفرق بين routerLink و href:
  • <a href="/about"> = بيعيد تحميل الصفحة كاملة ❌
  • <a routerLink="/about"> = بيغير المكون بس من غير تحميل ❌
  • استخدم routerLink للتنقل الداخلي، و href للروابط الخارجية بس

🆔 المسارات الديناميكية - Dynamic Routing

في التطبيقات الحقيقية، المسارات مش ثابتة. مثلاً: /user/123، /product/iphone-15، /post/angular-routing-tutorial. المعامل المتغير (زي 123 أو iphone-15) بيتعرف بـ Route Parameters.

🔍 الخطوة 1: تعريف المسار الديناميكي

// app-routing.module.ts const routes: Routes = [ { path: '', component: HomeComponent }, // ✅ :id هو معامل متغير - Angular بتمرر قيمته للمكون { path: 'user/:id', component: UserDetailComponent }, // معاملات متعددة { path: 'product/:productId/reviews/:reviewId', component: ReviewComponent }, { path: '**', component: NotFoundComponent } ];

🔍 الخطوة 2: قراءة المعاملات في المكون

// user-detail.component.ts - قراءة معاملات المسار import { Component, OnInit, OnDestroy } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @Component({ selector: 'app-user-detail', templateUrl: './user-detail.component.html' }) export class UserDetailComponent implements OnInit, OnDestroy { userId: number = 0; user: any = {}; loading = true; private destroy$ = new Subject<void>(); // ✅ ActivatedRoute بيحتوي على معلومات المسار الحالي constructor( private route: ActivatedRoute, private userService: UserService ) {} ngOnInit() { // ✅ الاشتراك في params عشان نقرأ :id this.route.params .pipe(takeUntil(this.destroy$)) .subscribe(params => { this.userId = +params['id']; // + بيحول الـ string لـ number this.loadUser(this.userId); }); } loadUser(id: number) { this.loading = true; this.userService.getUser(id) .pipe(takeUntil(this.destroy$)) .subscribe(data => { this.user = data; this.loading = false; }); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } }

🔍 الخطوة 3: التنقل إلى مسار ديناميكي

<!-- user-list.component.html --> <div class="user-list"> <h2>👥 قائمة المستخدمين</h2> <div *ngFor="let user of users" class="user-item"> <img [src]="user.avatar" [alt]="'صورة ' + user.name"> <h3>{{ user.name }}</h3> <!-- ✅ طريقة 1: routerLink مع مصفوفة --> <a [routerLink]="['/user', user.id]" class="btn-primary"> عرض التفاصيل </a> <!-- ✅ طريقة 2: routerLink مع string interpolation --> <a [routerLink]="'/user/' + user.id" class="btn-primary"> عرض التفاصيل </a> </div> </div>
📌 لماذا نستخدم +params['id']؟

المعاملات في URL بتكون دائماً String. لو عايز number، استخدم +params['id'] (علامة + بتعمل Type Coercion) أو Number(params['id']).

🔀 التنقل البرمجي - Programmatic Navigation

أحياناً مش عايز تستخدم رابط HTML - عايز تنقل المستخدم برمجياً بناءً على منطق معين (زي بعد تسجيل الدخول الناجح، أو بعد إرسال نموذج).

// login.component.ts - تنقل برمجي بعد تسجيل الدخول import { Component } from '@angular/core'; import { Router } from '@angular/router'; @Component({ selector: 'app-login', templateUrl: './login.component.html' }) export class LoginComponent { username: string = ''; password: string = ''; error = ''; // ✅ حقن Router عشان نستخدمه للتنقل constructor( private router: Router, private authService: AuthService ) {} login() { this.authService.login(this.username, this.password) .subscribe({ next: (response) => { // ✅ تسجيل الدخول ناجح - انتقل للـ Dashboard localStorage.setItem('token', response.token); this.router.navigate(['/dashboard']); }, error: (err) => { this.error = 'اسم المستخدم أو كلمة المرور غير صحيحة'; } }); } // ✅ التنقل مع معاملات إضافية (Query Params) searchProducts(query: string) { this.router.navigate( ['/products'], { queryParams: { search: query, page: 1 } } ); // النتيجة: /products?search=iphone&page=1 } // ✅ الرجوع للصفحة السابقة goBack() { this.router.navigate(['..']); // رجوع لمستوى واحد } }

🔍 قراءة Query Parameters

// products.component.ts - قراءة ?search=iphone&page=1 export class ProductsComponent implements OnInit { searchQuery = ''; currentPage = 1; constructor(private route: ActivatedRoute) {} ngOnInit() { // ✅ قراءة Query Parameters this.route.queryParams.subscribe(params => { this.searchQuery = params['search'] || ''; this.currentPage = +params['page'] || 1; }); } }

🔒 Route Guards - حماية المسارات

Route Guards هي Services بتتحكم فيما إذا كان المستخدم يقدر يدخل مسار معين ولا لأ. بتستخدم في:

🔍 إنشاء Auth Guard

// auth.guard.ts - حماية المسارات المحمية import { Injectable } from '@angular/core'; import { CanActivate, Router, UrlTree } from '@angular/router'; import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class AuthGuard implements CanActivate { constructor( private router: Router, private authService: AuthService ) {} canActivate(): | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { // ✅ التحقق من تسجيل الدخول const isLoggedIn = this.authService.isAuthenticated(); if (isLoggedIn) { return true; // ✅ مسموح بالدخول } else { // ❌ مش مسموح - انتقل لصفحة تسجيل الدخول return this.router.createUrlTree(['/login']); } } }

🔍 استخدام Guard في Routes

// app-routing.module.ts - تطبيق الحماية على المسارات const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'login', component: LoginComponent }, // ✅ مسارات محمية - محتاجة تسجيل دخول { path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuard] }, { path: 'profile/:id', component: ProfileComponent, canActivate: [AuthGuard] }, // ✅ مسار محمي + محتاج صلاحية Admin { path: 'admin', component: AdminComponent, canActivate: [AuthGuard, AdminGuard] }, { path: '**', component: NotFoundComponent } ];
📌 أنواع Route Guards في Angular:
  • CanActivate - هل مسموح بالدخول للمسار؟
  • CanActivateChild - هل مسموح بالدخول للمسارات الفرعية؟
  • CanDeactivate - هل مسموح بمغادرة المسار؟ (تأكيد قبل الخروج)
  • Resolve - جلب بيانات قبل تحميل المسار
  • CanLoad - هل مسموح بتحميل Module (مع Lazy Loading)؟

📊 Lazy Loading - تحميل مؤجل للمسارات

في التطبيقات الكبيرة، تحميل كل المكونات في البداية بيأثر على الأداء. Lazy Loading بيسمح لك تحمل المكونات بس لما المستخدم يدخل المسار بتاعها.

// app-routing.module.ts - Lazy Loading const routes: Routes = [ { path: '', component: HomeComponent }, // Eager Loading (تحميل فوري) // ✅ Lazy Loading - الموديول بيتحمل بس لما المستخدم يدخل /admin { path: 'admin', loadChildren: () => import('./admin/admin.module') .then(m => m.AdminModule) }, // ✅ Lazy Loading للمسارات الفرعية { path: 'shop', loadChildren: () => import('./shop/shop.module') .then(m => m.ShopModule) }, { path: '**', component: NotFoundComponent } ];

🔍 هيكل Module للـ Lazy Loading

// admin/admin-routing.module.ts const routes: Routes = [ { path: '', component: AdminComponent, children: [ { path: 'users', component: AdminUsersComponent }, { path: 'products', component: AdminProductsComponent }, { path: 'orders', component: AdminOrdersComponent } ] } ]; // admin/admin.module.ts @NgModule({ declarations: [AdminComponent, AdminUsersComponent, ...], imports: [ CommonModule, AdminRoutingModule // استخدم forChild مش forRoot ] }) export class AdminModule { }
🚀 تأثير Lazy Loading على الأداء:
  • بدون Lazy Loading: التطبيق بيحمل 500KB في البداية → بطيء
  • مع Lazy Loading: التطبيق بيحمل 150KB في البداية → سريع
  • المستخدم بيحمل بس الموديول اللي يحتاجه → تجربة أفضل

⚠️ أخطاء شائعة في Routing وتجنبها

❌ الخطأ 1: وضع wildcard ** في الأول

المشكلة: كل المسارات هتروح لـ 404.

الحل: path: '**' لازم يكون آخر مسار في القائمة.

❌ الخطأ 2: استخدام href بدل routerLink

المشكلة: الصفحة بترجع تتحمل من الأول → SPA مش شغال.

الحل: استخدم routerLink للتنقل الداخلي دايماً.

❌ الخطأ 3: نسيان إلغاء الاشتراك في ActivatedRoute

المشكلة: params Observable بيفضل شغال → Memory Leak.

الحل: استخدم takeUntil أو async pipe.

🎯 أفضل الممارسات في Angular Routing

📚 دروس ذات صلة

❓ أسئلة شائعة عن Angular Routing

ما الفرق بين RouterModule.forRoot و forChild؟

forRoot بتستخدم في AppRoutingModule (الرئيسي) وبتسجل الـ Router Services مرة واحدة. forChild بتستخدم في Feature Modules (زي AdminRoutingModule) وبتسجل المسارات بس من غير ما تعيد تسجيل الـ Services. استخدام forRoot أكتر من مرة بيسبب خطأ.

هل Lazy Loading يؤثر على SEO؟

في Angular Universal (SSR)، لا. Googlebot بيقدر يفهرس المحتوى المحمل بشكل Lazy. لكن في SPA عادي، قد يكون فيه تأخير بسيط. الحل: استخدم Angular Universal (Server-Side Rendering) عشان يتحمل المحتوى على السيرفر قبل ما يوصل للمتصفح.

إزاي أعمل Redirect بعد تسجيل الدخول؟

استخدم Router.navigate(['/dashboard']) بعد ما تتحقق من صحة البيانات. لو عايز ترجع للصفحة اللي المستخدم كان عايز يدخلها قبل ما يتوجه لـ Login، خزن الـ URL في localStorage أو استخدم returnUrl parameter.

ما الفرق بين params و queryParams؟

params هي جزء من الـ URL path (زي /user/123 - 123 هو param). queryParams هي بعد علامة ? (زي /products?search=iphone&page=1). params بتستخدم للـ IDs، وqueryParams بتستخدم للفلاتر والبحث.

← الدرس السابق