Angular2 路由基础教程

2,562 阅读14分钟

路由,也就是从一个页面跳转到另一个页面。在传统的web系统中,每一个页面都是从服务器获得,当用户点击页面上的某一个链接时,就会往服务器上发送一个http的请求,服务器上的某一段程序响应这个请求,处理并返回另一个html页面。所以在传统的web系统中,路由的定义都是在服务器端。
在单页应用中,页面的跳转不经过服务器,如果由纯JavaScript实现的话,当用户点击一个按钮或链接,打开一个页面时,实际上由某个js程序根据请求的类型,将某一个div的html页面加载到页面上的某一个区域中显示。当这个单页应用的页面比较多、业务比较复杂时,页面之间的跳转、数据传递、状态保存等都是一件非常麻烦的事情。
Angular2包含了一个路由框架,我们只需要定义一个个的路径、和它对应的组件,然后在页面跳转时也使用Angular2的方式,我们就能够很方便的实现路由控制。这个系列的文章将会逐步的介绍Angular2路由的使用、通过路由设置实现验证和授权,以及子模块和子路由模块的异步加载之类的问题。在这篇文章中,我们就先来看看在Angular2中是如何使用路由的。

Angular2的路由主要包括下面4个部分:

  • 路由定义
    路由定义,通俗来说定义的就是一个URL路径,打开的是哪个页面,由哪个控制器来控制数据交互和用户交互。在Angular2中,这个控制器就是组件(Component),页面就是在组件定义中定义的这个组件对应的模板页面。
  • 路由器
    路由器(Router),也就是分发器。它是由Angular2的框架实现。当我们点击一个链接时,就是由它来确定要打开哪一个组件,怎么封装和传递参数等。
  • 导航
    导航,也就是从一个页面打开另一个页面。一般有两种方式,一种是通过页面上的一个链接link,另一种是在js里面使用代码导航。
  • 参数
    当我们在页面之间跳转时,通常都需要传递参数。除了常用的通过url参数来传递以外,在REST风格的路径设计中,我们经常需要使用某一个id来作为url的一部分,也就是说把参数放在url里面

下面就来详细看看具体怎么定义和使用。

路由定义

首先,我们需要定义我们的路由,也就是路径-组件的对应关系。通常我们会创建一个单独的文件app.routes.ts,基本的内容如下:

import { Routes } from '@angular/router';
export const routes: Routes = [
    {
        path: '',
        redirectTo: '/todo/list',
        pathMatch: 'full'
    },
    {
        path: 'todo/list',
        component: TodoListComponent
    },
    {
        path: 'todo/detail/:id',
        component: TodoDetailComponent
    }
    ];

首先我们引入Routes,它其实就是一个路由列表类型Route[],而Route是Angular路由框架定义的一个接口。最基本的路由包括2个属性:pathcomponent,分别是这个路由对应的URL路径,和这个路径对应的组件。而我们的组件定义中包括模板、样式和控制器。所以,当在浏览器上打开这个url的时候,就会创建相应的组件,并把模板页面渲染后显示到页面上。
一般情况下,用户都是通过输入域名等打开我们的应用,因为用户没有在域名后面输入路径,这时候对应的url就相当于是''。对于这个路径,我们可以设置一个组件,也可以重定向到另一个路径。就像上面使用redirectTo: '/todo/list',重定向到任务列表。当我们使用redirectTo重定向时,需要pathMatch来指定匹配方式,也就是如何匹配上面的''。我们可以完全匹配full,也可以匹配前缀prefix。如果我们把上面的例子改成:

{
    path: '',
    redirectTo: '/todo/list',
    pathMatch: 'prefix'
    }

