Angular2 - ראוטינג




המקור להסברים כאן הוא מהאתר הרישמי: https://angular.io/docs/ts/latest/guide/router.html

ברוב המימושים של routing הדבר הראשון שצריך לעשות זה לקבוע את נקודת הבסיס לניווט בין דפי האתר. כדי להגדיר את הנקודה הזו משתמשים בתג <base> בתוך ה-index.html. אם ספריית app היא ספריית הבסיס באתר אז הגדרת נקודת הבסיס תהיה:


<base href="/">


הראוטר של אנגולר הוא לא חלק מה-core, אלא רכיב נפרד שאפשר להשתמש בו אבל לא חובה. הוא נמצא בספריה נפרדת באנגולר ואם רוצים להשתמש בו צריך לייבא אותו. כמו כן לא חובה לייבא את כולו. אפשר לייבא רק מה שרוצים מתוכו. לדוגמה:


import { RouterModule, Routes } from '@angular/router';

השימוש בראוטר הוא בצורת singleton. 
כאשר ה-URL משתנה הראוטר מחפש את הנתיב (route) המתאים כדי לקבוע איזה component להציג. בהתחלה אין לראוטר נתיבים בכלל עד שמגדירים לו מהם הנתיבים שלו.

הגדרת routes

כדי לקנפג את ה-router יוצרים מערך של routes. כל route מוגדר מ-path ו-component.
ה-path זה חלק מה-URL. וה-component הוא הרכיב שיוצג. 
דוגמה למערך של routes:


const appRoutes: Routes = [
  { path: 'first-page', component: FirstPageComponent },
  { path: 'second-page', component: SecondPageComponent },
];



Router outlet

המיקום שבו הראוטר יציג את ה-component הנבחר יהיה לאחר התג:
 <router-outlet></router-outlet> 

שנמצא ב-HTML של ה-host view של האתר.


Router links

עד כאן יש לנו טבלת routes ומיקום שאליו הראוטר מרנדר את ה-component. ה-URL יכול להגיע ישירות משורת הכתובת של ה-browser אבל רוב הפעמים הניווט באתר מתבצע ע"י לחיצות של המשתמש על לינקים.
נשתמש ב-template הבא כדי להסביר:


template: `
  <h1>Angular Router</h1>
  <nav>
    <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
    <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
  </nav>
  <router-outlet></router-outlet>
`

ה-routerLink, מאפשר לראוטר לשלוט על האלמנטים האלה ומציין לאיזה path ללכת כשלוחצים על הלינק.
ה-routerLinkActive, משייך את האלמנט הנוכחי ל-class שנקרא active ברגע שה-routerLink הופך להיות active, או במילים אחרות ברגע שלוחצים על הלינק הצבע של האלמנט משתנה כך שניתן לראות על מה לחצנו.


Router state

כל פעם שהראוטר מבצע route הוא בונה עץ של אובייקטים מסוג ActivatedRoute שמייצג את המצב הנוכחי של הראוטר. ניתן לגשת לסטאטוס של הראוטר מכל מקום בקוד ע"י ה-router service וה-routerState property. לכל אובייקט ActivatedRoute שב-RouterState יש פונקציות שמאפשרות גישה ל-route הבא, הקודם וגם ל-route צדדי.

ה-component הראשי - AppComponent 

ה-AppComponent הוא השלד של האפליקציה. יהיו בו חלקים קבועים לכל האתר וחלקים שמשתנים לפי הדף המוצג.
בד"כ יש לו title וגם navbar ו-footer שיופיעו בכל הדפים באתר. ויש את החלק שמשתנה לפי הדף המוצג. החלק הזה מגיע מה-router ומסומן ב-<router-outlet></router-outlet>.
דוגמה של template של AppComponent:

app.component.ts

template: `
  <h1>Angular Router</h1>
  <nav>
    <a routerLink="/first-page" routerLinkActive="active">First Page</a>
    <a routerLink="/second-page" routerLinkActive="active">Second Page</a>
  </nav>
  <router-outlet></router-outlet>
`

ב-template הזה ניתן לראות את ה-directive שנקרא routerLinkActive. מה שיש בתוך הגרשיים זה מחלקה של CSS. ה-router יוסיף את המחלקה הזו ברגע שהלינק הזה מופעל ויסיר את המחלקה הזו כשהלינק לא מופעל.
ה-router מוסיף את המחלקה לפי המצב ב-RouterState. וכיון שה-RouterState בונה עץ של אובייקטים (router tree) יתכן שיהיו אבא ובן ששניהם מופעלים. 

אם לא רוצים שלשניהם תתווסף המחלקה אז צריך לעשות binding בין [routerLinkActiveOptions] ל-{ exact: true }. ואז רק הדף שה-URL המדויק שלו פעיל יסומן כפעיל.

