iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 27
3
Modern Web

Angular 深入淺出三十天系列 第 27

[Angular 深入淺出三十天] Day 26 - 路由總結(二)

子路由模組

當我們將頁面或功能包裝成模組的時候,也能在其中加入路由的設定,使其變成含有路由的子路由模組。

跟 AppRoutingModule 不一樣的是,子路由模組使用的 RouterModule 函式是 forChild

const routes: Routes = [
  // ...
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class FeatureRoutingModule { }

且記得引入位置要擺在 AppRoutingModule 上面:

@NgModule({
  declarations: [
    // ...
  ],
  imports: [
    BrowserModule,
    FeatureModule, // 放在這裡
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

以免因為萬用路由的關係導致無法進入頁面。

延遲載入與預先載入

Angular 預設會將所有模組打包成一個檔案,但我們可以透過延遲載入的方式來讓使用者在進入特定路由的時候,才將需要的模組載入

延遲載入的優點:

  • 使用者需要的時候才會下載,在較大型的系統可有效降低一開始要下載的檔案的大小。
  • 加快初始化時的載入速度,因只需要下載必要的模組

使用延遲載入的方式:

// Angular 2 ~ 7
{
  path: 'feature',
  loadChildren: './feature/feature.module#FeatureModule'
}

// Angular 8+
{
  path: 'feature',
loadChildren: () => import('./feature/feature.module').then(module => module.FeatureModule)
}

./feature/feature.module 是模組檔案路徑; FeatureModule 是模組類別名稱。

但延遲載入有可能會造成畫面卡頓,所以我們可以透過在根模組的路由設定裡加上 { preloadingStrategy: PreloadAllModules } 來啟用預先載入,如:

import { PreloadAllModules } from '@angular/router';

@NgModule({
imports: [RouterModule.forRoot(routes, {
    enableTracing: true,
    preloadingStrategy: PreloadAllModules
  })],
  exports: [RouterModule]
})
export class AppRoutingModule { }

預先載入是在我們的應用初始化的時候就透過非同步的方式在背景下載,所以既不會影響畫面顯示或使用者操作,又能達到延遲載入的效果,是非常實用的功能!

Guard

一般常見使用路由守門員的時機大致上有兩種:

  1. canActivate - 當使用者想要造訪某個路由時,透過路由守門員來判斷要不要讓使用者造訪。
  2. canDeactivate - 當使用者想要離開某個路由時,透過路由守門員來判斷要不要讓使用者離開。

canActivate 的使用方式:

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> | Promise<boolean> | boolean {

    const canActivate = next.queryParams.name === 'Leo';

    if (!canActivate) {
      alert('你不是Leo,不能進去!');
    }

    return canActivate;

  }

}
const routes: Routes = [
  {
    path: '',
    component: LayoutComponent,
    canActivate: [AuthGuard],
    children: [
      // ...
    ]
  },
  // ...
];

canDeactivate 的使用方式:

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, CanDeactivate } from '@angular/router';
import { Observable } from 'rxjs';

import { LoginComponent } from './login.component';

@Injectable({
  providedIn: 'root'
})
export class EnsureLoginGuard implements CanDeactivate<LoginComponent> {

  /**
   * 當使用者要離開這個 Guard 所防守的路由時,會觸發這個函式
   *
   * @param {LoginComponent} component - 該路由的 Component
   * @param {ActivatedRouteSnapshot} currentRoute - 當前的路由
   * @param {RouterStateSnapshot} currentState - 當前路由狀態的快照
   * @param {RouterStateSnapshot} [nextState] - 欲前往路由的路由狀態的快照
   * @returns {(boolean | Observable<boolean> | Promise<boolean>)}
   * @memberof EnsureLoginGuard
   */
  canDeactivate(
    component: LoginComponent,
    currentRoute: ActivatedRouteSnapshot,
    currentState: RouterStateSnapshot,
    nextState?: RouterStateSnapshot
  ): boolean | Observable<boolean> | Promise<boolean> {

    if (component.name.trim()) {
      return confirm('是否要離開此頁面?');
    }

    return true;

  }

}
const routes: Routes = [
  {
    path: 'login',
    component: LoginComponent,
    canDeactivate: [EnsureLoginGuard]
  },
  // ...
];

Resolve

在實務應用上,我們常常會遇到在導頁的時候需要連帶地傳遞一些資料給該頁面。但如果是用昨天所分享過的方式,不管是 Query String 表示法還是 matrix URL notation 表示法,其實都沒辦法傳遞太複雜的資料。甚至有時候是需要先呼叫 API 跟伺服器拿資料的。

像這種時候其實就很適合使用路由的 Resolve 功能來做這樣的處理。

假設我們有個商品細節頁面用來呈現商品的細節,所以我們會從商品清單頁面點選了某一筆的商品之後,將商品 id 帶往商品細節頁。

這時候的路由設定大概會長這樣:

const routes: Routes = [
  {
    path: 'products',
    component: ProductListComponent,
    children: [
      {
        path: ':id',
        component: ProductDetailComponent
        resolve: {
          product: ProductDetailResolverService
        }
      }
    ]
  },
  // ...
];

ProductDetailResolverService 大概會長這樣:

import { Injectable } from '@angular/core';
import {
  Router, 
  Resolve,
  RouterStateSnapshot,
  ActivatedRouteSnapshot
} from '@angular/router';

// Class
import { Product } from './product';

// Service
import { ProductService }  from './product.service';

// RxJS
import { Observable, of, EMPTY } from 'rxjs';
import { mergeMap, take } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class ProductDetailResolverService implements Resolve<Product> {

  constructor(private productService: productService, private router: Router) {}

  resolve(
    route: ActivatedRouteSnapshot, 
    state: RouterStateSnapshot
  ): Observable<Product> | Observable<never> {
    
    const id = route.paramMap.get('id');
    
    return this
            .productService
            .getProduct(id)
            .pipe(
              take(1),
              mergeMap(product => {
                if (product) {
                  return of(product);
                } else { // id not found
                  this.router.navigate(['products']);
                  return EMPTY;
                }
              })
            );
  }
  
}

最後在 ProductDetailComponent 的頁面我們就透過這樣的方式來接值:

ngOnInit() {
  
  this
    .route
    .data
    .subscribe((data: { product: Product }) => {
      this.name= data.product.name;
    });
    
}

如此一來,我們就可以用很簡單且是非同步的方式來傳遞複雜的資料囉!


關於路由,我就大概分享到這邊。

接下來,我們就一起用 Angular 來寫一個簡單的網站來結束這三十天的分享吧!!


參考資料


上一篇
[Angular 深入淺出三十天] Day 25 - 路由總結(一)
下一篇
[Angular 深入淺出三十天] Day 27 - Angular 小學堂(四之一)
系列文
Angular 深入淺出三十天33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
WT
iT邦新手 5 級 ‧ 2018-12-27 13:27:42

LEO您好:
能否請教您一下,如下圖,我使用路由方式,在member下有CRUD這些子路由,member本身則顯示所有成員的List資料,那我要如何在CRUD這些子路由操作之後,刷新member裡面的list資料呢?
我本來在資料操作完成後使用 this.router.navigate(['member']),但是在同一個路由路徑下不會再刷新,實務上有什麼比較好的作法嗎?
https://ithelp.ithome.com.tw/upload/images/20181227/20112863NpYSiiRR58.png

看更多先前的回應...收起先前的回應...
Leo iT邦新手 3 級 ‧ 2018-12-27 13:54:38 檢舉

Hi WT,

可以把資料相關的處理邏輯都放在一支 Service 裡,CRUD 跟 MemberComponent 本身皆可使用這個 Service 來處理資料。

WT iT邦新手 5 級 ‧ 2018-12-27 15:13:38 檢舉

Hello Leo,
假設我在子路由的create頁面中新增一筆資料,希望在新增完成後,
父路由member頁面中就能自動刷新看到這一筆資料,也是可以照您建議的方式處理嗎?
謝謝您

WT iT邦新手 5 級 ‧ 2018-12-27 15:26:39 檢舉

因為由於是父子路由,我從子路由再回到父路由不會觸發ngOnInit()

Leo iT邦新手 3 級 ‧ 2018-12-28 11:14:14 檢舉

Hi WT,

是阿,可以透過這支共用的 Service 來處理。

只要父路由是綁定這個 Service 裡的資料,

那子路由只要透過這個 Service 新增資料的話,

父路由自然就會更新畫面囉!

WT iT邦新手 5 級 ‧ 2018-12-28 15:12:50 檢舉

OK 了解!! LEO謝囉!!

0
Tim Hsu
iT邦新手 1 級 ‧ 2020-09-08 12:00:35

感謝這兩天的路由總結,獲益良多!

Leo iT邦新手 3 級 ‧ 2020-09-08 13:34:22 檢舉

Hi Tim Hsu,

很高興能夠幫助到你,如果有任何問題都可以找我討論:)

我要留言

立即登入留言