就会变成不管用户打开任何路径,都会重定向到'/todo/list',因为任何路径都能看作是有一个空字符串的前缀。
现在我们定义到了路由,但是,我们还得告诉Angular来’使用’这个路由定义,这样这些定义才能起作用。这就需要在AppModule,一般也就是app.module.ts文件中来设置:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { routes } from './app.routes';
@NgModule({
    imports:        [BrowserModule, RouterModule.forRoot(routes)],
    declarations:   [AppComponent],
    bootstrap:      [AppComponent]
    })
    export class AppModule {}

我们先引入了Angular2的路由模块RouterModule,然后在下面的imports里面,通过RouterModule.forRoot(routes)用路由模块引用之前定义的路由设置。

在一些实例中,是在app.routes.ts文件中定义路由模块:

import { NgModule }     from '@angular/core';
import { RouterModule } from '@angular/router';
@NgModule({
  imports: [
    RouterModule.forRoot([
        {
            path: '',
            redirectTo: '/todo/list',
            pathMatch: 'full'
        },
        {
            path: 'todo/list',
            component: TodoListComponent
        },
        {
            path: 'todo/detail/:id',
            component: TodoDetailComponent
        }
    ])
  ],
  exports: [
    RouterModule
  ]
  })
  export class AppRoutingModule {}

然后在AppModule中的imports中直接引入这个AppRoutingModule
这两种方式其实都是一样,只不过就是RouterModule.forRoot(...)这部分是在哪儿定义的问题。
但是,如果我们要定义子模块和子模块路由,就需要使用AppRoutingModule的方式。在你的开发中,也建议使用路由模块的定义方式。

设置载入点

然后,我们还需要一个载入点,来告诉Angular我们的这个路由对应组件的页面,要载入到页面上哪一个地方,这个就是RouterOutlet
一般情况下,我们是在AppComponent组件对应的模板app.component.html里面,设置这个应用的布局,例如上面的header或toolbar,还有侧边栏的菜单,底部的footer等,然后在中间添加一个router-outlet来作为载入点。例如下面的就是一种布局方式:


.....

至此,我们的最基本的路由就可以起作用了。在浏览器上输入一个url,就应该能打开相应的页面。

设置base href

有时候,我们需要把我们的应用部署在某一个路径service1下,如果部署到服务器上,就是http://thedomain.com/service1/。这时候,我们就需要设置应用的base href,只需要修改index.htmlbase

如果读者有过Angular1.x的开发经验,可能就会注意到,在Angular2里,url中就没有再出现#,这是因为在Angular2中默认使用的是HTML5风格的导航。如果你确实需要使用#,可以在你的AppModule中设置:

import { LocationStrategy, HashLocationStrategy } from '@angular/common';
// 省略其他
@NgModule({
    providers: [{
        provide: LocationStrategy, // 导航路径的策略设置
        useClass: HashLocationStrategy // 使用'#'方式的策略
    }]
    })
    export class AppModule { }

由于我们没有使用#,那么,如果你的应用部署到了服务器上,比如nginx,当你想直接打开某一个url,如:
http://thedomain.com/todo/list,就会发生404的错误,因为nginx会试图从你部署的路径出发,找子目录’todo/list’,显然这不是子目录,就会出现404错误。所以,我们就需要对nginx进行设置,具体方法可以看之前的一篇文章:angular1.x使用html5模式去掉#

在Angular2的开发环境不会有这个问题,是因为angular-cli工具帮我们实现了类似的配置。

导航

使用链接导航

接下来,我们再看看各个页面之间的跳转,也就是导航该怎么做。先来看看链接方式。传统的链接,其实就是这样:

这是一个链接

但是,我们需要让Angular2的路由框架来接管导航的过程,所以就需要用他的方式来创建链接:

这是一个链接

routerLink="/todo/list"就是链接的定义方式,后面的routerLinkActive="active"表示如果当前的路由处于被激活的状态时,在这个元素上,就添加一个active的css类。也就是说,如果你打开了/todo/list这个路径,那个链接上就会添加一个active的css类。而且,即使是这个路由的子路由处于激活状态,也是如此。通过这个active,我们就可以通过css来定义某一个路由被激活以后,页面上指向这个路径的链接元素的样式。

代码中导航

有时候,我们需要在代码中根据具体情况跳转到不同的页面,我们可以通过Angular2提供的路由器Router来实现。
假设我们要在某一个组件中实现跳转,首先,在这个组件的构造方法中,我们需要注入Router,然后在需要的时候调用它的navigate(newUrl)方法。具体如下:

export class TodoItemComponent {
    constructor(private todoService: TodoService, private router: Router) {
    }
    gotoDetail(todo) {
        this.router.navigate(['/todo/detail', todo.id]);
    }
    }

传递参数

当我们实现页面挑战时,往往都需要传递一些参数,如果说到组件之间的通信好数据传递,有很多种方式,这是一个比较大的话题。如果只是在使用路由的时候传递参数的话,有2种方式:

  • 路径方式。参数作为路径的一部分,例如/todo/detail/12,其中的’12’就是参数,代表任务Id。
  • 参数方式。这种方式是把参数放在URL的参数里,例如/todo/detail?id=12&type=important
    对于这两种方式,虽然没有严格的规定说什么时候必须用什么类型,但是,合理的设计你的路由,对应用的开发和维护都能带来很大的便利。设计良好的路由有助于更好地利用子路由模块、基于路由验证授权等方法,使得我们的应用结构更加清晰、易于维护。具体的设计方法可以参考REST风格的路径设计。

跟导航对应,我们分别看看在链接和代码中怎么传递参数。

使用routerLink传递参数

要在链接中使用参数,假设要实现上面的/todo/detail/12链接,我们需要使用下面的方式:

在这里,我们使用[routerLink]的方式进行数据的绑定,绑定的值,就是['/todo/detail', item.id],他是一个表达式,Angular会把这个列表中的2个数据解析后拼接在一起,生成’/todo/detail/12’的链接。如果我们的连接格式是’/todo/12/detail’,那就是这样:

如果,要传递的参数是这种/todo/detail?id=12&type=important方式,也很简单:

使用代码传递参数

其实在代码中添加参数,跟上面的方法类似。对于/todo/detail/12类型的链接:

onSelect(item: Item) {
  this.router.navigate(['/todo/detail', item.id]);
  }

那么,要实现这种/todo/detail?id=12&type=important路径的导航,该怎么办呢?那就是:

onSelect(item: Item) {
  this.router.navigate(['/todo/detail'], { queryParams: { id: item.id, type: 'important' } });
  }

在这里,再说明一下使用TypeScript来开发Angular2应用给我们带来的一个好处。如果你使用VisualStudio Code或Sublime Text之类的编辑器,安装了相应的TypeScript的插件以后,你就可以很方便的查看类型定义和说明。例如,在上面的navigate(...)方法点右键或通过快捷键,就可以跳转到这个方法的定义文件里。这个类型定义文件里都有非常好的注释,只要你的英语不是很差,就能够通过注释了解一个方法的使用、参数的类型等。这些都是TypeScript的类型给我们带来的便利。

获取参数

知道了怎么把参数传过去,接下来我们再看看怎么获取传递过来的参数的值。当我们打开某一个路径时,也就是要创建这个路径对应的组件,并把它的模板显示到页面上。这个路由器作用的过程大致是这样:

  1. 先是从打开一个url开始
  2. 路由框架找到这个url对应的路由定义。
  3. 路由框架将url、参数等信息封装成一个Route对象。
  4. 路由框架根据找到的路由对应的组件创建组件。在这个组件的构造方法中,如有需要,就把这个路由注入进去。
  5. 在组件初始化的过程中,从当前路由中获得参数,并根据参数初始化数据等。剩下的就是渲染组件模板,就不是路由框架的事情了。
    理解了这个过程,我们才能正确的使用路由。接下来,在看怎么使用之前,我们先看看2种使用场景:
  6. 从当前页面的路径/todo/list,跳转到详情页/todo/1,回到/todo/list,再打开/todo/2
  7. 从当前页面的路径/todo/1,跳转到/todo/2,再跳转到/todo/4等等。
    这两个场景的区别就是,第一个场景是不同的组件来回切换,组件不一样,显示的页面不一样。第二个场景实际上就是一直都显示的同一个组件,只是根据参数id的不同,显示不同的数据。

