最初,每个网页的都是独立的 HTML 页面。当我们打开一个网页时,浏览器需要向服务器请求页面,并把接收到的 HTML 文档渲染显示出来。当你点击一个链接,跳转到另外的页面时,浏览器需要向服务器请求新的页面,然后再渲染显示出来。
不过现在这种传统的显示已经不那么常见了。越来越多的网站使用 JavaScript 去动态加载页面内容。当你在站点内部点击按钮导航时,并不会重新获取整个页面,而是仅仅去请求内容发生改变的部分。这种技术明显减少了网络请求的数据量。应用这种技术的站点通常被称为单页应用(Single Page Application,即 SPA)。
这种实现看起来很不错,但问题在于,用户点击按钮时,页面并不会发生任何改变,URL 也不会。这意味着我们并不能通过监听页面的变化或者 URL 的变化来获知用户是不是点击了按钮。同样,我们也不能通过 URL 收藏某个页面(因为页面变化了但 URL 不会变),也就不能把 URL 分享出去。因此,我们需要对此进行适配,也就是用户在站点内部导航时,也需要使用某种机制去改变 URL。这一机制就是路由。路由允许我们在页面中直接改变 URL,并且不会因 URL 的改变与服务器通信。
路由是 Angular 的内置模块,我们可以直接在 Angular 中使用路由。
URL 通常包含一个域名和路由定义,也就是路径(path)。Angular 的路由模块要求我们给出一个路由表,类似于键值对,其中,键是路径,值是当匹配到这个路径时,需要显示哪个组件。Angular 会从 URL 中读取路径,然后与我们在 Angular 中设置的路由表进行匹配,一旦匹配成功,就会将路由表中定义的组件显示出来。这种设计允许我们在应用中进行导航,并且在不启动首页组件的情况下直接显示具体路径对应的组件。路由同时可以支持浏览器的前进后退以及收藏的功能。
接下来,我们创建一个项目,实际看一下 Angular 路由如何使用。
# 创建项目,启用路由模块 ng new routing --routing ... cd routing # 创建若干组件 ng g c home ng g c users ng g c user ng g c projects
等待所有项目文件创建完成之后,我们会发现有一个新的 app-routing.module.ts 文件:
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; const routes: Routes = []; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
这是 Angular 的路由模块。Angular 建议为每一个需要路由的模块创建单独的路由模块,然后通过imports
声明,将这个模块引入对应的模块:
@NgModule({ declarations: [ AppComponent, HomeComponent, UsersComponent, UserComponent, ProjectsComponent ], imports: [ BrowserModule, AppRoutingModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
因此,我们的路由表就应该写在AppRoutingModule
里面。下面,我们来定义路由。
路由的定义取决于业务逻辑。之前我们创建了几个组件,现在就要规划它们之间的路由。我们计划在AppComponent
中添加一个菜单,用于各个路由之间的导航。默认应该显示HomeComponent
;点击 Users 菜单,跳转到UsersComponent
;点击 Projects 菜单,跳转到ProjectsComponent
;UsersComponent
中还有子路由,可以跳转到UserComponent
。现在,我们将routes
修改为:
const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'users', component: UsersComponent }, { path: 'user/:id', component: UserComponent }, { path: 'projects', component: ProjectsComponent } ];
routes
类型是Routes
,但实际就是Route
数组:
export declare type Routes = Route[];
Route
就是路由表的每一项。最简单的,path
即路径,component
即匹配到这个路径时,需要显示哪个组件。所以,我们可以看到,
{ path: 'users', component: UsersComponent }
意味着,如果路径是users
,那么就显示UsersComponent
。
我们还可以有另外的配置。比如,
const routes: Routes = [ { path: '', pathMatch: 'full', redirectTo: '/home' }, { path: 'home', component: HomeComponent } ];
前面的component
表示直接加到某个组件。现在我们使用redirectTo
,意味着,如果匹配到这个路径,那么重定向到另外的一个路由。注意,这里我们需要添加一个额外的属性:pathMatch
。
pathMatch
是路径匹配策略,其可选值是prefix
或full
,默认值是prefix
。顾名思义,prefix
以路径开头,而full
为完全匹配。例如,
{ path: 'abc', pathMatch: 'prefix', component: TestComponent }
匹配
- /abc
- /abc/
- /abc/d
不匹配
- /abcd
{ path: 'abc', pathMatch: 'full', component: TestComponent }
匹配
- /abc
- /abc/
不匹配
- /abc/d
- /abcd
考虑下面的场景,由于pathMatch
的默认值是prefix
,那么,
{ path: '', redirectTo: '/home' }
因为所有路径都是以''
开头,所以这个路由跳转到/home
。然而,/home
同样是以''
开头,所以又会做跳转,这就会陷入无限循环。因此,当空字符串作为路径时,必须使用pathMatch: 'full'
,才能避免这个问题。
下面的路由:
{ path: 'user/:id', component: UserComponent }
在路径中使用:
定义了路由参数。我们可以在组件中读取这个参数。在下面的部分,我们会介绍如何读取路由参数。
现在,路由表已经定义完毕。我们修改AppComponent
的模板:
<ul> <li><a routerLink="/">Home</a></li> <li><a routerLink="/users">Users</a></li> <li><a routerLink="/projects">Projects</a></li> </ul> <router-outlet></router-outlet>
我们使用<ul>
模拟菜单。注意,<a>
标签使用了routerLink
而不是通常的href
作为链接地址。routerLink
是 Angular 路由模块定义的指令,只能使用 Angular 定义的路由。点击链接,可以看到浏览器地址栏发现改变。
<router-outlet>
同样是 Angular 路由模块提供的组件,作为路由切换的占位符;也就是说,路由变化时,我们定义的组件会在<router-outlet>
所在位置显示。
接下来,我们看UsersComponent
:
users = [ { id: 1, name: 'Tom' }, { id: 2, name: 'Jerry' }, ];
我们在UsersComponent
中定义了users
数组。实际应用中,一般我们会从数据库读出一个列表。然后在 HTML 模板使用ngFor
指令循环显示:
<ul> <li *ngFor="let user of users"> <a routerLink="/user/{{user.id}}">{{ user.name }}</a> </li> </ul>
其中,<a>
标签依然使用routerLink
定义路径。注意,我们使用字符串插值语法,将 id 插入到路径中。那么,在UserComponent
中,
id = -1; constructor( private readonly route: ActivatedRoute ) { this.route.paramMap .subscribe(params => { this.id = +(params.get('id') ?? '-1'); }); }
我们在构造函数中注入ActivatedRoute
。这个类表示当前激活的路由,其paramMap
属性即路由中定义的参数映射。还记得之前我们定义的路由吗?
{ path: 'user/:id', component: UserComponent }
因此,paramMap
中会有一个名为id
的键,其值就是routerLink
拼接而来的具体数据。Angular 会按照参数位置给每一个参数赋值。我们只需订阅这个paramMap
,即可读取每一个 id 的值。实际应用中,我们应该是拿到这个 id,然后从数据库获取该条记录。
现在,运行一下程序,看看实际效果吧。注意浏览器地址栏的变化,以及前进后退按钮的功能是否正常。
项目文件:https://files.devbean.net/code/routing.zip