Angular路由
背景
Angular相关内容一个文件写不下,分开在多个文档里可能更好一些.
路由的引入
基础的实现只需要在模块的imports数据中使用.
1 | ({ |
不过通常会将路由的定义独立到外部的一个模块中
1 | import { Routes, RouterModule } from '@angular/router'; |
然后在主要的模块中引用
1 | ({ |
此处的 AppRoutingModule
本身是一个专注与提供路由功能的 模块.
只不过没有组件,且直接放在另一个模块的文件夹下而已
注意由于 app.module
是根模块,所以使用 forRoot
的写法,
之后会遇到功能拆分后独立出来的feature模块,他们的路由就需要使用 forChild
了,
这种规定似乎让angular能够按照特定顺序将路由组合在一起.
各种各样的路由
路由本身有多种情况
- 普通路由
- 子路由
- 带id的路由
- 默认路由,通常用来重定向到首页
- 通配符路由(通常用来显示404页面)
Angular的路由遵循先到显得的顺序,因此为了防止覆盖,通常在书写顺序上,
先写普通路由,然后是路径为空的默认路由,最后是通配符路由.
1 | const routes: Routes = [ |
访问特定地址
-
在浏览器地址栏中直接输入
-
html文件中使用 routerlink,使用数组则代表拼接
1
2
3
4<a routerLink="./crises" routerLinkActive="active">简单使用</a>
<!-- routerLinkActive用于指定一个该路由被激活时使用的css,active好像是内置css -->
<a [routerLink]="['/hero', hero.id]">拼接id,相当于/hero/id</a>
<a [routerLink]="['/crisis-center', { foo: 'foo' }]">使用可选参数</a> -
ts文件中使用navigate,路径格式类似routerLink
1
2
3
4gotoItems(hero: Hero) {
const heroId = hero ? hero.id : null;
this.router.navigate(['/heroes', { id: heroId }]);
} -
使用
router.parseUrl
让路由器自己去导航1
2
3canActivate(next: some, route: some) :boolean|urlTree {
return router.parseUrl()
}
路由信息的解析
常见web应用中,通常会解析访问时的一些
- 必选参数(比如id)
- 可选参数
- 查询参数(问号后面的)
- 数据
在Angular中可以使用 ActivatedRoute
来获取这些信息,
且获取到的是一个Observable.
对应上方
- 必选参数和可选参数放在
paramMap
中(params
是旧版用法) - 查询参数在
queryParamMap
中(queryParams
是旧版用法) - 数据放在
data
中
1 | import { Router, ActivatedRoute, ParamMap } from '@angular/router'; |
如果不希望得到一个Observable的值,
那么可以使用 route.snapshot.paramMap.get('id)
的方式来直接获得Obesrvable中包裹的值.
同时, route.snapshot
得到的数据类型为 ActivatedRouteSnapshot
之后会遇到另外的两个类
RouterStateSnapshot
用在路由守卫中,仅仅有一个成员state.url
表示用户来自的urlRoute
路由配置文件中的信息被放在了这个类中,不过这个类一般不怎么使用
路由的参数
按是否可观察分
- observable的,使用
this.route.paramMap
等获得. - 非observable的,使用
this.route.snapshot.paramMap
直接获得内容.
通常选observable的,这样可以只是更新数据而不需要重新构造组件,性能好.
按照参数用途分
- 必选参数(就像上面的id)
- 可选参数(可有可无)
可选参数的一个场景是:
从详情页面返回列表页面,可以选择性地带上该详情页面的id,并在列表页面中突出显示刚才点击的那个入口.
使用举例:
1 | // 详情页面 |
另外可选参数在地址栏中的显示,使用的是 矩阵url 标记法
1 | https://path/to/component;id=15;foo=foo |
路由出口与第二路由
路由出口
路由出口其实是父级路由对应的组件的模板中,为子路由预留的占位符.
假如有以下结构的路由
1 | - crisis-center |
一般情况下可能直接用list来做center的页面,不过这里多套了一层也没关系.
在list页面中,如果点击了某个条目则在下方显示detail,
如果没有点击,则在下方显示默认的welcome信息(center-home组件里面).
每个组件对应的模板为
1 | <!-- crisis-center --> |
这样,路由嵌套起来的页面就是
1 | <!-- crisis-center --> |
需要注意的是如果父路由对应的组件中没有outlet,那么子组件就无法显示出来,
除非父路由不使用组件,那么子路由对应的组件就会去找上级outlet.
第二路由
一个组件模板不仅仅可以有一个outlet,也可以有多个outlet.
比如想在页面上划分一个区域,这个区域显示的内容在用户切换路径时都不变.
一种方法是另外建立一套路由树并将页面划分出一部分用于显示这个路由树.
angular的想法正好是划分出一个outlet,并让该outlet响应单独的一套路由树.
不过为了管理方便,只允许有一个未命名的outlet作为默认,其他outlet都需要命名.
1 | <div [@routeAnimation]="getAnimationData(routerOutlet)"> |
如果一个路由需要使用名为popup的出口,则需要指定outlet的名称
1 | // 路由中指定 |
而这无疑给浏览器出了个难题,不过如今浏览器的显示可能是
1 | https://simple/path(outlet-name: second-route-tree-path) |
路由守卫
Angular提供了多种路由守卫,在特定场景下拒绝对特定路由的访问,具体实现方法见常见用例
- CanActivate (没有登陆不能访问)
- CanActivateChild (没有权限不能访问子页面,只能看个首页)
- CanDeactivate (页面数据没有保存不能离开)
- Resolve (为了用户体验,需要先见准备数据再进入页面而不是先进入页面再准备数据)
- CanLoad (满足条件才能开始加载一个模块,比如管理员登陆不成功就永远不加载管理相关模块,保证性能,
严格来说不仅仅用来拒绝对路由的访问了)
其实我看来守卫是路由定义时的字段,程序员来实现的,其实是用来答守卫话的代理人.
而如何答话,angular定义好了 接口,接口里面有相应的函数.
守卫的基本行为逻辑决定了其返回值的类型.
- 返回true表示可以继续导航
- 返回false表示导航终止,留在原地
- 返回UrlTree则表示导航终止,同时导航到该UrlTree.
不过个人认为守卫返回false表意才更明确.
至于想重新导航,完全可以在函数体中发起一个新的导航.this.router.navigate()
- 有时候守卫不能立即获得答案(比如向用户提出警告并要求确认),需要返回能够异步处理的类型.
以允许表面有阻塞的对话框的同时,背地里却依然可以做耗时处理.珍惜一分一秒挖个矿什么的.
这些可观察对象还必须是可结束的.- Obesrevable<boolean>
- Promise<boolean>
时常会遇到多层守卫的情况,此时就像从公司请假回家过年.
先从基层问起,canDeactivate?出了公司后,依次从大地方过安检去小地方.每次都问canActivate?
CanActivate
使用场景举例:
- 没有登陆不能访问
- 不是管理者不能访问
首先定义方法本身
1 | export class AuthGuard implements CanActivate { // 需要实现CanActivate接口里的canActivate方法 |
然后就能在路由定义时使用了
1 | const adminRoutes: Routes = [ |
CanActivateChild
注意到其实CanActivate只能保护 /hero
而不能连带 /hero/1
一起保护.
因此需要canActivateChild.
1 | export class AuthGuard implements CanActivate, CanActivateChild { // 1. 继承接口 |
在路由中可以这样使用
1 | { |
附带一个可能有用的checkLogin函数
1 | checkLogin(url: string): true|UrlTree { |
CanDeactivate
使用场景举例:
- 用户修改了form内容,想要离开
与上面两个守卫不同的是,canDeactivate函数必须再指定一个component的引用.
(如果用来做页面上form内容的校验,则很可能要用到组件本身)
1 | canDeactivate( |
守卫本身的实现方式上有不同的做法
-
为每个组件都定义一个专用的canDeactivate守卫,守卫亲历亲为验证组件的属性.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16export class CanDeactivateGuard implements CanDeactivate<CrisisDetailComponent> {
canDeactivate(
component: CrisisDetailComponent,
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | boolean {
// 引用compnent来检查内容,如果没有变就返回true
if (!component.crisis || component.crisis.name === component.editName) {
return true;
}
// 如果变了就提问用户,该方法返回observable
return component.dialogService.confirm('Discard changes?');
}
} -
每个组件自己写好校验方法.然后写一个通用的守卫,只是调用一下组件内的方法.
1
2
3
4// 假设所有组件都实现了该接口
export interface CanComponentDeactivate {
canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}1
2
3
4
5
6export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
canDeactivate(component: CanComponentDeactivate) { // 可能参数个数并不是固定死的?TODO
// 如果该组件有canDeactive方法则执行,否则直接返回true
return component.canDeactivate ? component.canDeactivate() : true;
}
}
在路由中使用起来很简单 canDeactivate: [CanDeactivateGuard]
Resolve
用户从列表页面进入详情页面后,等了一会儿, ngOnInit
才调用service把数据加载成功.过程中看到的是白页面.
使用上体验不如,用户进入页面前先在一个 工具函数 xxx
中调用service准备了数据,
然后带着数据导航到详情页面(当然如果找不到数据,就需要终止路由).
因此 工具函数 的功能有:
- 能够获取数据(需要注入获取数据的service)
- 能够控制路由接下来要如何做.(需要注入router)
- 能够返回一个特定类型的数据(为了返回值能够有类型,定义时就需要类型提示)
1 | export class CrisisDetailResolverService implements Resolve<Crisis> { |
路由中使用时要注意构造一个对象,用于存放resolver的返回值
1 | { |
在接下来的组件中要接收来自路由的数据并显示
1 | ngOnInit() { |
惰性加载(异步路由)
许多系统中都有类似的例子,为了加快启动速度,
一开始只加载一些必要的模块,当用户真正用到某些特定的模块时,才加载之.
angular也是如此.
做法上angular似乎采用了ES6里那种返回promise的动态rimpot方法.
触发的时间点通常是当用户访问了指定的path或满足了一定条件.
比如如下路由就是当用户真的访问了admin路径时才开始加载admin相关模块.
1 | // app-routing.module.ts |
不过这里需要注意,外层的app路由中已经指定了路径,那么模块自己的路由中,顶层的路径就不再需要字符
1 | // admin-routing.module.ts |
最后要保证根模块中不再引用这个feature模块,完成真正的动态加载
1 | @NgModule({ |
CanLoad
CanLoad是一个路由守卫的接口,允许angular在异步加载模块时有了更多的判断标准,
而不仅仅是用户是否访问了特定的路径.
比如用户虽然访问了admin路径,但却没能登陆成功,此时没有必要加载admin相关模块.
1 | export class AuthGuard implements CanActivate, CanActivateChild, CanLoad { // 注意接口 |
然后路由中就可以使用该守卫
1 | { |
预加载
有一些虽然不必要但常用的模块,希望让其稍微延迟一会儿,
等必要的加载完了,用户能看到界面了,就立即加载.而不是等待用户访问这个路径才慢半拍反应过来.
这种做法,angular起名叫预加载.
angular默认提供两种策略:
- 不做预加载,用户访问路径逼不得已才加载模块
- 预加载所有模块
有时会觉得这个方法一刀切,希望自定义一个策略:
- 看路由配置,如果有
{ preload: true }
则预加载
为了达成这一效果,angular认为可以做一个 service
来实现
1 | export class SelectivePreloadingStrategyService implements PreloadingStrategy { |
然后可以在路由中使用
1 | { |
路由动画
在不同路由之间切换时可以加入动画.
Angular的动画基于state,切换路由也是切换动画的state,
而在两个state之间如何切换,则需要人来定义.
另外将动画呈现在哪个路由出口,什么时候启用动画(trigger),都需要自行定义.
要使用动画,需要几个步骤
-
在app模块中引入
BrowserAnimationsModule
-
通常是在最外层的app组件中,引用动画内容.
-
模板中要定义哪个路由出口要使用该动画
1
2
3
4
5<!-- at符约定一个触发器 -->
<!-- 后面的函数中要将这个outlet作为显示动画的对象 -->
<div [@routeAnimation]="getAnimationData(routerOutlet)">
<router-outlet #routerOutlet="outlet"></router-outlet>
</div> -
然后ts代码中要定义好要用的动画,其实有些看不懂
1
2
3
4
5
6
7
8
9
10
11import { slideInAnimation } from './animations';
({
// ...
animations: [ slideInAnimation ] // 动画集合的名称
})
export class AppComponent {
getAnimationData(outlet: RouterOutlet) {
return outlet && outlet.activatedRouteData && outlet.activatedRouteData.animation;
}
}
-
-
想好要用那些state
比如想在英雄列表(计划绑定heroes状态)和英雄详情页面(计划绑定hero状态)之间切换. -
定义动画的内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44// src/app/animations.ts
import {
trigger, animateChild, group,
transition, animate, style, query
} from '@angular/animations';
export const slideInAnimation =
trigger('routeAnimation', [ // trigger要照应组件中的定义
transition('heroes <=> hero', [ // 在heroes状态和hero状态之间切换
// 不知道做了什么,但缺了会导致页面混乱
style({ position: 'relative' }),
// 首先声明,无论是即将进入的组件还是要离开的组件
// 被移动的页面会被视为一个position属性为absolute的HTML元素
// 然后像对齐原点
query(':enter, :leave', [
style({
position: 'absolute',
top: 0,
left: 0,
width: '100%'
})
]),
// 即将进入的组件,先放在-100%,即屏幕的左侧
query(':enter', [ // enter代表即将进入视角的组件页面
style({ left: '-100%'})
]),
query(':leave', animateChild()), // 似乎什么也不影响
// 组合动画,300ms内,一个页面离开,一个页面进入
group([
query(':leave', [
// 离开的页面的left达到100%,向右移出屏幕
animate('300ms ease-out', style({ left: '100%'}))
]),
query(':enter', [
// 进入的页面left变为0,已经进入页面
animate('300ms ease-out', style({ left: '0%'}))
])
]),
query(':enter', animateChild()), // 似乎什么也不影响
])
]); -
路由中绑定路由与动画定格.
1
2
3
4
5const heroesRoutes: Routes = [
// ...
{ path: 'superheroes', component: HeroListComponent, data: { animation: 'heroes' } },
{ path: 'superhero/:id', component: HeroDetailComponent, data: { animation: 'hero' } },
]
总结: 路由定义时的字段
事实上参考 Route
类的属性就知道了
- path 地址
- pathMatch 如果有重定向的话,路径匹配的策略如何
- matcher 还没见过
- redirectTo 重定向到的地址
- outlet 路由出口
- component 用来处理请求的组件
- canActivate,canActivateChild,canDeactivate,canLoad,resolve等路由守卫
- data 数据
- children 如果是一个父路由,则代表子路由们
- loadChildren? 专门用于惰性启动
- runGuardsAndResolvers 目前还没见过