Angular 教程:為英雄之旅添加路由支持-里程碑 6:異步路由

2022-07-05 11:34 更新

里程碑 6:異步路由

完成上面的里程碑后,應(yīng)用程序很自然地長大了。在某一個時間點,你將達到一個頂點,應(yīng)用將會需要過多的時間來加載。

為了解決這個問題,請使用異步路由,它會根據(jù)請求來惰性加載某些特性模塊。惰性加載有很多好處。

  • 你可以只在用戶請求時才加載某些特性區(qū)。
  • 對于那些只訪問應(yīng)用程序某些區(qū)域的用戶,這樣能加快加載速度。
  • 你可以持續(xù)擴充惰性加載特性區(qū)的功能,而不用增加初始加載的包體積。

你已經(jīng)完成了一部分。通過把應(yīng)用組織成一些模塊:?AppModule?、?HeroesModule?、?AdminModule ?和 ?CrisisCenterModule?,你已經(jīng)有了可用于實現(xiàn)惰性加載的候選者。

有些模塊(比如 ?AppModule?)必須在啟動時加載,但其它的都可以而且應(yīng)該惰性加載。比如 ?AdminModule ?就只有少數(shù)已認證的用戶才需要它,所以你應(yīng)該只有在正確的人請求它時才加載。

惰性加載路由配置

把 ?admin-routing.module.ts? 中的 ?admin ?路徑從 ?'admin'? 改為空路徑 ?''?。

可以用空路徑路由來對路由進行分組,而不用往 URL 中添加額外的路徑片段。用戶仍舊訪問 ?/admin?,并且 ?AdminComponent ?仍然作為用來包含子路由的路由組件。

打開 ?AppRoutingModule?,并把一個新的 ?admin ?路由添加到它的 ?appRoutes ?數(shù)組中。

給它一個 ?loadChildren ?屬性(替換掉 ?children ?屬性)。?loadChildren ?屬性接收一個函數(shù),該函數(shù)使用瀏覽器內(nèi)置的動態(tài)導入語法 ?import('...')? 來惰性加載代碼,并返回一個承諾(Promise)。其路徑是 ?AdminModule ?的位置(相對于應(yīng)用的根目錄)。當代碼請求并加載完畢后,這個 ?Promise ?就會解析成一個包含 ?NgModule ?的對象,也就是 ?AdminModule?。

{
  path: 'admin',
  loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
},
注意:
當使用絕對路徑時,?NgModule ?的文件位置必須以 ?src/app? 開頭,以便正確解析。對于自定義的 使用絕對路徑的路徑映射表,你必須在項目的 ?tsconfig.json? 中必須配置好 ?baseUrl ?和 ?paths ?屬性。

當路由器導航到這個路由時,它會用 ?loadChildren ?字符串來動態(tài)加載 ?AdminModule?,然后把 ?AdminModule ?添加到當前的路由配置中,最后,它把所請求的路由加載到目標 ?admin ?組件中。

惰性加載和重新配置工作只會發(fā)生一次,也就是在該路由首次被請求時。在后續(xù)的請求中,該模塊和路由都是立即可用的。

最后一步是把管理特性區(qū)從主應(yīng)用中完全分離開。根模塊 ?AppModule ?既不能加載也不能引用 ?AdminModule ?及其文件。

在 ?app.module.ts? 中,從頂部移除 ?AdminModule ?的導入語句,并且從 NgModule 的 ?imports ?數(shù)組中移除 ?AdminModule?。

CanLoad:保護對特性模塊的未授權(quán)加載

你已經(jīng)使用 ?CanActivate ?保護 ?AdminModule ?了,它會阻止未授權(quán)用戶訪問管理特性區(qū)。如果用戶未登錄,它就會跳轉(zhuǎn)到登錄頁。

但是路由器仍然會加載 ?AdminModule ?—— 即使用戶無法訪問它的任何一個組件。理想的方式是,只有在用戶已登錄的情況下你才加載 ?AdminModule?。

