首页 Angular Angular 学习之路 22 – 路由简介

Angular 学习之路 22 – 路由简介

0 1.9K

最初,每个网页的都是独立的 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 菜单,跳转到ProjectsComponentUsersComponent中还有子路由,可以跳转到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是路径匹配策略,其可选值是prefixfull,默认值是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

发表评论

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2