Route 异步路由

2020-07-08 11:47 更新

完成上面的里程碑后,应用程序很自然地长大了。在某一个时间点,你将达到一个顶点,应用将会需要过多的时间来加载。

为了解决这个问题,请使用异步路由,它会根据请求来惰性加载某些特性模块。惰性加载有很多好处。

你可以只在用户请求时才加载某些特性区。

对于那些只访问应用程序某些区域的用户,这样能加快加载速度。

你可以持续扩充惰性加载特性区的功能,而不用增加初始加载的包体积。

你已经完成了一部分。通过把应用组织成一些模块:AppModuleHeroesModuleAdminModuleCrisisCenterModule, 你已经有了可用于实现惰性加载的候选者。

有些模块(比如 AppModule)必须在启动时加载,但其它的都可以而且应该惰性加载。 比如 AdminModule 就只有少数已认证的用户才需要它,所以你应该只有在正确的人请求它时才加载。

惰性加载路由配置

把 "admin-routing.module.ts" 中的 admin 路径从 'admin' 改为空路径 ''

可以用空路径路由来对路由进行分组,而不用往 URL 中添加额外的路径片段。 用户仍旧访问 "/admin",并且 AdminComponent 仍然作为用来包含子路由的路由组件。

打开 AppRoutingModule,并把一个新的 admin 路由添加到它的 appRoutes 数组中。

给它一个 loadChildren 属性(替换掉 children 属性)。 loadChildren 属性接收一个函数,该函数使用浏览器内置的动态导入语法 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 字符串来动态加载 AdminModule,然后把 AdminModule 添加到当前的路由配置中, 最后,它把所请求的路由加载到目标 admin 组件中。

惰性加载和重新配置工作只会发生一次,也就是在该路由首次被请求时。在后续的请求中,该模块和路由都是立即可用的。

Angular 提供一个内置模块加载器,支持SystemJS 来异步加载模块。如果你使用其它捆绑工具比如 Webpack,则使用 Webpack 的机制来异步加载模块。

最后一步是把管理特性区从主应用中完全分离开。 根模块 AppModule 既不能加载也不能引用 AdminModule 及其文件。

在 "app.module.ts" 中,从顶部移除 AdminModule 的导入语句,并且从 NgModuleimports 数组中移除 AdminModule

CanLoad:保护对特性模块的未授权加载

你已经使用 CanActivate 保护 AdminModule 了,它会阻止未授权用户访问管理特性区。如果用户未登录,它就会跳转到登录页。

但是路由器仍然会加载 AdminModule —— 即使用户无法访问它的任何一个组件。 理想的方式是,只有在用户已登录的情况下你才加载 AdminModule

添加一个 CanLoad 守卫,它只在用户已登录并且尝试访问管理特性区的时候,才加载 AdminModule 一次。

现有的 AuthGuardcheckLogin() 方法中已经有了支持 CanLoad 守卫的基础逻辑。

打开 "auth.guard.ts",从 @angular/router 中导入 CanLoad 接口。 把它添加到 AuthGuard 类的 implements 列表中。 然后实现 canLoad,代码如下:

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

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


  return this.checkLogin(url);
}

路由器会把 canLoad() 方法的 route 参数设置为准备访问的目标 URL。 如果用户已经登录了,checkLogin() 方法就会重定向到那个 URL

现在,把 AuthGuard 导入到 AppRoutingModule 中,并把 AuthGuard 添加到 admin 路由的 canLoad 数组中。 完整的 admin 路由是这样的:

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

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

预加载:特性区的后台加载

除了按需加载模块外,还可以通过预加载方式异步加载模块。

当应用启动时,AppModule 被急性加载,这意味着它会立即加载。而 AdminModule 只在用户点击链接时加载,这叫做惰性加载。

预加载允许你在后台加载模块,以便当用户激活某个特定的路由时,就可以渲染这些数据了。 考虑一下危机中心。 它不是用户看到的第一个视图。 默认情况下,英雄列表才是第一个视图。为了获得最小的初始有效负载和最快的启动时间,你应该急性加载 AppModuleHeroesModule

你可以惰性加载危机中心。 但是,你几乎可以肯定用户会在启动应用之后的几分钟内访问危机中心。 理想情况下,应用启动时应该只加载 AppModuleHeroesModule,然后几乎立即开始后台加载 CrisisCenterModule。 在用户浏览到危机中心之前,该模块应该已经加载完毕,可供访问了。

  1. 预加载的工作原理

在每次成功的导航后,路由器会在自己的配置中查找尚未加载并且可以预加载的模块。 是否加载某个模块,以及要加载哪些模块,取决于预加载策略。

Router 提供了两种预加载策略:

  • 完全不预加载,这是默认值。惰性加载的特性区仍然会按需加载。

  • 预加载所有惰性加载的特性区。

路由器或者完全不预加载或者预加载每个惰性加载模块。 路由器还支持自定义预加载策略,以便完全控制要预加载哪些模块以及何时加载。

本节将指导你把 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 { }