מימוש הראוטר

ישנם שלוש אפשרויות בהגדרת הראוטר:

  1. כל הראוטר מוגדר בתוך ה-module הראשי (שנקרא בד"כ AppModule)
  2. כל הראוטר מוגדר ב-module נפרד משלו (שנקרא בד"כ AppRoutingModule)
  3. הראוטר מפוצל ומוגדר במספר modules. כל feature module מגדיר חלק מהראוטר לפי מה שהוא צריך
האפשרות הראשונה מתאימה לאתרים שדורשים ראוטינג פשוט.
האפשרות השניה מתאימה לאתרים יותר מורכבים שדורשים ראוטינג יותר מורכב ושימוש משמעותי יותר באפשרויות שהראוטר של Angular מציע.
האפשרות השלישית גם היא נועדה לאתרים שדורשים ראוטינג מורכב ויש לה כמה יתרונות.
נסביר כל אפשרות בצורה מפורטת.

אפשרות #1 - הראוטר מוגדר בתוך ה-module הראשי

כאשר מכניסים את ה-RouterModule ל-module הראשי (AppModule) אז ה-router זמין בכל האפליקציה.

בדוגמה הבאה מגדירים ארבעה נתיבים. מקנפגים את הראוטר ע"י שימוש בפונקציה-RouterModule.forRoot וההחזר של הפונקציה נכנס למערך ה-imports של ה-AppModule.

app.module.ts

const appRoutes: Routes = [
  { path: 'crisis-center', component: CrisisListComponent },
  { path: 'hero/:id',      component: HeroDetailComponent },
  { path: 'heroes',
    component: HeroListComponent,
    data: { title: 'Heroes List' }
  },
  { path: '',
    redirectTo: '/heroes',
    pathMatch: 'full'
  },
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [
    RouterModule.forRoot(appRoutes)
    // other imports here
  ],
  ...
})
export class AppModule { }


  • כל route ממפה URL ל-component. הראוטר בונה את ה-URL הסופי בשבילנו ומאפשר לנו להשתמש בכתובות מדויקות ויחסיות כדי לנווט בין views. 
  • ה-"id:" שב-route השני מייצג העברת פרמטר לדוגמה hero/3. ה-3 הוא הערך שנשלח בפרמטר id וה-component שישתמש בו הוא HeroDetailComponent כדי להציג את הגיבור שה-id שלו הוא 3.
  • ב-route השלישי יש property שנקרא data. ה-property הזה נועד לשמור מידע שנצרך ל-route השלישי. ניתן להשתמש ב-data property בכל route לפי הצורך כדי לשמור בו מידע סטאטי (read only).
  • ב-route הרביעי יש path ששווה למחרוזת ריקה, וזה מה שמגדיר את ה-default route של האתר. לשם יפנה האתר ברגע שה-path ריק שזה בד"כ המצב בהתחלה. בדוגמה הזו ה-default route מפנה ל-heroes/ ולכן הוא יציג את HerosListComponent.
  • ב-route הרביעי משתמשים ב-redirectTo. כשמשתמשים ב-redirectTo חייבים להגדיר את ה-property שנקרא pathMatch. ה-property הזה קובע באיזה URL יתבצע redirect. במקרה שלנו הוא מוגדר כ-'full', מה שאומר שרק כשקטע ה-URL שמעבר לבסיס (localhost:3000) יהיה שווה ל-' ', רק אז יתבצע redirect ל-heroes. ה-pathMatch יכול גם לקבל 'prefix', והמשמעות היא שאם קטע ה-URL שמעבר לבסיס מתחיל במה שכתוב ב-path כבר אז יתבצע redirect. במקרה הנ"ל אם נשתמש ב-'prefix' כל URL יתאים ותמיד יתבצע redirect, ואת זה כמובן אנחנו לא רוצים.
  • ב-path האחרון יש "**" שזה wildcard. הראוטר ישתמש ב-route הזה במקרה שה-URL לא תאם לאף אחד משאר האפשרויות. במקרה הזה ניתן להציג דף של שגיאת 404 או להפנות לדף אחר כגון דף הבית.
  • יש חשיבות לסדר של ה-routes. ה-router משתמש ב-route הראשון שמתאים, ולכן צריך למקם route שיותר מפורט לפני route כללי. זו הסיבה שבדוגמה לעיל ה-wildcard route ממוקם אחרון כיון שהוא מתאים לכל URL וצריך שהוא יבחר רק אם כל שאר האפשרויות לא מתאימות. אם נמקם אותו ראשון הוא יבחר תמיד.
אפשרות #2 - כל הראוטר מוגדר ב-module נפרד משלו

מימוש ה-router כמו באפשרות #1 מתאים לאפליקציות פשוטות. באפליקציות מורכבות יש צורך ב-Module נפרד שיטפל ב-routing. השימוש ב-router module נפרד יפריד את ענייני ה-routing מענייני האפליקציה. בנוסף אם יש צורך לשנות routing בשביל בדיקות צריך להחליף רק את ה-module הזה בלבד. הקובץ app-routing.module.ts יראה כך:

app-routing.module.ts

import { NgModule }              from '@angular/core';
import { RouterModule, Routes }  from '@angular/router';
import { FirstComponent }   from './first.component';
import { SecondComponent }     from './second.component';
import { PageNotFoundComponent } from './not-found.component';
const appRoutes: Routes = [
  { path: 'first-component',  component: FirstComponent },
  { path: 'second-component', component: SecondComponent },
  { path: '',   redirectTo: '/first-component', pathMatch: 'full' },  
  { path: '**', component: PageNotFoundComponent }
];
@NgModule({
  imports: [
    RouterModule.forRoot(appRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class AppRoutingModule {}

וה-module הראשי app.module.ts יראה כך:

app.module.ts

import { NgModule }       from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
import { FormsModule }    from '@angular/forms';
import { AppComponent }          from './app.component';
import { AppRoutingModule }      from './app-routing.module';
import { FirstComponent }        from './first.component';
import { SecondComponent }       from './second.component';
import { PageNotFoundComponent } from './not-found.component';
@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    FirstComponent ,
    SecondComponent ,
    PageNotFoundComponent
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

שימו לב שבקובץ app-routing.module.ts אנחנו עושים export ל-RouterModule של Angular, ובקובץ app.module.ts אנחנו עושים import ל-AppRoutingModule שלנו. בדרך הזו לכל ה-components שמוגדרים ב-AppModule תהיה גישה ל-directives של הראוטר של Angular. כמו לדוגמה RouterLink ו-RouterOutlet.
שימו לב שבאפשרות #2 ה-imports שב-app.module.ts השתנה ובמקום RouterModule.forRoot שהיה בו, יש בו עכשיו AppRoutingModule.

אפשרות #3 - הראוטר מפוצל ומוגדר במספר modules, כל feature module מגדיר חלק מהראוטר

את ה-module של הראוטר (בדוגמה הזו heroes-routing.module.ts) כדאי לשים באותה ספריה של ה-module שהוא משתייך אליו (heroes.module.ts).
בכל module שמגדיר חלק מהראוטר יהיה מערך של routes שרלוונטים לגבי ה-module הנוכחי. את המערך הזה נכניס לתוך ה-imports. אבל בשונה משני האפשרויות הראשונות, הפעם נשתמש בפונקציה forChild (ולא forRoot). השימוש ב-forRoot יהיה רק בראוטר שמוגדר במודול הראשי (אפשרות #1) או במודול של ראוטר ראשי (אפשרות #2).
בנוסף צריך להכניס למערך ה-exports את ה-RouterModule ולייצא את המודול של הראוטר הנוכחי (HeroRoutingModule).


heroes-routing.module.ts

import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HeroListComponent }    from './hero-list.component';
import { HeroDetailComponent }  from './hero-detail.component';
const heroesRoutes: Routes = [
  { path: 'heroes',  component: HeroListComponent },
  { path: 'hero/:id', component: HeroDetailComponent }
];
@NgModule({
  imports: [
    RouterModule.forChild(heroesRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class HeroRoutingModule { }

לאחר מכן צריך לייבא את הראוטר החדש (heroes-routing.module.ts) לתוך המודול שאליו הוא שייך (heroes.module.ts). נוסיף שורה של import בתחילת הקובץ ונוסיף אותו גם לתוך ה-imports.


heroes.module.ts

import { NgModule }       from '@angular/core';
import { CommonModule }   from '@angular/common';
import { FormsModule }    from '@angular/forms';
import { HeroListComponent }    from './hero-list.component';
import { HeroDetailComponent }  from './hero-detail.component';
import { HeroService } from './hero.service';
import { HeroRoutingModule } from './heroes-routing.module';
@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    HeroRoutingModule
  ],
  declarations: [
    HeroListComponent,
    HeroDetailComponent
  ],
  providers: [ HeroService ]
})
export class HeroesModule {}

את המודול heroes.module.ts מייבאים לתוך המודול הראשי, AppModule, וכך המודול וכל הראוטינג שלו יהיו זמינים בכל האפליקציה. גם כאן היבוא מתבצע בשני תוספות, א - הוספת שורת import בתחילת הקובץ, שמייבא את המודול מתוך הקובץ בו הוא מוגדר. ב - הוספה של המודול לתוך מערך ה-imports.


4 תגובות: