最初,每个网页的都是独立的 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