路由对于单页应用来说是至关重要的。在单页应用中,页面只在应用启动的时候加载一次,当用户需要在应用的不同内容之间切换时必须通过路由来实现。
angular路由的配置还是非常简单的,但是你有没有想过当点击一个链接,触发路由导航时究竟发生了哪些事情?这篇文章中我们将讨论这个问题,也就是路由导航的周期。
在这里先提出三个问题:
对于特定的url,应该被导航到哪一个组件?
是否能通过路由导航到某个组件上?
是否需要在导航到这些组件之前获取一些数据?
带着这些问题我们来了解导航的细节:
路由从开始到结束的整个处理过程。
路由是如何被构建的,ActivatedRouteSnapshot对象的使用方法。
使用指令渲染路由的内容。
下面这张图呈现了整个路由导航的过程:
导航
angular创建的是一个单页应用,也就是说当页面的url变化时并不会重新加载一个新的页面,而是在浏览器中基于url上的位置信息展示相应的内容,对于用户来说就好像看到了一个新的页面。
url的变化会触发导航行为的发生,也就是触发路由行为,这与使用锚点,通过href获取新的页面不同,后者会导致整个页面重新加载。因此angular中给我们提供了[routeLink]指令,当点击动作发生时,这个指令会通知路由更新url,同时把相应的内容渲染到指定的位置上,而不是重新加载整个页面。
href='localhost:4200/users'>Users
[routerLink]="['/users']">Users
导航发生的时候,在页面呈现对应的组件之前会发生一系列其它的动作,导航成功之后新的组件被渲染到指定的地方,同时ActivatedRoute对象上会生成一个数形结构的数据以保存导航记录,目前我们只需要知道angular的路由会使用这个对象上的数据,当然开发人员也可以使用这个对象来查询路由的相关参数等。
示例
完整的示例可以在这里找到。
下面是这个示例的路由配置:
const ROUTES = [
{ path: 'users',
component: UsersComponent,
canActivate: [CanActivateGuard],
resolve: {
users: UserResolver
}
}
];
应用导航发生时会检查查询参数以确定用户是否已经登录(登录时:login=1),然后通过mock服务取回一些用户信息展示到页面上。
我们不需要关注这个应用的细节,只是想通过它来解释一下导航发生的整个过程。
导航周期和路由事件
angular里提供了Router服务,可以通过订阅它查看到导航时发生时的所有事件。
constructor(private router: Router) {
this.router.events.subscribe( (event: RouterEvent) => console.log(event))
}
在开发的环境中也可以通过路由配置来查看导航发生时的所有事件。
RouterModule.forRoot(ROUTES, {
enableTracing: true
})
在控制台上可以看到导航到 /users 时路由上发生的所有事件。
这些事件对于学习或调试路由来说很有用,比如可以通过订阅这些事件来很方便的添加一些提示信息。
ngOnInit() {
this.router.events.subscribe(evt => {
if (evt instanceof NavigationStart) {
this.message = 'Loading...';
this.displayMessage = true;
}
if (evt instanceof NavigationEnd) this.displayMessage = false;
});
}
当导航开始时,会显示'loading...',导航结束时消失。
导航开始
对应的事件:NavigationStart
在示例中,用户通过这个链接触发导航:
[routerLink]="['/users']" [queryParams]="{'login': '1'}">Authorized Navigation
在导航发生时,路由上同时会传递一个查询参数:login=1
用户一旦点击了这个链接就会触发导航,当然触发导航的方法不只这一种,还可以通过Router服务的方法,比如:navigate和navigateByUrl方法。
匹配Url和重定向
对应的事件: RouterRecognized
路由使用深度优先的原则在配置中查找url对应的路径,配置就是示例中定义好的ROUTES,如果配置上有重定向设置,此时会触发重定向。
显然在这个配置中并没有重定向,因此url:/users 将会匹配到写好的这个配置:
{ path: 'users', component: UsersComponent, ... }
如果匹配到的路径需要一个懒加载模块,此时将会从后台加载这个模块。
此时路由会发出RoutesRecognized事件以表明已经匹配到了一个url,接下来将会导航到某一个组件(UsersComponent)。文章开头提到的第一个问题在这里似乎可以被解答了,但是还得稍等一下,因为路由还需要确定一下是否允许导航到这个组件。
路由守卫
对应的事件:GuardsCheckStart, GuardsCheckEnd
路由守卫本质上就是一些返回布尔值的函数,通过返回的布尔值决定导航动作是否允许发生。作为开发者,可以通过设置路由守卫来控制导航是否可以发生。
在我们的示例中使用了canActivate守卫来检查用户是否已经登录。路由守卫需要在配置中指定。
{ path: 'users', ..., canActivate: [CanActivateGuard] }
下面是守卫函数:
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
return this.auth.isAuthorized(route.queryParams.login);
}
在这个函数中给 auth 服务的方法传入了路由上的查询参数,如果这个函数返回true,导航动作将被允许,否则被禁止。如果禁止导航,路由上会发出 NavigationCancel 事件,整个导航过程将被终止。
对于其它的路由守卫,比如:canLoad,canActivateChild,canDeactivate执行过程是一样的,只是功能不同。
守卫和服务一样,也是被注册在providers上的,可以通过依赖注入的方式使用。一旦url变化,路由被触发,这些设置在路由配置中的守卫就会运行。
现在就可以回答文章开头提到的第二个问题了,如果通过了路由守卫,接下来就可以获取数据。
需要注意的是,canActivate 守卫的运行肯定是先于获取数据的 resolve 守卫的,这很好理解,如果导航不能发生那么根本没有必要去获取数据。
路由解析器
对应的事件:ResolveStart,ResolveEnd
解析器本质也是函数,只是功能不同。它可以在导航的过程中预先获取数据,也就是说数据获取的过程发生在页面渲染之前。
在路由配置中也是通过指定一个字段添加resolve守卫:
{ path: 'users', ..., resolve: { users: UserResolver } }
userResolve的实现
export class UserResolver implements Resolve<Observable> {
constructor(private userService: MockUserDataService) {}
resolve(): Observable {
return this.userService.getUsers();
}
}
在通过了路由守卫的校验之后,路由将会使用这个resolve方法来获取数据,并将获取到的数据保存在ActivatedRoute服务上data对象下的users字段中(因为在配置中指定了守卫对应的key是users)。通过订阅data对象就可以得到已经获取到的数据。
activatedRouteService.data.subscribe(data => data.users);
在这个示例中,我们在UsersComponent上使用了ActivatedRoute服务来获取resolve拿到的数据。
export class UsersComponent implements OnInit {
public users = [];
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.route.data.subscribe(data => this.users = data.users);
}
}
Resolvers允许在导航过程中预先获取组件中所需要的数据 这可以避免给用户呈现一个不完整的组件,我们知道,组件的OnInit过程发生之后用户就可以看到对应的模板,如果此时再去获取数据的话,用户看到的模板可能是不完整的,因为数据还没有获取回来。
然而,有时先让用户看到一个不完整的模板可能更加合适 先加载数据还是先呈现页面完全取决于开发者。先让用户看到一个不完整的页面,同时配合一些加载动画等可能更加有利于提高用户体验。
一旦数据获取完成,接下来就是开始将对应的组件渲染到对应的router outlet指定的地方。
激活路由
对应的事件:ActivationStart, ActivationEnd, ChildActivationStart, ChildActivationEnd
路由通过从ActivatedRouteSnapshots树上查询信息,把对应的组件渲染到恰当的位置。路由配置中的component属性表明了需要实例化一个UsersComponent组件,同时可以看到users数据已经被取回来,放在data对象的users字段下。
这个过程是通过路由上的 activatedWith 方法来实现的。
这里不过分纠结细节,主要看几个关键点:
第9行,使用ComponentFactoryResolver创建了一个组件,组件的信息是在第7行通过ActivatedRouteSnapshot获取到的。
第12行,location就是router-outlet对应的ViewContainerRef,表明了在哪个位置激活路由。
路由对应的组件被渲染出来后,如果模板中还有router-outlet,也就是说有嵌套的子路由,将接着执行这个过程,把子路由上的组件也渲染出来。
更新url
最后一步,更新页面的url,显示对应的路径 /users
updateTargetUrlAndHref(): void {
this.href = this.locationStrategy.prepareExternalUrl(this.router.serializeUrl(this.urlTree));
}
路由继续监听url的变化,如果url改变了,以上过程将再次执行。