我们先来看第二种场景。因为Angular2中大量使用了Observable,路由的参数,其实也是一个Observable对象。所以,我们从当前路由获得参数的时候,实际上是从Observable中获得一个一个的参数实例,然后从每个参数实例中获取需要的参数。所以对于上面说的第二种使用场景,Angular2的路由框架提供了非常直接的解决方法。
说了这么多,我们再来看看实现代码:

import { ActivatedRoute, Router, Params } from '@angular/router';
// 省略其他
export class TodoDetailComponent implements OnInit {
  selectedTodo: Todo;
  constructor(private route: ActivatedRoute
                private todoService: TodoService) { }
  ngOnInit() {
    this.route.params.forEach((params: Params) => {
      let todoId = +params['id']; // 使用+将字符串类型的参数转换成数字
      this.selectedTodo = this.todoService.getTodoById(todoId);
    });
  }
  }

在这个组件中的构造方法中,我们注入当前激活的路由ActivatedRoute赋给内部变量route。这个组件实现了一个接口OnInit,所以他需要实现一个方法ngOnInit(),这个方法会在这个组件创建成功后被调用。在这个初始化方法里,我们对当前路由的每个参数进行如下处理:

let todoId = +params['id']; // 使用+将字符串类型的参数转换成数字
this.selectedTodo = this.todoService.getTodoById(todoId);

注意我们是在初始化方法里面实现的。也就是说,我们给当前路由的参数,也就是一个Observable的对象注册了一个数据处理方法,每当有新的参数的时候,这个方法就会被调用。这就好像给一个数据源注册了一个数据处理方法,每当这个数据源有新数据的时候,这个处理方法就会被调用。

接下来,我们再看看对于第一个场景该怎么办。也就是路由上的参数不会发生变化,组件创建以后,不需要监听新的参数。实际上,我们完全可以使用上面的方式来实现。虽然我们监听了参数,但是实际上参数不会发生变化。所以处理方法也只会被触发一次。
但是,既然参数不会改变,还是用Observable的处理方式,总是觉得不太美好,好在Angular路由框架也提供了’一次性’的处理方式。还是直接看代码:

import { ActivatedRoute, Router, Params } from '@angular/router';
// 省略其他
export class TodoComponent implements OnInit {
  selectedTodo: Todo;
  constructor(private route: ActivatedRoute
                private pickupService: TicketPickupService) { }
  ngOnInit() {
      let todoId = +this.route.snapshot.params['id'];
      this.selectedTodo = this.todoService.getTodoById(todoId);
  }
  }

这种实现方法,跟上面的相比,就是使用了route.snapshot.params['id']snapshot的意思是’快照’,就是根据当前的路由产生一个快照,这个快照只有当前的参数和其他状态等信息。

上面说的是针对路径中的参数,如果我们要获得的参数是/todo/detail?id=12&type=important里面的参数,我们只需要将上面的代码中route.params换成route.queryParamssnapshot.params换成snapshot.queryParams即可。

总结

至此,我们对Angular2的路由的使用做了一个大概的介绍。Angular2的路由框架,借鉴了服务器端开发框架的路由的思想,使得我们的路由规则的定义和组件的实现分离。我们使用Angular2开发web应用,就好像,我们只需要开发一个个的零件,至于这些零件怎么相互作用最终变成一个复杂的机器,大部分由框架帮我们完成。
其次,Angular2的路由框架,使用了Observable来实现一些数据的传递。这就要求我们转变相关数据处理的思维方式,学会用Observable的方式来处理数据。