Route 異步路由

2020-07-08 11:47 更新

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

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

你可以只在用戶請求時才加載某些特性區(qū)。

對于那些只訪問應用程序某些區(qū)域的用戶,這樣能加快加載速度。

你可以持續(xù)擴充惰性加載特性區(qū)的功能,而不用增加初始加載的包體積。

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

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

惰性加載路由配置

把 "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 的位置(相對于應用的根目錄)。 當代碼請求并加載完畢后,這個 Promise 就會解析成一個包含 NgModule 的對象,也就是 AdminModule。

Path:"app-routing.module.ts (load children)" 。

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

注:

  • 當使用絕對路徑時,NgModule 的文件位置必須以 "src/app" 開頭,以便正確解析。對于自定義的 使用絕對路徑的路徑映射表,你必須在項目的 "tsconfig.json" 中必須配置好 baseUrlpaths 屬性。

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

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

Angular 提供一個內(nèi)置模塊加載器,支持SystemJS 來異步加載模塊。如果你使用其它捆綁工具比如 Webpack,則使用 Webpack 的機制來異步加載模塊。

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

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

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

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

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

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

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

打開 "auth.guard.ts",從 @angular/router 中導入 CanLoad 接口。 把它添加到 AuthGuard 類的 implements 列表中。 然后實現(xiàn) canLoad,代碼如下:

Path:"src/app/auth/auth.guard.ts (CanLoad guard)" 。

canLoad(route: Route): boolean {
  let url = `/${route.path}`;


  return this.checkLogin(url);
}

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

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

Path:"app-routing.module.ts (lazy admin route)" 。

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

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

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

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

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

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

  1. 預加載的工作原理

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

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

  • 完全不預加載,這是默認值。惰性加載的特性區(qū)仍然會按需加載。

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

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

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

  1. 惰性加載危機中心

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

  • CrisisCenterRoutingModule 中的路徑從 crisis-center 改為空字符串。

  • AppRoutingModule 中添加一個 crisis-center 路由。

  • 設置 loadChildren 字符串來加載 CrisisCenterModule。

  • 從 "app.module.ts" 中移除所有對 CrisisCenterModule 的引用。

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

  • Path:"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 {
        }

  • Path:"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 {}

  • Path:"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)用中:

Path:"src/app/app-routing.module.ts (preload all)" 。

    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 路由。回想一下,你可以在路由的 data 屬性中添加任何東西。

AppRoutingModulecrisis-center 路由中設置 data.preload 標志。

Path:"src/app/app-routing.module.ts (route data preload)" 。

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

生成一個新的 SelectivePreloadingStrategy 服務。

ng generate service selective-preloading-strategy

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

Path:"src/app/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 && route.data['preload']) {
      // 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. 要加載的路由。

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

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

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

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

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

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

  1. SelectivePreloadingStrategyService 導入到 AppRoutingModule 中。

  1. PreloadAllModules 策略替換成對 forRoot() 的調(diào)用,并且傳入這個 SelectivePreloadingStrategyService

  1. SelectivePreloadingStrategyService 策略添加到 AppRoutingModuleproviders 數(shù)組中,以便它可以注入到應用中的任何地方。

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

導入 SelectivePreloadingStrategyService(它是一個服務)。

把它注入到儀表盤的構(gòu)造函數(shù)中。

修改模板來顯示這個策略服務的 preloadedModules 數(shù)組。

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

Path:"src/app/admin/admin-dashboard/admin-dashboard.component.ts (preloaded modules)" 。

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'));
  }
}

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

使用重定向遷移 URL

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

把 /heroes 改為 /superheroes

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

Path:"src/app/heroes/heroes-routing.module.ts (heroes redirects)" 。

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ù)替換成相應的目標形式。以前,你導航到形如 "/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"。

Path:"src/app/app-routing.module.ts (superheroes redirect)" 。

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。

Path:"src/app/app.component.html (superheroes active routerLink))" 。

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

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

Path:"src/app/heroes/hero-detail/hero-detail.component.ts (goToHeroes)" 。

gotoHeroes(hero: Hero) {
  let 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' }]);
}

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

審查路由器配置

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

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

Path:"src/app/app.module.ts (inspect the router config)" 。

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));
  }
}

最終的應用

對這個已完成的路由器應用,參見 下載范例 的最終代碼。

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號