MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Angular路由机制详解:实现单页面应用

2021-03-073.6k 阅读

1. 单页面应用(SPA)概述

在深入探讨 Angular 路由机制之前,我们先来了解下单页面应用(Single - Page Application,简称 SPA)。传统的多页面应用(MPA)在用户交互过程中,每一次页面跳转都需要重新加载整个 HTML 页面,这不仅会带来较大的网络开销,还会产生明显的页面闪烁,影响用户体验。

而 SPA 则是在一个 HTML 页面中动态地加载和切换内容。当用户与应用进行交互时,页面不会进行完整的刷新,而是通过 JavaScript 来更新 DOM,从而实现局部内容的变化。这种方式极大地提升了应用的响应速度和用户体验,使得应用的交互更加流畅,类似于原生应用的体验。

例如,一个典型的 SPA 应用可能是一个社交媒体平台,用户登录后,可以在不刷新整个页面的情况下浏览动态、查看个人资料、发送消息等。当用户点击不同的功能按钮时,相应的内容会在同一页面中动态加载,给用户一种无缝的操作体验。

2. Angular 路由基础

2.1 什么是 Angular 路由

Angular 路由是 Angular 框架中用于实现单页面应用导航的核心模块。它允许我们根据用户在浏览器地址栏中输入的 URL 来加载不同的组件,从而实现页面内容的切换。通过路由,我们可以将一个复杂的单页面应用划分为多个功能模块,每个模块对应一个或多个组件,用户通过 URL 的变化来访问不同的功能模块。

2.2 路由模块的引入

在 Angular 项目中,要使用路由功能,首先需要引入路由模块。在 Angular CLI 创建的项目中,默认已经为我们生成了一个路由模块。如果没有,可以通过以下命令生成:

ng generate module app - routing --flat --module=app

--flat 选项表示将路由模块文件放在项目的根目录下,--module=app 表示将该路由模块注册到 app.module.ts 中。

生成的路由模块文件(通常命名为 app - routing.module.ts)内容大致如下:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

这里,Routes 是一个数组,用于定义路由规则。RouterModule.forRoot(routes) 方法用于将这些路由规则注册到应用的根路由中。

2.3 定义基本路由

接下来,我们定义一些基本的路由规则。假设我们有两个组件:HomeComponentAboutComponent。首先,确保这两个组件已经创建:

ng generate component home
ng generate component about

然后在 app - routing.module.ts 中定义路由规则:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';