添加一個 ?CanLoad ?守衛(wèi),它只在用戶已登錄并且嘗試訪問管理特性區(qū)的時候,才加載 ?AdminModule ?一次。

現(xiàn)有的 ?AuthGuard ?的 ?checkLogin()? 方法中已經(jīng)有了支持 ?CanLoad ?守衛(wèi)的基礎(chǔ)邏輯。

  1. 打開 ?auth.guard.ts?。
  2. 從 ?@angular/router? 導入 ?CanLoad ?接口。
  3. 把它添加到 ?AuthGuard ?類的 ?implements ?列表中。
  4. 然后像下面這樣實現(xiàn) ?canLoad()?:
canLoad(route: Route): boolean {
  const url = `/${route.path}`;

  return this.checkLogin(url);
}

路由器會把 ?canLoad()? 方法的 ?route ?參數(shù)設(shè)置為準備訪問的目標 URL。如果用戶已經(jīng)登錄了,?checkLogin()? 方法就會重定向到那個 URL。

現(xiàn)在,把 ?AuthGuard ?導入到 ?AppRoutingModule ?中,并把 ?AuthGuard ?添加到 ?admin ?路由的 ?canLoad ?數(shù)組中。完整的 ?admin ?路由是這樣的:

{
  path: 'admin',
  loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
  canLoad: [AuthGuard]
},

預加載:特性區(qū)的后臺加載

除了按需加載模塊外,還可以通過預加載方式異步加載模塊。

當應(yīng)用啟動時,?AppModule ?被急性加載,這意味著它會立即加載。而 ?AdminModule? 只在用戶點擊鏈接時加載,這叫做惰性加載。

預加載允許你在后臺加載模塊,以便當用戶激活某個特定的路由時,就可以渲染這些數(shù)據(jù)了??紤]一下危機中心。它不是用戶看到的第一個視圖。默認情況下,英雄列表才是第一個視圖。為了獲得最小的初始有效負載和最快的啟動時間,你應(yīng)該急性加載 ?AppModule ?和 ?HeroesModule?。

你可以惰性加載危機中心。但是,你幾乎可以肯定用戶會在啟動應(yīng)用之后的幾分鐘內(nèi)訪問危機中心。理想情況下,應(yīng)用啟動時應(yīng)該只加載 ?AppModule ?和 ?HeroesModule?,然后幾乎立即開始后臺加載 ?CrisisCenterModule?。在用戶瀏覽到危機中心之前,該模塊應(yīng)該已經(jīng)加載完畢,可供訪問了。

預加載的工作原理

在每次成功的導航后,路由器會在自己的配置中查找尚未加載并且可以預加載的模塊。是否加載某個模塊,以及要加載哪些模塊,取決于預加載策略。

?Router ?提供了兩種預加載策略:

策略

詳情

不預加載

這是默認值。惰性加載的特性區(qū)仍然會按需加載。

預加載

預加載所有惰性加載的特性區(qū)。

路由器或者完全不預加載或者預加載每個惰性加載模塊。 路由器還支持自定義預加載策略,以便完全控制要預加載哪些模塊以及何時加載。

本節(jié)將指導你把 ?CrisisCenterModule ?改成惰性加載的,并使用 ?PreloadAllModules ?策略來預加載所有惰性加載模塊。

惰性加載危機中心

修改路由配置,來惰性加載 ?CrisisCenterModule?。修改的步驟和配置惰性加載 ?AdminModule ?時一樣。

  1. 把 ?CrisisCenterRoutingModule ?中的路徑從 ?crisis-center? 改為空字符串。
  2. 往 ?AppRoutingModule ?中添加一個 ?crisis-center? 路由。
  3. 設(shè)置 ?loadChildren ?字符串來加載 ?CrisisCenterModule?。
  4. 從 ?app.module.ts? 中移除所有對 ?CrisisCenterModule ?的引用。