你可以现在尝试它,并确认在点击了 “Crisis Center” 按钮之后加载了 CrisisCenterModule

要为所有惰性加载模块启用预加载功能,请从 Angular 的路由模块中导入 PreloadAllModules

RouterModule.forRoot() 方法的第二个参数接受一个附加配置选项对象。 preloadingStrategy 就是其中之一。 把 PreloadAllModules 添加到 forRoot() 调用中:

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 守卫所保护的特性区。

几步之前,你刚刚给 AdminModule 中的路由添加了 CanLoad 守卫,以阻塞加载那个模块,直到用户认证结束。 CanLoad 守卫的优先级高于预加载策略。

如果你要加载一个模块并且保护它防止未授权访问,请移除 CanLoad 守卫,只单独依赖CanActivate 守卫。

自定义预加载策略

在很多场景下,预加载的每个惰性加载模块都能正常工作。但是,考虑到低带宽和用户指标等因素,可以为特定的特性模块使用自定义预加载策略。

本节将指导你添加一个自定义策略,它只预加载 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

使用下列内容替换 "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 实现了 PreloadingStrategy,它有一个方法 preload()

路由器会用两个参数来调用 preload() 方法:

  1. 要加载的路由。

  1. 一个加载器(loader)函数,它能异步加载带路由的模块。

preload 的实现要返回一个 Observable。 如果该路由应该预加载,它就会返回调用加载器函数所返回的 Observable。 如果该路由不应该预加载,它就返回一个 null 值的 Observable 对象。

在这个例子中,如果路由的 data.preload 标志是真值,则 preload() 方法会加载该路由。

它的副作用是 SelectivePreloadingStrategyService 会把所选路由的 path 记录在它的公共数组 preloadedModules 中。

很快,你就会扩展 AdminDashboardComponent 来注入该服务,并且显示它的 preloadedModules 数组。

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

  1. SelectivePreloadingStrategyService 导入到 AppRoutingModule 中。

  1. PreloadAllModules 策略替换成对 forRoot() 的调用,并且传入这个 SelectivePreloadingStrategyService

  1. SelectivePreloadingStrategyService 策略添加到 AppRoutingModuleproviders 数组中,以便它可以注入到应用中的任何地方。

现在,编辑 AdminDashboardComponent 以显示这些预加载路由的日志。

导入 SelectivePreloadingStrategyService(它是一个服务)。

把它注入到仪表盘的构造函数中。

修改模板来显示这个策略服务的 preloadedModules 数组。

现在文件如下:

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 特性区中的记录就可以验证它,“Preloaded Modules”中列出了 crisis-center。 它也被记录到了浏览器的控制台。

使用重定向迁移 URL

你已经设置好了路由,并且用命令式和声明式的方式导航到了很多不同的路由。但是,任何应用的需求都会随着时间而改变。 你把链接 "/heroes" 和 "hero/:id" 指向了 HeroListComponentHeroDetailComponent 组件。 如果有这样一个需求,要把链接 "heroes" 变成 "superheroes",你可能仍然希望以前的 URL 能正常导航。 但你也不想在应用中找到并修改每一个链接,这时候,重定向就可以省去这些琐碎的重构工作。

把 /heroes 改为 /superheroes

本节将指导你将 Hero 路由迁移到新的 URL。在导航之前,Router 会检查路由配置中的重定向语句,以便将来按需触发重定向。要支持这种修改,你就要在 "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 { }

注意,这里有两种类型的重定向。第一种是不带参数的从 "/heroes" 重定向到 "/superheroes"。这是一种非常直观的重定向。第二种是从 "/hero/:id" 重定向到 "/superhero/:id",它还要包含一个 :id 路由参数。 路由器重定向时使用强大的模式匹配功能,这样,路由器就会检查 URL,并且把 path 中带的路由参数替换成相应的目标形式。以前,你导航到形如 "/hero/15" 的 URL 时,带了一个路由参数 id,它的值是 15。

在重定向的时候,路由器还支持查询参数和片段(fragment)。

- 当使用绝对地址重定向时,路由器将会使用路由配置的 `redirectTo` 属性中规定的查询参数和片段。

- 当使用相对地址重定向时,路由器将会使用源地址(跳转前的地址)中的查询参数和片段。

目前,空路径被重定向到了 "/heroes",它又被重定向到了 "/superheroes"。这样不行,因为 Router 在每一层的路由配置中只会处理一次重定向。这样可以防止出现无限循环的重定向。

所以,你要在 "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 与路由配置无关,所以你要修改相关的路由链接,以便在新的路由激活时,它们也能保持激活状态。还要修改 "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() 方法,使用可选的路由参数导航回 "/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 也仍然能正常工作。

审查路由器配置

要确定你的路由是否真的按照正确的顺序执行的,你可以审查路由器的配置。

可以通过注入路由器并在控制台中记录其 config 属性来实现。 例如,把 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));
  }
}

最终的应用

对这个已完成的路由器应用,参见 下载范例 的最终代码。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号