最近在使用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],
  }

可以看到这部分代码,有canLoadcanActivateChildcanActivate几个节点,表示在路由执行的这几个节点都需要执行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添加了两个组件PersonDetailComHeaderCom,并且HeaderCom指定了outletheader
这样,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

订阅“前端记事本”,了解最新的前端信息。

图片说明