下面是打開預加載之前的模塊修改版:

  • app.module.ts
  • import { NgModule } from '@angular/core';
    import { BrowserModule } from '@angular/platform-browser';
    import { FormsModule } from '@angular/forms';
    import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
    
    import { Router } from '@angular/router';
    
    import { AppComponent } from './app.component';
    import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
    import { ComposeMessageComponent } from './compose-message/compose-message.component';
    
    import { AppRoutingModule } from './app-routing.module';
    import { HeroesModule } from './heroes/heroes.module';
    import { AuthModule } from './auth/auth.module';
    
    @NgModule({
      imports: [
        BrowserModule,
        BrowserAnimationsModule,
        FormsModule,
        HeroesModule,
        AuthModule,
        AppRoutingModule,
      ],
      declarations: [
        AppComponent,
        ComposeMessageComponent,
        PageNotFoundComponent
      ],
      bootstrap: [ AppComponent ]
    })
    export class AppModule {
    }
  • app-routing.module.ts
  • import { NgModule } from '@angular/core';
    import {
      RouterModule, Routes,
    } from '@angular/router';
    
    import { ComposeMessageComponent } from './compose-message/compose-message.component';
    import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
    
    import { AuthGuard } from './auth/auth.guard';
    
    const appRoutes: Routes = [
      {
        path: 'compose',
        component: ComposeMessageComponent,
        outlet: 'popup'
      },
      {
        path: 'admin',
        loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
        canLoad: [AuthGuard]
      },
      {
        path: 'crisis-center',
        loadChildren: () => import('./crisis-center/crisis-center.module').then(m => m.CrisisCenterModule)
      },
      { path: '',   redirectTo: '/heroes', pathMatch: 'full' },
      { path: '**', component: PageNotFoundComponent }
    ];
    
    @NgModule({
      imports: [
        RouterModule.forRoot(
          appRoutes,
        )
      ],
      exports: [
        RouterModule
      ]
    })
    export class AppRoutingModule {}
  • crisis-center-routing.module.ts
  • import { NgModule } from '@angular/core';
    import { RouterModule, Routes } from '@angular/router';
    
    import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component';
    import { CrisisListComponent } from './crisis-list/crisis-list.component';
    import { CrisisCenterComponent } from './crisis-center/crisis-center.component';
    import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component';
    
    import { CanDeactivateGuard } from '../can-deactivate.guard';
    import { CrisisDetailResolverService } from './crisis-detail-resolver.service';
    
    const crisisCenterRoutes: Routes = [
      {
        path: '',
        component: CrisisCenterComponent,
        children: [
          {
            path: '',
            component: CrisisListComponent,
            children: [
              {
                path: ':id',
                component: CrisisDetailComponent,
                canDeactivate: [CanDeactivateGuard],
                resolve: {
                  crisis: CrisisDetailResolverService
                }
              },
              {
                path: '',
                component: CrisisCenterHomeComponent
              }
            ]
          }
        ]
      }
    ];
    
    @NgModule({
      imports: [
        RouterModule.forChild(crisisCenterRoutes)
      ],
      exports: [
        RouterModule
      ]
    })
    export class CrisisCenterRoutingModule { }

你可以現(xiàn)在嘗試它,并確認在點擊了“Crisis Center”按鈕之后加載了 ?CrisisCenterModule?。

要為所有惰性加載模塊啟用預加載功能,請從 Angular 的路由模塊中導入 ?PreloadAllModules?。

?RouterModule.forRoot()? 方法的第二個參數(shù)接受一個附加配置選項對象。?preloadingStrategy ?就是其中之一。把 ?PreloadAllModules ?添加到 ?forRoot()? 調(diào)用中:

RouterModule.forRoot(
  appRoutes,
  {
    enableTracing: true, // <-- debugging purposes only
    preloadingStrategy: PreloadAllModules
  }
)