const routes: Routes = [
  { path: 'home', component: HomeComponent },
  { path: 'about', component: AboutComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

在上述代码中,path 表示 URL 的路径,component 表示当访问该路径时要加载的组件。当用户访问 http://localhost:4200/home 时,HomeComponent 将会被加载并显示在页面中;当访问 http://localhost:4200/about 时,AboutComponent 将会被加载。

3. 路由导航

3.1 使用 <router - outlet>

在 Angular 中,<router - outlet> 是一个指令,它作为路由组件的占位符。在应用的主模板(通常是 app.component.html)中,我们需要添加 <router - outlet> 来告诉 Angular 在哪里显示路由组件。

<app - header></app - header>
<router - outlet></router - outlet>
<app - footer></app - footer>

这样,当用户访问不同的路由时,相应的组件就会显示在 <router - outlet> 的位置。

3.2 导航链接

为了让用户能够方便地在不同路由之间切换,我们需要创建导航链接。在 HTML 模板中,可以使用 <a> 标签结合 routerLink 指令来创建路由链接。例如,在 app.component.html 中添加导航链接:

<ul>
  <li><a routerLink="/home">Home</a></li>
  <li><a routerLink="/about">About</a></li>
</ul>
<router - outlet></router - outlet>

当用户点击这些链接时,浏览器的 URL 会相应地改变,并且 <router - outlet> 中会显示对应的组件。

3.3 编程式导航

除了通过导航链接进行路由切换,我们还可以在组件的 TypeScript 代码中进行编程式导航。在组件中注入 Router 服务,就可以使用它的方法来实现导航。例如,在 HomeComponent 中添加一个按钮,点击按钮导航到 AboutComponent

import { Component } from '@angular/core';
import { Router } from '@angular/router';

@Component({
  selector: 'app - home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})
export class HomeComponent {
  constructor(private router: Router) { }

  goToAbout() {
    this.router.navigate(['/about']);
  }
}

home.component.html 中添加按钮:

<button (click)="goToAbout()">Go to About</button>

当用户点击按钮时,goToAbout 方法会被调用,通过 router.navigate 方法导航到 /about 路由。

4. 路由参数

4.1 静态路由参数

有时候,我们需要在路由中传递一些参数。例如,我们有一个 ProductComponent,用于显示特定产品的详细信息,产品的 ID 可以通过路由参数传递。首先,在 app - routing.module.ts 中定义带参数的路由:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ProductComponent } from './product/product.component';

const routes: Routes = [
  { path: 'product/:id', component: ProductComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

这里,:id 是一个参数占位符。在 ProductComponent 中,可以通过注入 ActivatedRoute 服务来获取这个参数:

import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app - product',
  templateUrl: './product.component.html',
  styleUrls: ['./product.component.css']
})
export class ProductComponent {
  productId: string;

  constructor(private route: ActivatedRoute) {
    this.route.params.subscribe(params => {
      this.productId = params['id'];
      // 这里可以根据 productId 从服务器获取产品详细信息
    });
  }
}

在导航到 ProductComponent 时,可以传递参数:

<a [routerLink]="['/product', 123]">View Product 123</a>

这样,当用户点击链接时,ProductComponent 会获取到 id123 的参数。

4.2 查询参数

除了路径参数,我们还可以使用查询参数。查询参数通常用于传递一些可选的、临时性的信息。例如,我们希望在搜索结果页面中传递搜索关键词作为查询参数。在导航时,可以这样传递查询参数:

this.router.navigate(['/search'], { queryParams: { keyword: 'angular' } });

SearchComponent 中,可以通过 ActivatedRoute 获取查询参数:

import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app - search',
  templateUrl: './search.component.html',
  styleUrls: ['./search.component.css']
})
export class SearchComponent {
  keyword: string;

  constructor(private route: ActivatedRoute) {
    this.route.queryParams.subscribe(params => {
      this.keyword = params['keyword'];
      // 这里可以根据 keyword 进行搜索操作
    });
  }
}

5. 路由守卫

5.1 什么是路由守卫

路由守卫是 Angular 提供的一种机制,用于在导航发生之前或之后执行一些逻辑判断。例如,我们可能需要在用户访问某个路由之前检查用户是否已经登录,如果未登录,则导航到登录页面。路由守卫可以分为多种类型,包括 CanActivateCanDeactivateResolve 等。

5.2 CanActivate 守卫

CanActivate 守卫用于决定是否可以激活某个路由。假设我们有一个 AdminComponent,只有管理员用户才能访问。我们可以创建一个 AdminGuard 来实现这个逻辑。首先,创建一个守卫类:

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { AuthService } from './auth.service';

@Injectable({
  providedIn: 'root'
})
export class AdminGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) { }

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): boolean {
    if (this.authService.isAdmin()) {
      return true;
    } else {
      this.router.navigate(['/login']);
      return false;
    }
  }
}

在上述代码中,AuthService 是一个自定义的服务,用于检查用户是否是管理员。如果用户是管理员,canActivate 方法返回 true,允许导航到目标路由;否则,导航到登录页面并返回 false

然后,在 app - routing.module.ts 中使用这个守卫:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AdminComponent } from './admin/admin.component';
import { AdminGuard } from './admin.guard';

const routes: Routes = [
  { path: 'admin', component: AdminComponent, canActivate: [AdminGuard] }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

这样,当用户尝试访问 /admin 路由时,AdminGuard 会先进行检查。

5.3 CanDeactivate 守卫

CanDeactivate 守卫用于决定是否可以离开某个路由。例如,当用户在填写表单时,可能希望在离开页面之前提示用户是否保存数据。假设我们有一个 FormComponent,创建一个 FormGuard 来实现这个功能:

import { Injectable } from '@angular/core';
import { CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { FormComponent } from './form.component';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class FormGuard implements CanDeactivate<FormComponent> {
  canDeactivate(
    component: FormComponent,
    currentRoute: ActivatedRouteSnapshot,
    currentState: RouterStateSnapshot,
    nextState?: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    return component.canLeave();
  }
}

FormComponent 中定义 canLeave 方法:

import { Component } from '@angular/core';

@Component({
  selector: 'app - form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.css']
})
export class FormComponent {
  canLeave(): boolean {
    // 这里可以检查表单是否有未保存的数据
    // 如果有,弹出提示框询问用户是否保存
    return window.confirm('Do you want to leave without saving?');
  }
}

app - routing.module.ts 中使用 FormGuard

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { FormComponent } from './form.component';
import { FormGuard } from './form.guard';

const routes: Routes = [
  { path: 'form', component: FormComponent, canDeactivate: [FormGuard] }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

5.4 Resolve 守卫

Resolve 守卫用于在路由激活之前获取数据。例如,在显示产品详细信息的 ProductComponent 之前,我们希望先从服务器获取产品数据。创建一个 ProductResolver

import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { ProductService } from './product.service';
import { Observable } from 'rxjs';
import { Product } from './product';

@Injectable({
  providedIn: 'root'
})
export class ProductResolver implements Resolve<Product> {
  constructor(private productService: ProductService) { }

  resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<Product> | Promise<Product> | Product {
    const productId = route.params['id'];
    return this.productService.getProduct(productId);
  }
}

ProductService 中定义 getProduct 方法来从服务器获取产品数据。然后在 app - routing.module.ts 中使用 ProductResolver

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ProductComponent } from './product.component';
import { ProductResolver } from './product.resolver';

const routes: Routes = [
  { path: 'product/:id', component: ProductComponent, resolve: { product: ProductResolver } }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

ProductComponent 中,可以通过 ActivatedRoute 获取解析后的数据:

import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app - product',
  templateUrl: './product.component.html',
  styleUrls: ['./product.component.css']
})
export class ProductComponent {
  product;

