路由与导航
基础
- Angular 的路由器是一个可选的服务,它用来呈现指定的 URL 所对应的视图。 它并不是 Angular 核心库的一部分,而是在它自己的 @angular/router 包中。 像其它 Angular 包一样,你可以从它导入所需的一切。
<base url="/">
: 大多数带路由的应用都要在index.html的<head>
标签下先添加一个<base>
元素,来告诉路由器该如何合成导航用的 URL。
参数说明
path?: string
: 是一个用于路由匹配 DSL 中的字符串,注意不要使用/开头pathMatch?: string
: 是一个用来指定路由匹配策略的字符串。可选项有 prefix(默认值)和 full。具体请看下述的匹配策略matcher?: UrlMatcher
: 定义了一个用于路径匹配的自定义策略,指定了它就会代替 path 和 pathMatchcomponent?: Type<any>
: 组件redirectTo?: string
: 是一个 URL 片段,它将会代替当前匹配的 URL 片段outlet?: string
: 是该组件要放进的出口的名字canActivate?: any[]
: 是一个 DI 令牌的数组,具体请看下述守卫函数一节canActivateChild?: any[]
: 是一个 DI 令牌的数组,具体请看下述守卫函数一节canDeactivate?: any[]
: 是一个 DI 令牌的数组,具体请看下述守卫函数一节canLoad?: any[]
: 是一个 DI 令牌的数组,具体请看下述守卫函数一节data?: Data
: 是一个可通过ActivatedRoute
提供给组件的附加数据resolve?: ResolveData
: 是一个 DI 令牌的映射表,用于查阅数据解析器。具体请看下述守卫函数一节children?: Routes
: 是一个子路由定义构成的数组loadChildren?: LoadChildren
: 是一个用于惰性加载子路由的引用。具体请看下述惰性加载一节runGuardsAndResolvers?: RunGuardsAndResolvers
: 定义了路由守卫和解析器的运行时机。默认情况下(paramsChange
),它们只会在路由的矩阵参数(#)变化时才会执行。 当设置为paramsOrQueryParamsChange
时,它们在查询参数(?)变化时也会执行。当设置为always
时,它们每次都会执行。
使用说明
const appRoutes: Routes = [
{ path: '', redirectTo: '/heroes', pathMatch: 'full' },
{ path: 'heroes', component: HeroListComponent, data: { title: 'Heroes List' } },
{ path: 'lits/:id', component: ListComponent },
{ path: '**', redirectTo: '/404', pathMatch: 'full' },
{ path: '404', component: ErrorComponent }
];
@NgModule({
imports: [ RouterModule.forRoot(appRoutes) ]
})
export class AppModule { }
简单配置
[
{
path: 'team/:id',
component: Team,
children: [
{
path: 'user/:name',
component: User
}
]
}
]
多重路由出口
[
{
path: 'team/:id',
component: Team
},
{
path: 'chat/:user',
component: Chat,
outlet: 'aux'
}
]
通配符
[{ path: '**', component: Sink }]
重定向
[
{
path: 'team/:id',
component: Team,
children: [
{
path: 'legacy/user/:name',
redirectTo: 'user/:name'
},
{
path: 'user/:name',
component: User
}
]
}
]
空路径
[
{
path: 'team/:id',
component: Team,
children: [
{
path: '',
component: WrapperCmp,
children: [
{
path: 'user/:name',
component: User
}
]
}
]
}
]
无组件路由
[
{
path: 'parent/:id',
children: [{ path: 'a', component: MainChild }, { path: 'b', component: AuxChild, outlet: 'aux' }]
}
]
路由嵌套
// 相当于访问 /parent/left 或者 /parent/right
const routes: Routes = [
{
path: 'parent',
component: DetailComponent,
canActivate: [CanActivateService],
children: [
{
path: 'left',
component: DetailLeftComponent
},
{
path: 'right',
component: DetailRightComponent
}
]
}
]
// 同级显示多页路由视图
const routes: Routes = [
{
path: 'parent',
component: DetailComponent,
canActivate: [CanActivateService],
children: [
{
path: '',
component: DetailLeftComponent,
outlet: 'left'
},
{
path: '',
component: DetailRightComponent,
outlet: 'right'
}
]
}
]
匹配策略
默认情况下,路由器会查看当前 URL 中还剩下什么,并检查它是否以指定的路径开头(比如 /team/11/user 就是用 team/:id 开头的)。我们可以修改匹配策略,以确保该路径匹配所有尚未消费的 url,它相当于 unconsumedUrl === path 或正则表达式中的 $。如果要把空路径路由重定向到别处,这尤其重要。
如下图:由于空路径是任何 url 的前缀,所以即使想导航到 ‘/main’,路由器仍然会执行这次跳转。如果指定了 pathMatch: full,则路由器只有在导航到 ‘/’ 时才会执行这次跳转。
[
{
path: '',
pathMatch: 'full',
redirectTo: 'main'
},
{
path: 'main',
component: Main
}
]
惰性加载
惰性加载可以通过把应用拆分成多个发布包,并按需加载它们,来加速应用的启动时间。 路由器的设计让惰性加载非常简易。只要用 loadChildren 属性代替 children 属性就可以了。我们需要注意一下几点:
- 组件必须在某个模块中定义,因此组件不能惰性加载
- 模块允许惰性加载
综上所述我们需要实现惰性加载需要一下几个步骤:
- 根据顶级路由来划分路由模块
- 路由模块中定义需要独立使用到的组件、指令和管道。
@Injectable({ providedIn: 'root' })
定义所有的自定义服务,可以在应用程序的任何地方直接使用 - 新建一个模块
AppCommonModule
, 这个模块引入所有需要公共使用的组件、指令和管道,以及第三方插件等等。所有的路由模块和AppModule
导入该模块 - 通过 loadChildren 惰性加载路由模块。
loadChildren: './login-routing.module#LoginRoutingModule'
# 符号后边的表示export
方式导出的、需要加载的模块
路由预加载
定义路由的时候可以对模块采用惰性加载的方式进行处理,这样处理好的处是能够够节省资源开销,只有在用户需要的时候通过点击路由之后再加载对应的模块。
但是在有些业务逻辑下,我们希望用户能够直接访问到模块而不是通过点击路由之后再去加载,因为有些情况下业务逻辑比较复杂的时候访问的时候再去加载用户体现就不会太好,这样的情况下可以在对应模块的定义文件中对路由模块的加载策略做定义使用预加载模式。
预加载模式:在首页模块加载完成之后,按照指定的策略异步去加载剩余的模块,这样我们就不用等到点击对应模块的时候采取加载。
import { NgModule } from '@angular/core'
import { Routes, RouterModule } from '@angular/router'
import { AppPreloadingStrategyService } from '../service/app-preloading-strategy.service'
const routes: Routes = []
@NgModule({
imports: [
RouterModule.forRoot(routes, {
preloadingStrategy: AppPreloadingStrategyService // 定义预加载策略
})
],
exports: [RouterModule]
})
export class AppRoutingModule {}
// app-preloading-strategy.service
import { Injectable } from '@angular/core'
import { PreloadingStrategy, Route } from '@angular/router'
import { Observable, of } from 'rxjs'
@Injectable({
providedIn: 'root'
})
export class AppPreloadingStrategyService implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
// 路由中的数据包括 preload: true 的模块启用预加载
return route.data && route.data.preload ? load() : of(null)
// 这里是等首页加载完成之后立即加载
// 同样我们在这里可以进行特殊处理,比如等待 3 秒再去加载
return route.data && route.data.preload ? setTimeout(() => load(), 3000) : of(null)
}
}
出口
RouterOutlet 是一个来自路由模块中的指令,它的用法类似于组件。 它扮演一个占位符的角色,用于在模板中标出一个位置,路由器将会把要显示在这个出口处的组件显示在这里。
<router-outlet></router-outlet>
<router-outlet name="aux"></router-outlet>
导航
<!-- path: 'name/:id' -->
<a [routerLink]="[ '/name/1', { queryParams: { page: 1 } } ]">
<a [routerLink]="[ '/name/2' ]" [queryParams]="{ page: 1 }">
this.router.navigate(['/results'], { queryParams: { page: 1 } });
导航时用到的额外选项:
relativeTo : ActivatedRoute
: 允许从当前激活的路由进行相对导航。父级 …/ 开始,子级 ./ 开始queryParams : Params
: 设置 URL 的查询参数/results?page=1
fragment : String
: 设置 URL 的哈希片段queryParamsHandling : QueryParamsHandling
: 配置后续导航时对查询(?)参数的处理策略preserveFragment : boolean
: 在后续导航时保留#片段skipLocationChange : boolean
: 导航时不要把新状态记入历史replaceUrl : boolean
: 导航时不要把当前状态记入历史- take
其他指令:
routerLinkActive
: 基于当前的 RouterState 为活动的 RouterLink 切换所绑定的 css 类。<a routerLink="/heroes" routerLinkActive="active">Heroes</a>
路由事件
import { Component, OnInit } from '@angular/core'
import { Router, NavigationEnd, NavigationStart } from '@angular/router'
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {
constructor(private router: Router) {}
ngOnInit(): void {
this.router.events.subscribe(event => {
if (event instanceof NavigationStart) {
console.log('NavigationStart')
} else if (event instanceof NavigationEnd) {
console.log('NavigationEnd')
}
})
}
}
路由器事件 | 说明 |
---|---|
NavigationStart | 在导航开始时触发 |
RouteConfigLoadStart | 在 Router 惰性加载 某个路由配置之前触发 |
RouteConfigLoadEnd | 在惰性加载了某个路由后触发 |
RoutesRecognized | 在路由器解析完 URL,并识别出了相应的路由时触发 |
GuardsCheckStart | 在路由器开始 Guard 阶段之前触发 |
ChildActivationStart | 在路由器开始激活路由的子路由时触发 |
ActivationStart | 在路由器开始激活某个路由时触发 |
GuardsCheckEnd | 在路由器成功完成了 Guard 阶段时触发 |
ResolveStart | 在 Router 开始解析(Resolve)阶段时触发 |
ResolveEnd | 在路由器成功完成了路由的解析(Resolve)阶段时触发 |
ChildActivationEnd | 在路由器激活了路由的子路由时触发 |
ActivationEnd | 在路由器激活了某个路由时触发 |
NavigationEnd | 在导航成功结束之后触发 |
NavigationCancel | 在导航被取消之后触发。 这可能是因为在导航期间某个路由守卫返回了 false |
NavigationError | 在导航由于意料之外的错误而失败时触发 |
Scroll | 本事件代表一个滚动事件 |
守卫函数
canActivate
: 控制是否允许进入路由canActivateChild
: 等同 canActivate,只不过针对是所有子路由canDeactivate
: 控制是否允许离开路由canLoad
: 控制是否允许延迟加载整个模块
示例:
import { CanActivate, Router } from '@angular/router'
import { Injectable } from '@angular/core'
import { LoginService } from './login.service'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
@Injectable({
providedIn: 'root'
})
export class CanActivateService implements CanActivate {
constructor(private router: Router, private login: LoginService) {}
canActivate(): Observable<any> {
return this.login.isLogin().pipe(
map(data => {
if (!data) {
this.router.navigate(['login'])
}
return data
})
)
}
}
[{ path: 'home', component: HomeComponent, canActivate: [CanActivateService] }]