這項配置會讓 ?Router ?預加載器立即加載所有惰性加載路由(帶 ?loadChildren ?屬性的路由)。

當訪問 ?http://localhost:4200? 時,?/heroes? 路由立即隨之啟動,并且路由器在加載了 ?HeroesModule ?之后立即開始加載 ?CrisisCenterModule?。

目前,?AdminModule ?并沒有預加載,因為 ?CanLoad ?阻塞了它。

CanLoad 會阻塞預加載

?PreloadAllModules ?策略不會加載被?CanLoad?守衛(wèi)所保護的特性區(qū)。

幾步之前,你剛剛給 ?AdminModule ?中的路由添加了 ?CanLoad ?守衛(wèi),以阻塞加載那個模塊,直到用戶認證結(jié)束。?CanLoad ?守衛(wèi)的優(yōu)先級高于預加載策略。

如果你要加載一個模塊并且保護它防止未授權(quán)訪問,請移除 ?canLoad ?守衛(wèi),只單獨依賴?CanActivate?守衛(wèi)。

自定義預加載策略

在很多場景下,預加載的每個惰性加載模塊都能正常工作。但是,考慮到低帶寬和用戶指標等因素,可以為特定的特性模塊使用自定義預加載策略。

本節(jié)將指導你添加一個自定義策略,它只預加載 ?data.preload? 標志為 ?true ?路由?;叵胍幌拢憧梢栽诼酚傻?nbsp;?data ?屬性中添加任何東西。

在 ?AppRoutingModule ?的 ?crisis-center? 路由中設(shè)置 ?data.preload? 標志。

{
  path: 'crisis-center',
  loadChildren: () => import('./crisis-center/crisis-center.module').then(m => m.CrisisCenterModule),
  data: { preload: true }
},

生成一個新的 ?SelectivePreloadingStrategy ?服務(wù)。

ng generate service selective-preloading-strategy

使用下列內(nèi)容替換 ?selective-preloading-strategy.service.ts?:

import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class SelectivePreloadingStrategyService implements PreloadingStrategy {
  preloadedModules: string[] = [];

  preload(route: Route, load: () => Observable<any>): Observable<any> {
    if (route.data?.['preload'] && route.path != null) {
      // add the route path to the preloaded module array
      this.preloadedModules.push(route.path);

      // log the route path to the console
      console.log('Preloaded: ' + route.path);

      return load();
    } else {
      return of(null);
    }
  }
}

?SelectivePreloadingStrategyService ?實現(xiàn)了 ?PreloadingStrategy?,它有一個方法 ?preload()?。

路由器會用兩個參數(shù)來調(diào)用 ?preload()? 方法:

  1. 要加載的路由。
  2. 一個加載器(loader)函數(shù),它能異步加載帶路由的模塊。

?preload ?的實現(xiàn)要返回一個 ?Observable?。如果該路由應(yīng)該預加載,它就會返回調(diào)用加載器函數(shù)所返回的 ?Observable?。如果該路由應(yīng)該預加載,它就返回一個 ?null ?值的 ?Observable ?對象。

在這個例子中,如果路由的 ?data.preload? 標志是真值,則 ?preload()? 方法會加載該路由。

它的副作用是 ?SelectivePreloadingStrategyService ?會把所選路由的 ?path ?記錄在它的公共數(shù)組 ?preloadedModules ?中。

很快,你就會擴展 ?AdminDashboardComponent ?來注入該服務(wù),并且顯示它的 ?preloadedModules ?數(shù)組。

但是首先,要對 ?AppRoutingModule ?做少量修改。

  1. 把 ?SelectivePreloadingStrategyService ?導入到 ?AppRoutingModule ?中。
  2. 把 ?PreloadAllModules ?策略替換成對 ?forRoot()? 的調(diào)用,并且傳入這個 ?SelectivePreloadingStrategyService?。

