最近在使用angular来做项目,今天就简单的聊聊angular的路由管理。
首先了解一些简单的概念
什么是路由
无论是angular还是其它两个主流框架,整体上就是一个大的组件树,包括一些可以复用的UI组件或者业务组件。而路由的作用就是关于如何使用分配这些组件。它来觉得在某种情况下该显示哪些组件,隐藏哪些组件。
我们来看一个angular的路由配置:
const routes: Routes = [
{
path: 'home',
component: HomeComponent,
children: [
{
path: 'persons',
children: [
{
path: 'list',
component:PersonListCom,
}
]
},
{
path: 'single',
children: [
{
path: 'personDetail',
component: PersonDetailCom
},
{
path: 'personDetail/:id',
component: PersonDetailCom
}
]
},
{
path: '/', redirectTo: '/persons/list', pathMatch: 'full'
},
]
},
{
path: 'login',
component: LoginComponent,
}
]; Route对象定义应用程序中某些可路由状态(组件、重定向等)与URL段之间的关系。通过导入RouterModule并将路由配置数组传递给RouterModule.forRoot()在应用程序中声明性地指定路由器配置。
大家可以先不看具体代码,我将路由的配置结构转成了下面这张图。
整个路由配置就是一个组件树,每个节点代表一个路由,从图中可以看出,有的节点和组件相关联,在访问这个路由时,对应的组件就会加载展示在屏幕上。
路由访问
我们在访问路由的时候,就是访问组件树的某条子树。比如,我想要访问PersonListCom组件,那就是home/persons/list的路由访问顺序,路由节点对应的组件就会进行实例化。
需要注意的是,并不是所有的路由配置都是有效的,比如直接访问home路径,下面有两条子树,它们无法同时渲染,因为从标准上来说,outlet就是指组件具体放置在DOM中的位置,所以同一个位置只能放一个组件。所以我们添加了redirect配置,在无法访问的时候,进行重定向。
导航
路由的一个重要的功能就是在不同的路由状态之间进行切换,并且更新组件的实例。
从路由访问部分可以看出,要访问某一个路由节点,就是将某条子树的节点串联起来,比如:home/persons/list,从另一个角度来说,这条节点串就是我们常说的URL,即,URL就是路由的扁平化标识。
而导航的作用就是从一个路由到另一个路由状态的切换。我们在访问不同页面的时候,主要就是修改浏览器中的URL地址,框架会分析新的路由,隐藏旧的组件,并将新的组件渲染出来加载到页面上。
所以路由器只允许我们表达应用程序可能处于的所有潜在状态,并提供一种从一种状态导航到另一种状态的机制。
与原生应用程序相比,URL栏为web应用程序提供了巨大的优势。它允许我们引用状态,为它们添加书签,并与我们的朋友共享它们。在性能良好的web应用程序中,任何应用程序状态转换都会导致URL更改,而任何URL更改都会导致状态转换。换句话说,URL只不过是序列化的路由器状态。angular路由器负责管理URL,以确保它始终与路由器状态同步。
URL访问过程
路由器的核心是一个强大的URL匹配引擎。如果不能将url与要呈现的适当组件集相关联,则无法在应用程序中导航。所以我们需要了解整个URL的访问过程
比如我们要访问:home/single/personDetail/23
一个angular路由对url的处理过程:
- 处理重定向
- 识别路由状态
- 进行路由守卫和处理数据
- 激活所有相关的组件
- 管理导航
处理重定向
重定向:重定向是URL部分的替换。重定向可以是本地的,也可以是绝对的。本地重定向用不同的段替换单个段。绝对重定向替换整个URL。重定向是本地的,除非您在url前面加上斜线。
当用户点击跳转时,URL地址会发生变化,router就会检测到这种变化,首先要处理的就是是否需要重定向。重定向可以在路由器配置树中的每个嵌套级别发生,但每个级别只能发生一次。这是为了避免任何无限的重定向循环。
在前面的配置中有一个重定向的配置path: '/', redirectTo: '/persons/list', pathMatch: 'full',使用/persons/list 替换 /,这就是一种绝对替换。
我们要访问home/persons/list,不是/所以不会发生重定向。
识别路由状态
下一步就是路由状态的识别。路由会从URL中获取到路由状态,然后路由器一个接一个地遍历路由配置数组,检查URL是否以路由的路径开始。如果无法匹配到正确的路由,导航跳转就会失败。但如果它完成匹配,代表应用会构造出将要跳转页面的路由器状态。
路由器状态由激活的路由组成。每个激活的路由可以与一个组件相关联。另外,请注意,我们总是有一个激活的路由与应用程序的根组件相关联。
路由守卫
在这个阶段,我已经有了将要跳转页面的路由状态。接下来就要通过路由守卫来检测是否允许完成这次跳转。
import { HomeGuard } from './page/home/home.guard';
{
path: 'home',
canLoad: [HomeGuard],
canActivate: [HomeGuard],
canActivateChild: [HomeGuard],
} 可以看到这部分代码,有canLoad、canActivateChild、canActivate几个节点,表示在路由执行的这几个节点都需要执行HomeGuard文件中的判断方法来检测是否允许执行下面的步骤。如果所有守卫都返回 true,就会继续导航。如果任何一个守卫返回了 false,就会取消导航。
HomeGuard中包括一下几个接口,对应不同阶段的处理。
/**
* 决定该路由能否激活
**/
canActivate(){
}
/**
* 来决定该路由的子路由能否激活
**/
canActivateChild() {
}
/**
* 是否可加载模块
*/
canLoad(){
} 数据处理
在通过路由守卫之后,就可以处理数据了。在这个路径中,23是要访问的用户id,我们需要通过这个id参数获取用户的数据。
在日常业务中,有时候只需要在组件加载完成之后,组件自己获取到URL中的参数,请求数据就可以了。但有些时候,需要在页面加载之前去获取用户数据,如果有数据,就将数据传给组件去渲染,如果没有就停止加载页面。
[
{
path: 'single',
children: [
{
path: 'personDetail',
component: PersonDetailCom
},
{
path: 'personDetail/:id',
component: PersonDetailCom,
resolve: {
conversations: ConversationsResolver
}
}
]
}
] 在下面,我们定义了ConversationsResolver。
@Injectable()
class ConversationsResolver implements Resolve<any> {
constructor(
private repo: ConversationsRepo,
private currentPerson: Person) {}
resolve(route: ActivatedRouteSnapshot, state: RouteStateSnapshot):
Promise<Conversation[]> {
return this.repo.fetchAll(
route.paramMap.get('id'),
this.currentPerson
);
}
} 当导航到single/personDetail/23的时候,会获取到当前路由的状态,包括参数id:23。使用这个参数请求用户的Conversation数据。
然后在组件PersonDetailCom中,可以获取到resolver中请求获取到的数据。
@Component({
template: `
<conversation *ngFor="let c of conversations | async">
</conversation>
`
})
class PersonDetailCom {
conversations: Observable<Conversation[]>;
constructor(route: ActivatedRoute) {
this.conversations = route.data.pluck('conversations');
}
} 激活组件
在这个阶段,我们通过已经获取到的路由状态实例化需要的组件,将它放到对应的router-outlet。
下面看下怎么使用router-outlet,我们在HomeComponent中添加两个outlets:默认的和header。
@Component({ template: `
...
<router-outlet name="header"></router-outlet>
...
<router-outlet></router-outlet>
`
})
class HomeComponent { } 我们可以在路由配置中指定组件渲染的outlet。
[
{
path: 'single',
children: [
{
path: 'personDetail',
component: PersonDetailCom
},
{
path: 'personDetail/:id',
children: [
{ path: '', component: PersonDetailCom },
{
path: '',
component: HeaderCom,
outlet: 'header',
}
],
resolve: {
conversations: ConversationsResolver
}
}
]
}
] 在路由personDetail/:id下通过children添加了两个组件PersonDetailCom和HeaderCom,并且HeaderCom指定了outlet是header。
这样,PersonDetailCom组件实例化之后会被放在默认的outelet上,HeaderCom会被放置在header上。
组件中获取参数,请求数据
除了配置resolver之外,还可以在组件中获取参数id,请求对应数据。
@Component({...})
class PersonDetailCom {
conversation: Observable<Conversation>;
id: Observable<string>;
constructor(r: ActivatedRoute) {
// r.data is an observable
this.conversation = r.data.map(d => d.conversation);
// r.paramMap is an observable
this.id = r.paramMap.map(p => p.get('id')); }
} 通过ActivatedRoute可以获取到URL上的参数,通过参数获取数据。
导航
在这一点上,路由器已经创建了一个路由器状态并实例化了组件。接下来,我们需要能够从这个路由器状态导航到另一个路由器状态。有两种方法可以实现这一点:
- 强制调用router.navigate
- 声明性地使用RouterLink指令
this.router.navigateByUrl('/home/single/personDetail/23');
这样就完成了路由的整个处理过程。
成功匹配URL的结果是,一些组件集将被路由到,并通过使用`router outlet`指令在屏幕上呈现。但此操作还有一个有用的副作用——创建RouterState和RouterStateSnapshot对象。
### RouterState和RouterStateSnapshot
路由完成后,我们可能希望访问有关URL和路由到的组件集(称为当前路由器状态)的信息。我们介绍一下`RouterState`和`RouterStateSnapshot`属性,它允许我们访问有关当前路由到的URL和组件的信息。
在重定向之后,就会获取到将要跳转页面的路由器状态`RouterStateSnapshot `。
**routestastesnapshot是一种不可变的数据结构,表示路由器在特定时刻的状态。**在组件增加、删除或者参数变化的时候就会创建新的snapshot。
**router state与RouteStateSnapshot类似,只是它表示路由器随时间变化的状态。**
#### RouterStateSnapshot
```javascript
interface RouterStateSnapshot {
root: ActivatedRouteSnapshot;
}
interface ActivatedRouteSnapshot {
url: UrlSegment[];
params: {[name:string]:string};
data: {[name:string]:any};
queryParams: {[name:string]:string};
fragment: string;
root: ActivatedRouteSnapshot;
parent: ActivatedRouteSnapshot;
firstchild: ActivatedRouteSnapshot;
children: ActivatedRouteSnapshot[];
}从定义可以看出RouterStateSnapshot是一个已经激活的路由树,此树中的每个节点都知道“已使用”的URL段、提取的参数和已解析的数据。
每个节点头可以通过parent和children属性访问它的父节点和子节点。
当我们导航到/home/single/personDetail/23时,路由器将查看URL并构造以下RouterStateSnapshot:
现在,我们再导航到/home/single/personDetail/24,那么它的snapshot会变为
为了避免不必要的DOM修改,当相应路由的参数改变时,路由器将复用这些组件。所以在这个例子中id由23变成了24,组件复用意味着我们不能将ActivatedRouteSnapshot插入到PersonDetailCom中,那么这时候组件的id还是23,数据就存在问题了。
snapshot路由状态是保存某一时刻的路由数据,这也是它被称为snapshot的原因。但是组件会一直复用,而参数是会不断变化的,这时候RouterStateSnapshot就不能满足我们的需求了。
这就需要另一个数据结构了:RouterState。
RouterState
interface RouterState {
snapshot: RouterStateSnapshot; //returns current snapshot
root: ActivatedRoute;
}
interface ActivatedRoute {
snapshot: ActivatedRouteSnapshot; //returns current snapshot
url: Observable<UrlSegment[]>;
params: Observable<{[name:string]:string}>;
data: Observable<{[name:string]:any}>;
queryParams: Observable<{[name:string]:string}>;
fragment: Observable<string>;
root: ActivatedRout;
parent: ActivatedRout;
firstchild: ActivatedRout;
children: ActivatedRout[];
} 它的结构和RouterStateSnapshot相似,只不过它暴露出来的值都是可观察的,这对于处理获取随时间变化的值非常有用。
路由器实例化的任何组件都可以注入ActivatedRoute。
@Component(...)
class PersonDetailCom {
id: Observable<string>;
constructor(r: ActivatedRoute) {
this.id = r.data.map(d => d.id);
}
}在id由23变为24的时候,可以获取到最新的id值,通过此值作为参数请求我们需要的数据。
ActivatedRoute
ActivatedRoute提供对url、params、data、queryParams和fragment observates的访问。因为用户访问页面是通过更改URL实现的,所以URL的变化是引起路由变化的原因。
每当URL改变时,路由器就从中派生出一组新的参数:路由器接受匹配URL段的位置参数(例如':id')和最后一个匹配URL段的矩阵参数并将它们组合起来。此操作是纯操作:必须更改URL才能更改参数。或者换句话说,相同的URL将始终导致相同的参数集。
接下来,路由器调用路由的数据解析器,并将结果与提供的静态数据相结合。由于数据解析器是任意的函数,路由器无法保证在给定相同的URL时,您将获得相同的对象。URL包含资源的id,该id是固定的,数据解析器获取该资源的内容,这些内容通常随时间而变化。
@Component({...})
class ConversationCmp {
constructor(r: ActivatedRoute) {
/**
* 获取url
**/
r.url.subscribe((s:UrlSegment[]) => {
console.log("url", s);
});
/**
* 获取参数
**/
r.params.subscribe((p => {
console.log("params", params);
});
}
} 看下关于data的处理,在路由配置中可以添加data属性,给对应路由的组件传递固定的data值。data属性用于将固定对象传递到激活的路由。它在应用程序的整个生命周期内都不会更改。
{
path: 'personDetail/:id',
children: [
{ path: '', component: PersonDetailCom },
{
path: '',
component: HeaderCom,
outlet: 'header',
}
],
data: [
personStatus: 'activited'
],
resolve: {
conversations: ConversationsResolver
}
} 可以通过订阅ActivatedRoute中的data属性,获取对应的值。
@Component({...})
class MessageCmp {
constructor(r: ActivatedRoute) {
/**
* 获取路由配置中的data
**/
r.data.subscribe((d => {
console.log('data', d);
});
}
}好了,今天先介绍这些,希望对大家有帮助。
参考文章:
Angular Router: Understanding Router State
The Three Pillars of the Angular Router
Angular-router
订阅“前端记事本”,了解最新的前端信息。

京公网安备 11010502036488号