اغلب برنامههای بزرگ Angular، ویژگیهای مختلف خود را به ماژولهای مجزایی تبدیل میکنند. این ماژولها شبیه به مفهوم Area در ASP.NET MVC هستند و هدف آنها نظم بخشیدن به کامپوننتهای ویژهی یک قسمت خاص از برنامه، در ناحیهای مختص به آن میباشد. به علاوه ایجاد ماژولها، قابلیت lazy loading مسیریابیها را نیز مسیر میکند. هر برنامهی Angular حداقل به همراه یک ماژول است که بر اساس قراردادی، AppModule نام گرفتهاست و در فایل src\app\app.module.ts قرار دارد. با توسعهی برنامه و بیشتر شدن قابلیتهای آن، استفادهی از این تک ماژول پیش فرض، مشکل تداخل مسئولیتها را به همراه خواهد داشت. برای رفع این مشکل توصیه شدهاست که کامپوننتهای مرتبط به یک ویژگی خاص را درون ماژولی مختص به خود قرار دهید؛ برای مثال مانند ماژول محصولات، برای نظم دهی به کامپوننتهای لیست محصولات، جزئیات محصولات، ویرایش محصولات و غیره، ماژول کاربران برای تعریف کامپوننتهای لاگین، تغییر کلمهی عبور و امثال آن. در این قسمت قصد داریم نحوهی تنظیمات مسیریابی و هدایت کاربران را به ماژولهای مختلف برنامه، بررسی کنیم.
تنظیم مسیریابی ماژولها
در اینجا نیازی به تنظیم base path نیست و این تنظیم تنها یکبار به ازای کل برنامه انجام میشود. همانطور که
در قسمت قبل نیز عنوان شد، ماژول مسیریابی Angular و یا همان RouterModule، به همراه سرویسی برای دسترسی به امکانات آن، تنظیمات مسیریابی و یک سری دایرکتیو مانند routerLink، جهت تعامل با آن است. از آنجائیکه سرویس ماژول مسیریابی در فایل src\app\app-routing.module.ts تعریف و تنظیم شدهاست، باید اطمینان حاصل کرد که این سرویس تنها یکبار در طول عمر برنامه وهله سازی میشود و از آنجائیکه هر ماژول تنظیمات مجزای مسیریابی خود را خواهد داشت، دیگر نمیتوان از متد RouterModule.forRoot سراسری استفاده کرد و در اینجا باید از متد forChild این ماژول، جهت تعریف تنظیمات مسیریابیهای ماژولهای مختلف کمک گرفت. متد forChild نیز شبیه به همان آرایهی تنظیمات مسیریابی متد forRoot را دریافت میکند.
یک مثال: در ادامهی مثالی که در قسمت قبل به کمک
Angular CLI ایجاد کردیم، ماژول جدید محصولات را به همراه تنظیمات ابتدایی مسیریابی آن ایجاد میکنیم:
>ng g m product --routing
پس از اجرای این دستور، ماژول جدید محصولات در فایل src\app\product\product.module.ts و تنظیمات ابتدایی آن در فایل src\app\product\product-routing.module.ts تشکیل میشوند:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ProductRoutingModule { }
همانطور که مشاهده میکنید، در حین تشکیل تنظیمات ابتدایی مسیریابی این ماژول جدید، اینبار از متد forChild استفاده شدهاست و نه متد forRoot که مختص به ماژول اصلی و سراسری برنامهاست.
سپس ProductRoutingModule به قسمت imports ماژول محصولات به صورت خودکار اضافه شدهاست:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProductRoutingModule } from './product-routing.module';
@NgModule({
imports: [
CommonModule,
ProductRoutingModule
],
declarations: []
})
export class ProductModule { }
در ادامه کامپوننت جدید لیست محصولات را به این ماژول اضافه میکنیم:
>ng g c product/ProductList
به این ترتیب داخل پوشهای به نام produc-list، کامپوننت product-list.component.ts تشکیل خواهد شد. در حقیقت این دستور اعمال ذیل را انجام میدهد:
installing component
create src\app\product\product-list\product-list.component.css
create src\app\product\product-list\product-list.component.html
create src\app\product\product-list\product-list.component.spec.ts
create src\app\product\product-list\product-list.component.ts
update src\app\product\product.module.ts
اگر به سطر آخر آن دقت کنید، کار به روز رسانی فایل ماژول محصولات، جهت درج این کامپوننت جدید، در قسمت declarations فایل product.module.ts نیز به صورت خودکار انجام شدهاست:
import { ProductListComponent } from './product-list/product-list.component';
@NgModule({
imports: [
],
declarations: [ProductListComponent]
})
export class ProductModule { }
اکنون که این ماژول جدید را به همراه یک کامپوننت نمونه در آن تعریف کردیم، برای افزودن مسیریابی به آن، به فایل src\app\product\product-routing.module.ts مراجعه کرده و آرایهی Routes آنرا تکمیل میکنیم:
import { ProductListComponent } from './product-list/product-list.component';
const routes: Routes = [
{ path: 'products', component: ProductListComponent }
];
ابتدا کامپوننت لیست محصولات import شده و سپس آرایهی Routes مسیری را به این کامپوننت تعریف کردهایم.
در ادامه میخواهیم لینکی را به این مسیریابی جدید اضافه کنیم.
در قسمت قبل منویی را به برنامه اضافه کردیم. به همین جهت به فایل src\app\app.component.html مراجعه کرده و routerLink جدیدی را به آن اضافه میکنیم:
<nav class="navbar navbar-default">
<div class="container-fluid">
<a class="navbar-brand">{{title}}</a>
<ul class="nav navbar-nav">
<li>
<a [routerLink]="['/home']">Home</a>
</li>
<li>
<a [routerLink]="['/products']">Product List</a>
</li>
</ul>
</div>
</nav>
<div class="container">
<router-outlet></router-outlet>
</div>
پیشتر لینکی را به کامپوننت welcome در قسمت قبل اضافه کرده بودیم. در اینجا لینک جدیدی را به کامپوننت لیست محصولات، در ذیل آن تعریف کردهایم.
در اینجا نیز نحوهی تعریف لینکها مانند قبل است و آرایهی تنظیمات پارامترهای لینک باید به مقدار خاصیت path تعریف شده اشاره کند.
اکنون دستور ng serve -o را صادر کنید تا برنامه در حافظه ساخته شده و در مرورگر نمایش داده شود. در ادامه اگر بر روی لینک لیست محصولات کلیک کنید، صفحهی ذیل را مشاهده خواهید کرد:
به این معنا که برنامه اطلاعی از این مسیریابی جدید نداشته و صفحهی یافت نشدن مسیریابی را که در قسمت قبل تنظیم کردیم، نمایش دادهاست. برای رفع این مشکل باید به فایل src\app\app.module.ts مراجعه کرده و این ماژول جدید را به آن معرفی کنیم:
import { ProductModule } from './product/product.module';
@NgModule({
declarations: [
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
ProductModule,
AppRoutingModule
],
در اینجا در ابتدا ماژول محصولات import شده و سپس به قسمت لیست imports ماژول App اضافه گردیدهاست. اکنون مسیریابی به این کامپوننت جدید واقع شدهی در قسمت ماژول محصولات، کار میکند:
نکته 1: علت اینکه ProductModule را پیش از AppRoutingModule تعریف کردیم این است که AppRoutingModule دارای تعریف مسیریابی ** یا catch all است که در قسمت قبل آنرا جهت مدیریت مسیرهای یافت نشده به برنامه افزودیم. اگر ابتدا AppRoutingModule تعریف میشد و سپس ProductModule، هیچگاه فرصت به پردازش مسیریابیهای ماژول محصولات نمیرسید؛ چون مسیر ** پیشتر برنده شده بود.
نکته 2: میتوان در قسمت import متد RouterModule.forRoot را نیز مستقیما قرار داد (بجای AppRoutingModule). اگر این کار صورت گیرد، ابتدا مسیریابیهای موجود در ماژولها پردازش میشوند و در آخر مسیرهای موجود در RouterModule.forRoot صرفنظر از محل قرارگیری آن در این لیست بررسی خواهد شد (حتی اگر در ابتدای لیست قرار گیرد). هرچند جهت مدیریت بهتر برنامه، این متد به AppRoutingModule منتقل شدهاست. بنابراین اکنون «نکتهی 1» برقرار است.
انتخاب استراتژی مناسب نامگذاری مسیرها
هنگام کار کردن با تعدادی ویژگی مرتبط به هم قرار گرفتهی داخل یک ماژول، بهتر است روش نامگذاری مناسبی را برای تنظیمات مسیریابی آن درنظر گرفت تا مسیرهای تعیین شده علاوه بر زیبایی، وضوح بیشتری را نیز پیدا کنند. به علاوه این نامگذاری مناسب، گروه بندی مسیریابیها و lazy loading آنها را نیز سادهتر میکند.
استراتژی ابتدایی که به ذهن میرسد، نامگذاری هر مسیر بر اساس عملکرد آنها است مانند products برای نمایش لیست محصولات، product/:id برای نمایش جزئیات محصولی خاص که در اینجا id پارامتر مسیریابی است و productEdit/:id برای ویرایش جزئیات یک محصول مشخص. همانطور که مشاهده میکنید، هرچند این مسیرها متعلق به یک ماژول هستند، اما مسیرهای تعیین شدهی برای آنها اینگونه به نظر نمیرسد. بنابراین بهتر است تمام ویژگیهای قرار گرفتهی درون یک ماژول را با مسیر ریشهی یکسانی شروع کنیم. به این ترتیب نمایش لیست محصولات همان products باقی خواهد ماند اما برای نمایش جزئیات محصولی خاص از مسیر products/:id استفاده میکنیم (همان اسم جمع ریشهی مسیر؛ بجای اسم مفرد). اینبار مسیر ویرایش جزئیات یک محصول به صورت products/:id/edit تنظیم خواهد شد:
products
products/:id
products/:id/edit
در اینجا نام ریشهی یکسانی را برای عناصر مختلف قرارگرفتهی درون یک ماژول انتخاب کردهایم؛ تا ارتباط بین آنها بهتر مشخص شود و همچنین در آینده بتوان مباحث گروه بندی و lazy loading را نیز بر این اساس پیاده سازی کرد.
فعالسازی یک مسیر با کدنویسی
تا اینجا نحوهی فعالسازی یک مسیر را با استفاده از دایرکتیو routerLink بررسی کردیم. اما گاهی از اوقات نیاز است تا بتوان با کدنویسی نیز کاربران را به مسیری خاص هدایت کرد. برای مثال پس از عملیات logout میخواهیم مجددا صفحهی اول سایت نمایش داده شود. برای اینکار از سرویس Router مسیریاب Angular کمک گرفته میشود. ابتدا آنرا در سازندهی یک کامپوننت تزریق کرده و سپس میتوان به قابلیتهای آن مانند استفادهی از متد navigate آن، در کدهای برنامه دسترسی یافت.
باید درنظر داشت که دایرکتیو routerLink نیز در پشت صحنه از همین متد navigate سرویس Router استفاده میکند. بنابراین تمام پارامترهای آن در متد navigate نیز قابل استفاده هستند. برای مثال زمانیکه تعداد پارامترهای routerLink یک مورد است، میتوان آرایهی آنرا به یک رشته خلاصه کرد. یک چنین قابلیتی با متد navigate نیز میسر است.
متد navigate تنها قسمتهایی از URL جاری را تغییر میدهد. اگر نیاز باشد تا کل آدرس تعویض شود، میتوان از متد دیگر سرویس Router به نام navigateByUrl استفاده کرد. این متد تمام URL segments موجود را با مسیر جدیدی جایگزین میکند. به علاوه برخلاف متد navigate، تنها یک رشته را به عنوان پارامتر میپذیرد.
در ادامه مثال جاری میخواهیم پیاده سازی ابتدایی login و logout را به برنامه اضافه کنیم. به همین منظور ابتدا ماژول جدید user را به همراه تنظیمات ابتدایی مسیریابی آن اضافه میکنیم:
به این ترتیب دو فایل src\app\user\user-routing.module.ts و src\app\user\user.module.ts به برنامه اضافه میشوند.
همانند ماژول قبلی، نیاز است UserModule را به قسمت imports فایل src\app\app.module.ts نیز معرفی کنیم:
import { UserModule } from './user/user.module';
@NgModule({
declarations: [
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
ProductModule,
UserModule,
AppRoutingModule
],
سپس کامپوننت جدید لاگین را به ماژول user برنامه اضافه میکنیم:
که اینکار سبب به روز رسانی فایل user.module.ts جهت تکمیل قسمت declarations آن با LoginComponent نیز میشود.
در ادامه به فایل src\app\user\user-routing.module.ts مراجعه کرده و مسیریابی جدیدی را به کامپوننت لاگین تعریف میکنیم:
import { LoginComponent } from './login/login.component';
const routes: Routes = [
{ path: 'login', component: LoginComponent}
];
ابتدا این کامپوننت import شده و سپس یک path جدید را به آن انتساب میدهیم.
مرحلهی بعد، فعالسازی این مسیریابی است، با تعریف لینکی به آن. به همین جهت به فایل src\app\app.component.html مراجعه کرده و منوی برنامه را تکمیل میکنیم:
<nav class="navbar navbar-default">
<div class="container-fluid">
<a class="navbar-brand">{{title}}</a>
<ul class="nav navbar-nav">
<li>
<a [routerLink]="['/home']">Home</a>
</li>
<li>
<a [routerLink]="['/products']">Product List</a>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li>
<a [routerLink]="['/login']">Log In</a>
</li>
</ul>
</div>
</nav>
<div class="container">
<router-outlet></router-outlet>
</div>
اکنون دستور ng serve -o را صادر کنید تا برنامه در حافظه ساخته شده و در مرورگر نمایش داده شود و سپس بر روی لینک login کلیک کنید تا قالب ابتدایی آن نمایش داده شود:
تکمیل کامپوننت login و افزودن لینک logout
در ادامه میخواهیم یک فرم لاگین مقدماتی را پس از کلیک بر روی لینک لاگین نمایش دهیم و هدایت به صفحهی لیست محصولات را پس از لاگین و مخفی کردن لینک لاگین و نمایش لینک خروج را در این حالت پیاده سازی کنیم. برای این منظور ابتدا اینترفیس خالی کاربر را ایجاد میکنیم:
که سبب ایجاد فایل src\app\user\user.ts خواهد شد. این اینترفیس را به صورت زیر تکمیل میکنیم:
export interface IUser {
id: number;
userName: string;
isAdmin: boolean;
}
پس از آن یک سرویس ابتدایی اعتبارسنجی کاربران را نیز اضافه خواهیم کرد:
>ng g s user/auth -m user/user.module
که سبب افزوده شدن سرویس auth.service.ts و همچنین به روز رسانی خودکار قسمت providers ماژول user.module.ts نیز میشود:
installing service
create src\app\user\auth.service.spec.ts
create src\app\user\auth.service.ts
update src\app\user\user.module.ts
اگر نام ماژول را ذکر نکنیم، سرویس مدنظر تولید خواهد شد، اما قسمت providers هیچ ماژولی به صورت خودکار تکمیل نمیشود.
پس از ایجاد قالب ابتدایی فایل auth.service.ts آنرا به نحو ذیل تکمیل کنید:
import { IUser } from './user';
import { Injectable } from '@angular/core';
@Injectable()
export class AuthService {
currentUser: IUser;
constructor() { }
isLoggedIn(): boolean {
return !this.currentUser;
}
login(userName: string, password: string): boolean {
if (!userName || !password) {
return false;
}
if (userName === 'admin') {
this.currentUser = {
id: 1,
userName: userName,
isAdmin: true
};
return true;
}
this.currentUser = {
id: 2,
userName: userName,
isAdmin: false
};
return true;
}
logout(): void {
this.currentUser = null;
}
}
در اینجا اگر کاربر هر نوع کلمهی عبور و یا نام کاربری را وارد کند، به سیستم وارد خواهد شد. اگر نامش admin باشد، دسترسی admin پیدا میکند و همچنین متدهای logout با null کردن یوزر وارد شدهی به سیستم و isLoggedIn برای مشخص بودن نال نبودن شیء کاربر جاری، به این سرویس اضافه شدهاند.
سپس کامپوننت لاگین واقع در فایل src\app\user\login\login.component.ts را به نحو ذیل تکمیل کنید:
import { Router } from '@angular/router';
import { AuthService } from './../auth.service';
import { Component, OnInit } from '@angular/core';
import { NgForm } from '@angular/forms';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
errorMessage: string;
pageTitle = 'Log In';
constructor(private authService: AuthService,
private router: Router) { }
ngOnInit() {
}
login(loginForm: NgForm) {
if (loginForm && loginForm.valid) {
let userName = loginForm.form.value.userName;
let password = loginForm.form.value.password;
if (this.authService.login(userName, password)) {
this.router.navigate(['/products']);
}
} else {
this.errorMessage = 'Please enter a user name and password.';
};
}
}
در این کامپوننت دو سرویس AuthService، که پیشتر ایجاد کردیم و سرویس Router، به سازندهی کلاس تزریق شدهاند.
از AuthService برای اعتبارسنجی کاربر و لاگین او به سیستم استفاده میکنیم و از سرویس مسیریاب Angular جهت فراخوانی متد navigate آن به صفحهی مشاهدهی محصولات، پس از لاگین کاربر استفاده شدهاست.
اکنون میخواهیم قالب این کامپوننت را نیز تکمیل کنیم. پیش از آن به فایل src\app\user\user.module.ts مراجعه کرده و در قسمت imports آن FormsModule را نیز اضافه کنید:
import { FormsModule } from '@angular/forms';
@NgModule({
imports: [
CommonModule,
FormsModule,
UserRoutingModule
],
سپس فایل src\app\user\login\login.component.html را به نحو ذیل تغییر دهید:
<div class="panel panel-default">
<div class="panel-heading">
{{pageTitle}}
</div>
<div class="panel-body">
<form class="form-horizontal" novalidate (ngSubmit)="login(loginForm)" #loginForm="ngForm" autocomplete="off">
<fieldset>
<div class="form-group" [ngClass]="{'has-error': (userNameVar.touched ||
userNameVar.dirty) &&
!userNameVar.valid }">
<label class="col-md-2 control-label" for="userNameId">User Name</label>
<div class="col-md-8">
<input class="form-control" id="userNameId" type="text"
placeholder="User Name (required)" required
(ngModel)="userName" name="userName" #userNameVar="ngModel" />
<span class="help-block" *ngIf="(userNameVar.touched ||
userNameVar.dirty) &&
userNameVar.errors">
<span *ngIf="userNameVar.errors.required">
User name is required.
</span>
</span>
</div>
</div>
<div class="form-group" [ngClass]="{'has-error': (passwordVar.touched ||
passwordVar.dirty) &&
!passwordVar.valid }">
<label class="col-md-2 control-label" for="passwordId">Password</label>
<div class="col-md-8">
<input class="form-control" id="passwordId" type="password"
placeholder="Password (required)" required
(ngModel)="password" name="password" #passwordVar="ngModel" />
<span class="help-block" *ngIf="(passwordVar.touched ||
passwordVar.dirty) &&
passwordVar.errors">
<span *ngIf="passwordVar.errors.required">
Password is required.
</span>
</span>
</div>
</div>
<div class="form-group">
<div class="col-md-4 col-md-offset-2">
<span>
<button class="btn btn-primary"
type="submit"
style="width:80px;margin-right:10px"
[disabled]="!loginForm.valid">
Log In
</button>
</span>
<span>
<a class="btn btn-default"
[routerLink]="['/welcome']">
Cancel
</a>
</span>
</div>
</div>
</fieldset>
</form>
<div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div>
</div>
</div>
تا اینجا صفحهی لاگین نمایش داده شده و کاربر میتواند به سیستم وارد شود. تا زمانیکه کلمهی عبور و نام کاربری وارد نشده باشند، دکمهی login این فرم غیرفعال باقی میماند. پس از آن کاربر با هر نوع ترکیبی از کلمهی عبور و نام کاربری میتواند به سیستم وارد شده و بلافاصله به صفحهی لیست محصولات هدایت میشود.
اکنون میخواهیم پس از ورود او، نام او را نمایش داده و همچنین دکمهی logout را بجای login در منوی بالای سایت نمایش دهیم. به همین جهت در قالب کامپوننت App که منوی برنامه در آن تنظیم شدهاست، نیاز است بتوانیم به سرویس Auth سفارشی دسترسی یافته و خروجی متد isLoggedIn آنرا بررسی کنیم. به همین منظور به فایل src\app\app.component.ts مراجعه کرده و آنرا به صورت ذیل تکمیل کنید:
import { Router } from '@angular/router';
import { AuthService } from './user/auth.service';
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
pageTitle: string = 'Routing Lab';
constructor(private authService: AuthService,
private router: Router) { }
logOut(): void {
this.authService.logout();
this.router.navigateByUrl('/welcome');
}
}
در اینجا دو سرویس Auth و Router به سازندهی کامپوننت App تزریق شدهاند. به این ترتیب میتوان از شیء authService در قالب این کامپوننت برای دسترسی به متد isLoggedIn و سایر خواص این سرویس استفاده کرد. همچنین از سرویس مسیریاب Angular برای پیاده سازی logout و هدایت کاربر به صفحهی welcome کمک گرفتهایم. در اینجا از متد navigateByUrl استفاده شدهاست؛ از این جهت که پس از Logout دیگر حفظ URL Segments موجود بیمفهوم است و تمام آنها باید پاک شده و با آدرس جدید جایگزین شوند.
پس از این تغییرات، اکنون میتوان قالب src\app\app.component.html را به نحو ذیل تکمیل کرد:
<nav class="navbar navbar-default">
<div class="container-fluid">
<a class="navbar-brand">{{title}}</a>
<ul class="nav navbar-nav">
<li>
<a [routerLink]="['/home']">Home</a>
</li>
<li>
<a [routerLink]="['/products']">Product List</a>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li *ngIf="authService.isLoggedIn()">
<a>Welcome {{ authService.currentUser.userName }}</a>
</li>
<li *ngIf="!authService.isLoggedIn()">
<a [routerLink]="['/login']">Log In</a>
</li>
<li *ngIf="authService.isLoggedIn()">
<a (click)="logOut()">Log Out</a>
</li>
</ul>
</div>
</nav>
<div class="container">
<router-outlet></router-outlet>
</div>
همانطور که مشاهده میکنید، دوبار لاگین بودن کاربر جاری توسط متد authService.isLoggedIn بررسی شدهاست. اگر خروجی این متد true باشد، نام کاربری شخص به همراه لینک Logout نمایش داده میشود. اگر خروجی این متد false باشد، تنها لینک login نمایان شده و مابقی گزینهها (لینک لاگین و نمایش نام شخص) از صفحه حذف میشوند.
اکنون اگر برنامه را توسط دستور ng serve -o اجرا کنید، صفحهی لاگین و منوی بالای صفحه چنین شکلی را خواهد داشت:
پس از لاگین، لینک لاگین از منو حذف شده و سپس نام کاربری و لینک به logout نمایان میشوند.
اینبار اگر بر روی logout کلیک کنید، نام کاربری و لینک logout از صفحه حذف و مجددا لینک لاگین نمایش داده میشود.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-01.zip
برای اجرای آن فرض بر این است که پیشتر
Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.