現(xiàn)在,編輯 ?AdminDashboardComponent ?以顯示這些預加載路由的日志。

  1. 導入 ?SelectivePreloadingStrategyService?(它是一個服務(wù))。
  2. 把它注入到儀表盤的構(gòu)造函數(shù)中。
  3. 修改模板來顯示這個策略服務(wù)的 ?preloadedModules ?數(shù)組。

現(xiàn)在文件如下:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import { SelectivePreloadingStrategyService } from '../../selective-preloading-strategy.service';

@Component({
  selector: 'app-admin-dashboard',
  templateUrl: './admin-dashboard.component.html',
  styleUrls: ['./admin-dashboard.component.css']
})
export class AdminDashboardComponent implements OnInit {
  sessionId!: Observable<string>;
  token!: Observable<string>;
  modules: string[] = [];

  constructor(
    private route: ActivatedRoute,
    preloadStrategy: SelectivePreloadingStrategyService
  ) {
    this.modules = preloadStrategy.preloadedModules;
  }

  ngOnInit() {
    // Capture the session ID if available
    this.sessionId = this.route
      .queryParamMap
      .pipe(map(params => params.get('session_id') || 'None'));

    // Capture the fragment if available
    this.token = this.route
      .fragment
      .pipe(map(fragment => fragment || 'None'));
  }
}

一旦應(yīng)用加載完了初始路由,?CrisisCenterModule ?也被預加載了。通過 ?Admin ?特性區(qū)中的記錄就可以驗證它,“Preloaded Modules”中列出了 ?crisis-center?。它也被記錄到了瀏覽器的控制臺。

使用重定向遷移 URL

你已經(jīng)設(shè)置好了路由,并且用命令式和聲明式的方式導航到了很多不同的路由。但是,任何應(yīng)用的需求都會隨著時間而改變。你把鏈接 ?/heroes? 和 ?hero/:id? 指向了 ?HeroListComponent ?和 ?HeroDetailComponent ?組件。如果有這樣一個需求,要把鏈接 ?heroes ?變成 ?superheroes?,你可能仍然希望以前的 URL 能正常導航。但你也不想在應(yīng)用中找到并修改每一個鏈接,這時候,重定向就可以省去這些瑣碎的重構(gòu)工作。

把 /heroes 改為 /superheroes

本節(jié)將指導你將 ?Hero ?路由遷移到新的 URL。在導航之前,?Router ?會檢查路由配置中的重定向語句,以便將來按需觸發(fā)重定向。要支持這種修改,你就要在 ?heroes-routing.module? 文件中把老的路由重定向到新的路由。

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

import { HeroListComponent } from './hero-list/hero-list.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';

const heroesRoutes: Routes = [
  { path: 'heroes', redirectTo: '/superheroes' },
  { path: 'hero/:id', redirectTo: '/superhero/:id' },
  { path: 'superheroes',  component: HeroListComponent, data: { animation: 'heroes' } },
  { path: 'superhero/:id', component: HeroDetailComponent, data: { animation: 'hero' } }
];

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

注意,這里有兩種類型的重定向。第一種是不帶參數(shù)的從 ?/heroes? 重定向到 ?/superheroes?。這是一種非常直觀的重定向。第二種是從 ?/hero/:id? 重定向到 ?/superhero/:id?,它還要包含一個 ?:id? 路由參數(shù)。路由器重定向時使用強大的模式匹配功能,這樣,路由器就會檢查 URL,并且把 ?path ?中帶的路由參數(shù)替換成相應(yīng)的目標形式。以前,你導航到形如 ?/hero/15? 的 URL 時,帶了一個路由參數(shù) ?id?,它的值是 ?15?。

在重定向的時候,路由器還支持查詢參數(shù)片段(fragment)。
  • 當使用絕對地址重定向時,路由器將會使用路由配置的 ?redirectTo ?屬性中規(guī)定的查詢參數(shù)和片段。
  • 當使用相對地址重定向時,路由器將會使用源地址(跳轉(zhuǎn)前的地址)中的查詢參數(shù)和片段。