  constructor(private route: ActivatedRoute) {
    this.route.data.subscribe(data => {
      this.product = data['product'];
    });
  }
}

6. 嵌套路由

6.1 什么是嵌套路由

在实际应用中,我们经常会遇到一些组件具有子视图的情况。例如,一个电商应用可能有一个 ProductComponent,在这个组件中又有 ProductDetailsComponentProductReviewsComponent 作为子视图。这时候就需要用到嵌套路由。

6.2 配置嵌套路由

假设我们有一个 DashboardComponent,它有两个子组件 DashboardHomeComponentDashboardSettingsComponent。首先,创建这些组件:

ng generate component dashboard
ng generate component dashboard - home
ng generate component dashboard - settings

然后在 app - routing.module.ts 中定义父路由:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { DashboardComponent } from './dashboard/dashboard.component';

const routes: Routes = [
  { path: 'dashboard', component: DashboardComponent, children: [
    { path: '', component: DashboardHomeComponent },
    { path:'settings', component: DashboardSettingsComponent }
  ] }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

DashboardComponent 的模板(dashboard.component.html)中添加 <router - outlet> 来显示子路由组件:

<h1>Dashboard</h1>
<ul>
  <li><a routerLink="">Home</a></li>
  <li><a routerLink="settings">Settings</a></li>
</ul>
<router - outlet></router - outlet>

当用户访问 http://localhost:4200/dashboard 时,DashboardHomeComponent 会显示在 <router - outlet> 中;当访问 http://localhost:4200/dashboard/settings 时,DashboardSettingsComponent 会显示。

7. 路由的懒加载

7.1 为什么要使用懒加载

随着应用的不断发展,代码体积可能会变得越来越大。如果在应用启动时一次性加载所有的模块和组件,会导致应用的启动时间变长。路由的懒加载可以解决这个问题。懒加载允许我们在需要的时候才加载特定的模块,而不是在应用启动时就加载所有模块,从而提高应用的初始加载速度。

7.2 配置懒加载

假设我们有一个 UserModule,包含 UserListComponentUserDetailsComponent。首先,创建 UserModule

ng generate module user --routing

user - routing.module.ts 中定义路由:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { UserListComponent } from './user - list/user - list.component';
import { UserDetailsComponent } from './user - details/user - details.component';

const routes: Routes = [
  { path: 'list', component: UserListComponent },
  { path: 'details/:id', component: UserDetailsComponent }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class UserRoutingModule { }

app - routing.module.ts 中配置懒加载:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  {
    path: 'users',
    loadChildren: () => import('./user/user.module').then(m => m.UserModule)
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

这里,loadChildren 是一个函数,它使用动态导入(import())来延迟加载 UserModule。当用户访问 http://localhost:4200/users/list 时,UserModule 才会被加载。

8. 处理路由错误

8.1 捕获路由错误

在应用运行过程中,可能会出现路由错误,例如用户手动输入了一个不存在的 URL。我们可以通过 Router 服务的 events 属性来捕获路由错误。在 app.component.ts 中添加如下代码:

import { Component, OnInit } from '@angular/core';
import { Router, Event, NavigationError } from '@angular/router';

@Component({
  selector: 'app - component',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  constructor(private router: Router) { }

  ngOnInit() {
    this.router.events.subscribe((event: Event) => {
      if (event instanceof NavigationError) {
        console.error('Navigation error:', event.error);
        // 这里可以进行错误处理,例如导航到错误页面
      }
    });
  }
}

8.2 配置 404 页面

为了给用户更好的体验,当出现路由错误时,我们可以导航到一个 404 页面。在 app - routing.module.ts 中添加一个通配符路由:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { PageNotFoundComponent } from './page - not - found/page - not - found.component';

const routes: Routes = [
  // 其他路由定义...
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

当用户访问一个不存在的 URL 时,PageNotFoundComponent 将会被加载并显示。

通过以上对 Angular 路由机制的详细讲解,我们深入了解了如何使用路由来构建高效的单页面应用。从基础的路由定义、导航,到路由参数、守卫、嵌套路由、懒加载以及错误处理等方面,掌握这些知识将有助于我们开发出更加健壮和用户体验良好的 Angular 应用。