Guards and Resolvers (Phần 1)
- 14-01-2024
- Toanngo92
- 0 Comments
Mục lục
Angular Router Navigation Cycle
Angular Router thực hiện năm hoạt động cơ bản khi nhận một URL mới:
- Thực Hiện Redirects:
- Trong trường hợp có các redirection được cấu hình, Angular Router sẽ áp dụng chúng. Điều này giúp chuyển hướng người dùng từ một URL đến URL khác theo các quy tắc được đặt ra trước.
- Nhận Biết Trạng Thái Router:
- Router nhận biết các trạng thái của nó dựa trên URL được cung cấp. Điều này giúp nó xác định đâu là trạng thái hiện tại của ứng dụng.
- Chạy Guards và Resolve Data:
- Trước khi chuyển đến trạng thái mới, Router chạy các guards (bảo vệ) và giải quyết dữ liệu liên quan. Guards có thể kiểm tra điều kiện trước khi cho phép điều hướng diễn ra, trong khi Resolve giúp tải dữ liệu trước khi route được kích hoạt.
- Kích Hoạt Tất Cả Các Component Cần Thiết:
- Sau khi hoàn tất các bước trước đó, Router kích hoạt tất cả các component liên quan đến trạng thái mới. Các component này sẽ hiển thị và tham gia vào giao diện người dùng.
- Quản Lý Điều Hướng:
- Router quản lý quá trình điều hướng, giữ cho ứng dụng di chuyển mượt mà giữa các trạng thái và component khác nhau.
Để minh họa, chúng ta sử dụng cấu hình routing trong ứng dụng như sau:
app-routing.module.ts:
const routes: Routes = [
{
path: "admin",
loadChildren: () =>
import("./admin/admin.module").then((m) => m.AdminModule),
},
{
path: "",
redirectTo: "article",
pathMatch: "full",
},
];
article-routing.module.ts:
const routes: Routes = [
{
path: "article",
component: ArticleComponent,
children: [
{
path: "",
component: ArticleListComponent,
},
{
path: ":slug",
component: ArticleDetailComponent,
},
],
},
];
admin-routing.module.ts:
const routes: Routes = [
{
path: "",
component: AdminComponent,
children: [
{
path: "",
component: AdminArticleListComponent,
},
],
},
];
Cấu hình này định nghĩa các route cho ứng dụng, bao gồm các module lazy-loaded như AdminModule, và quy tắc chuyển hướng khi URL trống được chuyển đến “/article”.
Navigation
Quá trình đầu tiên trong Angular Router gọi là Navigation hoặc Áp dụng chuyển hướng (Applies redirects).
Với thẻ a href
thông thường, hành vi mặc định là gửi yêu cầu đến URL được chỉ định. Tuy nhiên, Angular Router cung cấp một directive là routerLink
để thay thế hành vi đó. Directive này mang tính chất declarative, cung cấp một cách khai báo để thực hiện navigation. Ngoài ra, chúng ta cũng có các cách imperative như sử dụng Router.navigate()
hoặc Router.navigateByUrl()
.
Navigation thường được thực hiện bằng cách thay đổi URL thông qua các phương tiện nêu trên.
Sau bước này, Angular Router sẽ phát sinh sự kiện NavigationStart
.
Recognizes router states
Quá trình thứ hai trong Angular Router được gọi là Recognizes router states, tức là Nhận diện trạng thái của router. Ở bước này, Angular Router thực hiện một số thuật toán như Backtracking và Depth-First để xác định xem route nào phù hợp với URL mà chúng ta muốn chuyển hướng đến. Đây là một phần quan trọng của The Powerful URL Matching Engine của Angular Router.
Ở bước này, có thể chúng ta sẽ bị chuyển hướng đến một URL khác, tuy nhiên, kết quả cuối cùng sẽ rơi vào một trong hai trường hợp: hoặc là một lỗi do không có route nào thích hợp, hoặc là route được nhận diện.
Ví dụ, trong ứng dụng demo của chúng ta, khi người dùng điều hướng đến /, nó sẽ tự động chuyển hướng về /article và sẽ được nhận diện. Tuy nhiên, nếu người dùng truy cập /something-not-present, chúng ta sẽ nhận được một thông báo lỗi: “Error: Cannot match any routes. URL Segment: ‘something-not-present'”.
Sau khi hoàn thành bước này, Angular Router sẽ tạo ra một RouterState chứa thông tin về các activated route, như component được kết nối, các tham số, dữ liệu, và nhiều thông tin khác, thông qua ActivatedRoute.
Sau bước này, Angular Router sẽ phát sinh sự kiện RoutesRecognized.
Runs guards
Bước tiếp theo mà chúng ta sẽ khám phá chủ yếu trong ngày hôm nay là Runs Guards, hay “Thực hiện các bảo vệ”.
Ở giai đoạn này, chúng ta sẽ có một trạng thái tương lai của router, và Router sẽ kiểm tra xem liệu đến địa điểm mới này có được phép hay không.
Chúng ta có thể áp dụng nhiều loại Guards khác nhau. Router sẽ kiểm tra xem tất cả các Guards có trả về true, hoặc là một Promise<<true>>, hoặc là một Observable<<true>> không. Nếu điều này xảy ra, thì chúng ta sẽ có “giấy thông hành” để tiếp tục. Ngược lại, nếu bất kỳ Guard nào trả về giá trị false, hoặc là một Promise<<false>>, hoặc là một Observable<<false>>, hoặc là một UrlTree, thì quá trình không được phép thực hiện.
Ở giai đoạn này, Router sẽ phát sinh hai sự kiện là GuardsCheckStart và GuardsCheckEnd.
Đối với ứng dụng của chúng ta, có nhiều điểm để áp dụng Guards, như khi truy cập trang /admin, hoặc khi vào chức năng chỉnh sửa một bài viết. Cũng có thể kiểm tra xem có cho phép điều hướng ra khỏi trang chỉnh sửa khi người dùng đang thực hiện việc chỉnh sửa hay không, và nhiều trường hợp khác nữa.
Resolves data
Sau khi hoàn tất giai đoạn kiểm tra bảo vệ với Guards, Router sẽ tiến đến bước Resolves data. Ở giai đoạn này, chúng ta có thể trước đón dữ liệu trước khi Router hiển thị bất kỳ nội dung nào.
Trong quá trình này, Router sẽ phát sinh hai sự kiện là ResolveStart và ResolveEnd.
Khi giai đoạn này kết thúc, Router sẽ cập nhật thông tin từ các resolvers vào thuộc tính data của ActivatedRoute.
Sau khi đã chạy xong tất cả các resolvers được thiết lập trước đó, Router sẽ tiếp tục kích hoạt component và hiển thị chúng trong các router-outlet tương ứng theo cấu hình đã được xác định trước đó.
Activating Components
Tại bước này, Router sẽ kích hoạt các component đã được liên kết với các activated route. Điều này bao gồm cả việc tạo mới các component hoặc tái sử dụng chúng, sau đó hiển thị chúng trong các vùng router-outlet tương ứng. Thông thường, vùng mặc định sẽ là primary outlet, tức là <router-outlet></router-outlet>
mà không có tên.
Các sự kiện tương ứng trong quá trình này là ActivationStart, ActivationEnd, ChildActivationStart, ChildActivationEnd.
Sau khi quá trình điều hướng hoàn tất, Router sẽ phát sinh sự kiện NavigationEnd. Đây là sự kiện mà bạn có thể quan sát để thực hiện các hành động cần thiết sau khi điều hướng.
Tiếp theo, Router sẽ cập nhật URL trên thanh địa chỉ của trình duyệt, giúp bạn có thể theo dõi thay đổi, trừ khi bạn đã thiết lập skipLocationChange = true.
Manages navigation
Ở bước này, Router tiếp tục theo dõi, nếu có bất kỳ yêu cầu nào để thay đổi URL thông qua các phương thức đã được nêu trên, nó sẽ kích hoạt một chu trình điều hướng mới.
Route Guards
Route Guards đóng vai trò quan trọng trong việc quyết định liệu tôi có được phép chuyển hướng đến URL này hay không.
Nếu tất cả các guards trả về true, quá trình chuyển hướng sẽ tiếp tục. Ngược lại, nếu bất kỳ guard nào trả về false, chuyển hướng sẽ bị hủy bỏ. Trong trường hợp mà bất kỳ guard nào trả về một UrlTree, chuyển hướng hiện tại sẽ bị hủy bỏ và một chuyển hướng mới sẽ được kích hoạt đến UrlTree được trả về từ guard.
Angular Router cung cấp một số loại guards như sau:
- CanActivate: Được sử dụng để kiểm tra xem một route có thể được kích hoạt hay không.
interface CanActivate {
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
):
| Observable<boolean | UrlTree>
| Promise<boolean | UrlTree>
| boolean
| UrlTree;
}
2. CanActivateChild: Kiểm tra xem một route con có thể được kích hoạt hay không.
interface CanActivateChild {
canActivateChild(
childRoute: ActivatedRouteSnapshot,
state: RouterStateSnapshot
):
| Observable<boolean | UrlTree>
| Promise<boolean | UrlTree>
| boolean
| UrlTree;
}
3. CanDeactivate: Kiểm tra xem một component có thể được hủy kích hoạt hay không.
interface CanDeactivate<T> {
canDeactivate(
component: T,
currentRoute: ActivatedRouteSnapshot,
currentState: RouterStateSnapshot,
nextState?: RouterStateSnapshot
):
| Observable<boolean | UrlTree>
| Promise<boolean | UrlTree>
| boolean
| UrlTree;
}
4. CanLoad: Kiểm tra xem một module có thể được load ẩn độ hay không (lazy loading).
interface CanLoad {
canLoad(
route: Route,
segments: UrlSegment[]
):
| Observable<boolean | UrlTree>
| Promise<boolean | UrlTree>
| boolean
| UrlTree;
}
CanActivate
Giả sử chúng ta muốn thêm chức năng chỉ cho phép người tạo bài viết mới có thể chỉnh sửa bài của mình. Bằng cách sử dụng CanActivate
, chúng ta có thể kiểm tra quyền truy cập trước khi cho phép người dùng di chuyển vào trang chỉnh sửa. Dưới đây là cách thực hiện:
Đầu tiên, chúng ta cần cấu hình routing như sau:
const routes: Routes = [
{
path: "article",
component: ArticleComponent,
children: [
{
path: "",
component: ArticleListComponent,
},
{
path: ":slug",
component: ArticleDetailComponent,
},
{
path: ":slug/edit",
component: ArticleEditComponent,
canActivate: [CanEditArticleGuard],
},
],
},
];
Sau đó, tạo một service để kiểm tra quyền và đăng ký guard:
import { Injectable } from "@angular/core";
import {
CanActivate,
ActivatedRouteSnapshot,
RouterStateSnapshot,
UrlTree,
} from "@angular/router";
import { Observable } from "rxjs";
@Injectable({
providedIn: "root",
})
export class CanEditArticleGuard implements CanActivate {
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
):
| Observable<boolean | UrlTree>
| Promise<boolean | UrlTree>
| boolean
| UrlTree {
// Logic để kiểm tra quyền truy cập, trả về true nếu có quyền
return true; // Thay thế bằng logic thực tế
}
}
Thêm logic kiểm tra quyền trong guard, có thể sử dụng một service để biết user hiện tại là ai:
import { Injectable } from "@angular/core";
@Injectable({
providedIn: "root",
})
export class UserService {
currentUser = {
username: "TiepPhan",
};
constructor() {}
}
Cuối cùng, implement logic kiểm tra quyền trong guard:
import { Injectable } from "@angular/core";
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from "@angular/router";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";
import { UserService } from "./user.service";
import { ArticleService } from "./article.service";
@Injectable({
providedIn: "root",
})
export class CanEditArticleGuard implements CanActivate {
constructor(
private userService: UserService,
private articleService: ArticleService
) {}
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
):
| Observable<boolean | UrlTree>
| Promise<boolean | UrlTree>
| boolean
| UrlTree {
// Lấy thông tin bài viết
return this.articleService
.getArticleBySlug(next.paramMap.get("slug"))
.pipe(
map(
(article) => article.author === this.userService.currentUser.username
)
);
}
}
Kết quả là, chúng ta không thể vào trang chỉnh sửa của bài viết 2 vì tác giả không phải là người đăng nhập.
Tương tự như CanActivate
, chúng ta có CanActivateChild
cũng thực hiện một cách tương tự, nhưng được áp dụng cho các thành phần con của một route.