目前,空路徑被重定向到了 ?/heroes?,它又被重定向到了 ?/superheroes?。這樣不行,因為 ?Router ?在每一層的路由配置中只會處理一次重定向。這樣可以防止出現(xiàn)無限循環(huán)的重定向。

所以,你要在 ?app-routing.module.ts? 中修改空路徑路由,讓它重定向到 ?/superheroes?。

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

import { ComposeMessageComponent } from './compose-message/compose-message.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

import { AuthGuard } from './auth/auth.guard';
import { SelectivePreloadingStrategyService } from './selective-preloading-strategy.service';

const appRoutes: Routes = [
  {
    path: 'compose',
    component: ComposeMessageComponent,
    outlet: 'popup'
  },
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
    canLoad: [AuthGuard]
  },
  {
    path: 'crisis-center',
    loadChildren: () => import('./crisis-center/crisis-center.module').then(m => m.CrisisCenterModule),
    data: { preload: true }
  },
  { path: '',   redirectTo: '/superheroes', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [
    RouterModule.forRoot(
      appRoutes,
      {
        enableTracing: false, // <-- debugging purposes only
        preloadingStrategy: SelectivePreloadingStrategyService,
      }
    )
  ],
  exports: [
    RouterModule
  ]
})
export class AppRoutingModule { }

由于 ?routerLink ?與路由配置無關(guān),所以你要修改相關(guān)的路由鏈接,以便在新的路由激活時,它們也能保持激活狀態(tài)。還要修改 ?app.component.ts? 模板中的 ?/heroes? 這個 ?routerLink?。

<div class="wrapper">
  <h1 class="title">Angular Router</h1>
  <nav>
    <a routerLink="/crisis-center" routerLinkActive="active" ariaCurrentWhenActive="page">Crisis Center</a>
    <a routerLink="/superheroes" routerLinkActive="active" ariaCurrentWhenActive="page">Heroes</a>
    <a routerLink="/admin" routerLinkActive="active" ariaCurrentWhenActive="page">Admin</a>
    <a routerLink="/login" routerLinkActive="active" ariaCurrentWhenActive="page">Login</a>
    <a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>
  </nav>
  <div [@routeAnimation]="getRouteAnimationData()">
    <router-outlet></router-outlet>
  </div>
  <router-outlet name="popup"></router-outlet>
</div>

修改 ?hero-detail.component.ts? 中的 ?goToHeroes()? 方法,使用可選的路由參數(shù)導航回 ?/superheroes?。

gotoHeroes(hero: Hero) {
  const heroId = hero ? hero.id : null;
  // Pass along the hero id if available
  // so that the HeroList component can select that hero.
  // Include a junk 'foo' property for fun.
  this.router.navigate(['/superheroes', { id: heroId, foo: 'foo' }]);
}

當這些重定向設(shè)置好之后,所有以前的路由都指向了它們的新目標,并且每個 URL 也仍然能正常工作。

審查路由器配置

要確定你的路由是否真的按照正確的順序執(zhí)行的,你可以審查路由器的配置。

可以通過注入路由器并在控制臺中記錄其 ?config ?屬性來實現(xiàn)。比如,把 ?AppModule ?修改為這樣,并在瀏覽器的控制臺窗口中查看最終的路由配置。

export class AppModule {
  // Diagnostic only: inspect router configuration
  constructor(router: Router) {
    // Use a custom replacer to display function names in the route configs
    const replacer = (key, value) => (typeof value === 'function') ? value.name : value;

    console.log('Routes: ', JSON.stringify(router.config, replacer, 2));
  }
}

最終的應(yīng)用

對這個已完成的路由器應(yīng)用,參見 現(xiàn)場演練 / 下載范例的最終代碼。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號