Angular路由与导航配置

本章目标:深入理解Angular路由机制,掌握路由配置、导航守卫、路由参数传递、懒加载和嵌套路由等核心功能。

Angular路由核心概念

路由模块

配置路由规则

路由导航

页面间跳转

导航守卫

路由访问控制

什么是Angular路由?

Angular路由是一个强大的导航系统,它允许我们在单页面应用(SPA)中实现页面间的无缝切换,同时保持应用的响应性和性能。

基本功能
  • 定义URL路径与组件的映射关系
  • 实现单页面应用的无刷新导航
  • 管理浏览器的历史记录
  • 支持前进/后退按钮
高级特性
  • 嵌套路由和子路由
  • 路由参数和查询参数
  • 懒加载模块
  • 路由动画
安全控制
  • 导航守卫(路由保护)
  • 路由数据预加载
  • 路由事件监听
  • 错误处理

1. 创建路由模块

使用Angular CLI可以快速创建带有路由功能的应用,也可以手动配置路由模块。

# 创建新项目时启用路由
ng new my-app --routing

# 为现有模块添加路由
ng generate module app-routing --flat --module=app

# 创建带路由的功能模块
ng generate module admin --routing

基本路由配置

路由模块负责定义URL路径与组件之间的映射关系:

// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

// 导入组件
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
import { ContactComponent } from './contact/contact.component';
import { ProductsComponent } from './products/products.component';
import { ProductDetailComponent } from './products/product-detail/product-detail.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

// 定义路由配置
const routes: Routes = [
  // 默认路由重定向
  { path: '', redirectTo: '/home', pathMatch: 'full' },

  // 基本路由
  { path: 'home', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'contact', component: ContactComponent },

  // 带参数的路由
  { path: 'products', component: ProductsComponent },
  { path: 'products/:id', component: ProductDetailComponent },

  // 通配符路由(必须放在最后)
  { path: '**', component: PageNotFoundComponent }
];

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

路由出口

在根组件模板中添加<router-outlet></router-outlet>标签,用于显示路由内容:

<!-- app.component.html -->
<div class="app-container">
  <!-- 导航栏 -->
  <nav class="navbar navbar-expand-lg navbar-light bg-light">
    <div class="container-fluid">
      <a class="navbar-brand" [routerLink]="['/']">MyApp</a>
      <div class="navbar-nav">
        <a class="nav-link" [routerLink]="['/home']">首页</a>
        <a class="nav-link" [routerLink]="['/about']">关于</a>
        <a class="nav-link" [routerLink]="['/products']">产品</a>
        <a class="nav-link" [routerLink]="['/contact']">联系</a>
      </div>
    </div>
  </nav>

  <!-- 路由出口 -->
  <div class="container mt-4">
    <router-outlet></router-outlet>
  </div>

  <!-- 页脚 -->
  <footer class="mt-5 text-center">
    <p>© 2024 MyApp</p>
  </footer>
</div>

2. 路由导航

Angular提供两种方式进行路由导航:模板导航和编程导航。

路由导航流程图
用户操作

点击链接或按钮

路由匹配

Angular匹配路由配置

守卫检查

执行导航守卫逻辑

组件加载

加载对应组件到出口

模板导航

使用routerLink指令在模板中创建导航链接。

<!-- 基本导航 -->
<a routerLink="/home">首页</a>
<a [routerLink]="['/about']">关于</a>

<!-- 带CSS类绑定 -->
<a [routerLink]="['/contact']"
   routerLinkActive="active"
   [routerLinkActiveOptions]="{exact: true}">
  联系我们
</a>

<!-- 带参数的导航 -->
<a [routerLink]="['/products', productId]">
  产品详情
</a>

<!-- 带查询参数的导航 -->
<a [routerLink]="['/search']"
   [queryParams]="{q: 'angular', page: 1}">
  搜索
</a>

<!-- 按钮导航 -->
<button [routerLink]="['/dashboard']">
  仪表板
</button>

<!-- 导航链接样式 -->
<nav>
  <a *ngFor="let link of navLinks"
     [routerLink]="link.path"
     routerLinkActive="active-link">
    {{ link.label }}
  </a>
</nav>
编程导航

在组件类中使用Router服务进行导航。

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

@Component({
  selector: 'app-navigation-demo',
  template: `
    <button (click)="goToHome()">去首页</button>
    <button (click)="goToProduct(123)">查看产品123</button>
    <button (click)="goBack()">返回</button>
  `
})
export class NavigationDemoComponent {
  constructor(private router: Router) {}

  // 基本导航
  goToHome(): void {
    this.router.navigate(['/home']);
  }

  // 带参数的导航
  goToProduct(productId: number): void {
    this.router.navigate(['/products', productId]);
  }

  // 带查询参数的导航
  goToSearch(keyword: string): void {
    this.router.navigate(['/search'], {
      queryParams: { q: keyword },
      queryParamsHandling: 'merge' // 合并现有查询参数
    });
  }

  // 相对路径导航
  goToRelative(): void {
    this.router.navigate(['../sibling'], { relativeTo: this.route });
  }

  // 导航并保留查询参数
  goToAbout(): void {
    this.router.navigate(['/about'], {
      queryParamsHandling: 'preserve'
    });
  }

  // 导航到根路径
  goToRoot(): void {
    this.router.navigate(['/']);
  }

  // 浏览器后退
  goBack(): void {
    this.router.navigate(['../']); // 上一级
    // 或使用 location.back()
  }

  // 浏览器前进
  goForward(): void {
    // 使用 location.forward()
  }

  // 获取当前URL
  getCurrentUrl(): string {
    return this.router.url;
  }
}
路由导航实时演示

3. 路由参数

路由参数允许我们在URL中传递数据,Angular支持路径参数、查询参数和片段参数。

参数类型 语法 用途 示例
路径参数 /path/:param 标识资源的唯一标识 /products/123
查询参数 ?key=value 过滤、排序或配置选项 /search?q=angular&page=1
片段参数 #fragment 页面内跳转(锚点) /about#team

获取路由参数

在组件中通过ActivatedRoute服务获取路由参数:

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

@Component({
  selector: 'app-product-detail',
  template: `
    <h2>产品详情</h2>
    <p>产品ID: {{ productId }}</p>
    <p>搜索关键词: {{ searchQuery }}</p>
  `
})
export class ProductDetailComponent implements OnInit {
  productId!: string;
  searchQuery!: string;

  constructor(private route: ActivatedRoute) {}

  ngOnInit(): void {
    // 获取路径参数(方法1:快照)
    this.productId = this.route.snapshot.params['id'];

    // 获取查询参数(方法1:快照)
    this.searchQuery = this.route.snapshot.queryParams['q'];

    // 获取路径参数(方法2:观察者 - 推荐)
    this.route.params.subscribe((params: Params) => {
      this.productId = params['id'];
      // 当参数变化时,重新加载数据
      this.loadProductData(this.productId);
    });

    // 获取查询参数(方法2:观察者)
    this.route.queryParams.subscribe((params: Params) => {
      this.searchQuery = params['q'];
      // 当查询参数变化时,执行搜索
      if (this.searchQuery) {
        this.performSearch(this.searchQuery);
      }
    });

    // 获取片段参数
    this.route.fragment.subscribe(fragment => {
      console.log('页面片段:', fragment);
      if (fragment) {
        this.scrollToElement(fragment);
      }
    });

    // 获取所有参数
    const paramMap = this.route.snapshot.paramMap;
    const queryParamMap = this.route.snapshot.queryParamMap;

    this.productId = paramMap.get('id') || '';
    this.searchQuery = queryParamMap.get('q') || '';
  }

  private loadProductData(id: string): void {
    // 加载产品数据
    console.log('加载产品数据:', id);
  }

  private performSearch(query: string): void {
    // 执行搜索
    console.log('执行搜索:', query);
  }

  private scrollToElement(elementId: string): void {
    // 滚动到指定元素
    const element = document.getElementById(elementId);
    if (element) {
      element.scrollIntoView({ behavior: 'smooth' });
    }
  }
}

路由参数传递示例

路径参数配置
// 路由配置
const routes: Routes = [
  { path: 'products/:id', component: ProductDetailComponent },
  { path: 'users/:userId/posts/:postId', component: PostComponent }
];

// 导航到带参数的路径
this.router.navigate(['/products', product.id]);

// 获取参数
this.route.params.subscribe(params => {
  const productId = params['id'];
});
查询参数配置
// 导航带查询参数
this.router.navigate(['/search'], {
  queryParams: {
    q: 'angular',
    page: 1,
    sort: 'name'
  }
});

// 合并查询参数
this.router.navigate(['/search'], {
  queryParams: { page: 2 },
  queryParamsHandling: 'merge'
});

// 保留查询参数
this.router.navigate(['/results'], {
  queryParamsHandling: 'preserve'
});

// 获取查询参数
this.route.queryParams.subscribe(params => {
  const query = params['q'];
  const page = params['page'];
});

4. 导航守卫

导航守卫允许我们在路由导航发生之前、期间和之后执行自定义逻辑,用于权限控制、数据预加载等。

导航守卫类型
CanActivate

控制是否可以访问路由(进入路由前)

用途:登录验证、权限检查
CanActivateChild

控制是否可以访问子路由

用途:子路由权限控制
CanDeactivate

控制是否可以离开当前路由

用途:表单未保存提示
Resolve

在路由激活前预加载数据

用途:数据预加载
CanLoad

控制是否可以加载模块(懒加载)

用途:模块加载权限控制

实现导航守卫

// auth.guard.ts - 认证守卫
import { Injectable } from '@angular/core';
import {
  CanActivate,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  Router,
  UrlTree
} from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';

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

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {

    // 检查用户是否已登录
    if (this.authService.isLoggedIn) {
      return true;
    }

    // 如果未登录,重定向到登录页
    // 同时保存当前URL,以便登录后返回
    this.authService.redirectUrl = state.url;
    return this.router.parseUrl('/login');
  }
}

// admin.guard.ts - 管理员权限守卫
@Injectable({ providedIn: 'root' })
export class AdminGuard implements CanActivate {
  constructor(private authService: AuthService) {}

  canActivate(): boolean {
    // 检查用户角色是否为管理员
    const user = this.authService.currentUser;
    return user?.role === 'admin';
  }
}

// unsaved-changes.guard.ts - 未保存更改守卫
@Injectable({ providedIn: 'root' })
export class UnsavedChangesGuard implements CanDeactivate<CanComponentDeactivate> {
  canDeactivate(
    component: CanComponentDeactivate,
    currentRoute: ActivatedRouteSnapshot,
    currentState: RouterStateSnapshot,
    nextState?: RouterStateSnapshot
  ): Observable<boolean> | Promise<boolean> | boolean {

    // 调用组件的canDeactivate方法
    return component.canDeactivate ?
      component.canDeactivate() : true;
  }
}

// 在组件中实现CanComponentDeactivate接口
export interface CanComponentDeactivate {
  canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}

@Component({...})
export class EditProductComponent implements CanComponentDeactivate {
  hasUnsavedChanges = false;

  canDeactivate(): boolean | Observable<boolean> {
    if (!this.hasUnsavedChanges) {
      return true;
    }

    // 如果有未保存的更改,显示确认对话框
    return confirm('您有未保存的更改,确定要离开吗?');
  }
}

在路由配置中使用守卫

const routes: Routes = [
  // 公开路由(不需要守卫)
  { path: 'home', component: HomeComponent },
  { path: 'login', component: LoginComponent },

  // 需要认证的路由
  {
    path: 'profile',
    component: ProfileComponent,
    canActivate: [AuthGuard]  // 使用认证守卫
  },

  // 需要管理员权限的路由
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard, AdminGuard]  // 多个守卫
  },

  // 带数据预加载的路由
  {
    path: 'product/:id',
    component: ProductDetailComponent,
    resolve: {
      product: ProductResolver  // 使用数据解析器
    }
  },

  // 子路由守卫
  {
    path: 'dashboard',
    component: DashboardComponent,
    canActivateChild: [AuthGuard],  // 子路由守卫
    children: [
      { path: 'stats', component: StatsComponent },
      { path: 'settings', component: SettingsComponent }
    ]
  },

  // 懒加载模块守卫
  {
    path: 'reports',
    canLoad: [AuthGuard],  // 模块加载守卫
    loadChildren: () => import('./reports/reports.module')
      .then(m => m.ReportsModule)
  }
];

5. 懒加载模块

懒加载可以显著提高应用的初始加载速度,通过按需加载功能模块来减少初始包大小。

懒加载优势
减少初始加载时间

只加载当前路由所需的代码

代码分割

将应用拆分为多个独立的包

优化内存使用

按需加载和卸载模块

更好的用户体验

快速显示初始界面

配置懒加载模块

// app-routing.module.ts
const routes: Routes = [
  // 主路由
  { path: '', redirectTo: '/home', pathMatch: 'full' },
  { path: 'home', component: HomeComponent },

  // 懒加载功能模块
  {
    path: 'products',
    loadChildren: () => import('./products/products.module')
      .then(m => m.ProductsModule)
  },

  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.module')
      .then(m => m.AdminModule),
    canLoad: [AuthGuard, AdminGuard]  // 模块加载守卫
  },

  {
    path: 'user',
    loadChildren: () => import('./user/user.module')
      .then(m => m.UserModule)
  },

  // 通配符路由(必须在最后)
  { path: '**', component: PageNotFoundComponent }
];

// products.module.ts
@NgModule({
  declarations: [
    ProductsComponent,
    ProductDetailComponent,
    ProductListComponent
  ],
  imports: [
    CommonModule,
    ProductsRoutingModule  // 产品模块有自己的路由
  ]
})
export class ProductsModule { }

// products-routing.module.ts
const routes: Routes = [
  { path: '', component: ProductsComponent },
  { path: ':id', component: ProductDetailComponent }
];

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

预加载策略

Angular提供了多种预加载策略来平衡性能和用户体验:

// 使用预加载策略
@NgModule({
  imports: [RouterModule.forRoot(routes, {
    // 1. 不预加载(默认)
    preloadingStrategy: NoPreloading,

    // 2. 预加载所有模块
    preloadingStrategy: PreloadAllModules,

    // 3. 自定义预加载策略
    preloadingStrategy: CustomPreloadingStrategy,

    // 4. 其他配置选项
    enableTracing: true,  // 启用路由调试
    useHash: false,       // 是否使用hash模式
    scrollPositionRestoration: 'enabled'  // 滚动位置恢复
  })],
  exports: [RouterModule]
})
export class AppRoutingModule { }

// 自定义预加载策略
@Injectable({ providedIn: 'root' })
export class CustomPreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
    // 检查路由是否有data.preload标记
    if (route.data && route.data['preload'] === true) {
      return load();
    }

    // 否则不预加载
    return of(null);
  }
}

// 在路由配置中标记需要预加载的模块
const routes: Routes = [
  {
    path: 'dashboard',
    loadChildren: () => import('./dashboard/dashboard.module')
      .then(m => m.DashboardModule),
    data: { preload: true }  // 标记为需要预加载
  },
  {
    path: 'settings',
    loadChildren: () => import('./settings/settings.module')
      .then(m => m.SettingsModule)
    // 不标记,使用懒加载
  }
];

6. 嵌套路由和辅助路由

嵌套路由允许在组件内部定义子路由,辅助路由允许在同一页面显示多个路由出口。

嵌套路由配置

// 路由配置
const routes: Routes = [
  {
    path: 'dashboard',
    component: DashboardComponent,
    children: [  // 定义子路由
      {
        path: '',
        redirectTo: 'overview',
        pathMatch: 'full'
      },
      {
        path: 'overview',
        component: OverviewComponent
      },
      {
        path: 'analytics',
        component: AnalyticsComponent
      },
      {
        path: 'reports',
        component: ReportsComponent
      },
      {
        path: 'settings',
        component: SettingsComponent
      }
    ]
  }
];

// dashboard.component.html
<div class="dashboard">
  <!-- 侧边栏导航 -->
  <nav class="sidebar">
    <a [routerLink]="['overview']" routerLinkActive="active">
      概览
    </a>
    <a [routerLink]="['analytics']" routerLinkActive="active">
      分析
    </a>
    <a [routerLink]="['reports']" routerLinkActive="active">
      报告
    </a>
    <a [routerLink]="['settings']" routerLinkActive="active">
      设置
    </a>
  </nav>

  <!-- 子路由出口 -->
  <div class="content">
    <router-outlet></router-outlet>
  </div>
</div>

辅助路由(命名路由出口)

// 路由配置
const routes: Routes = [
  // 主路由
  { path: 'home', component: HomeComponent },

  // 辅助路由
  {
    path: 'sidebar',
    component: SidebarComponent,
    outlet: 'sidebar'  // 命名路由出口
  },
  {
    path: 'chat',
    component: ChatComponent,
    outlet: 'chat'
  }
];

// app.component.html
<div class="app-container">
  <!-- 主路由出口 -->
  <div class="main-content">
    <router-outlet></router-outlet>
  </div>

  <!-- 辅助路由出口 -->
  <div class="sidebar">
    <router-outlet name="sidebar"></router-outlet>
  </div>

  <div class="chat-widget">
    <router-outlet name="chat"></router-outlet>
  </div>
</div>

// 导航到辅助路由
// 同时激活主路由和辅助路由
this.router.navigate([
  { outlets: { primary: ['home'], sidebar: ['sidebar'] } }
]);

// 只导航辅助路由
this.router.navigate([{ outlets: { sidebar: ['chat'] } }]);

// 关闭辅助路由
this.router.navigate([{ outlets: { sidebar: null } }]);

路由最佳实践

推荐做法
  • 使用懒加载模块来提高初始加载性能
  • 为路由配置合理的预加载策略
  • 使用导航守卫保护敏感路由
  • 合理组织路由配置,保持代码清晰
  • 使用路由参数而不是组件属性传递数据
  • 为路由配置添加合适的数据(title、breadcrumb等)
  • 使用routerLinkActive高亮当前激活路由
  • 处理路由错误和404页面
应避免的做法
  • 避免在组件构造函数中依赖路由参数(使用ngOnInit)
  • 不要忘记取消订阅路由参数观察者
  • 避免创建过于复杂的嵌套路由结构
  • 不要在守卫中执行耗时操作(使用Resolve预加载)
  • 避免硬编码路由路径(使用路由常量)
  • 不要忘记处理路由守卫的返回类型(Observable/Promise)
  • 避免在路由之间传递大量数据(使用服务)
  • 不要忽略路由安全性(始终验证权限)

路由配置速查表

配置选项 描述 示例
基本路由 路径到组件的映射 { path: 'home', component: HomeComponent }
重定向路由 重定向到其他路径 { path: '', redirectTo: '/home', pathMatch: 'full' }
路径参数 URL中的参数 { path: 'products/:id', component: ProductDetailComponent }
子路由 嵌套路由配置 children: [{ path: 'details', component: DetailsComponent }]
懒加载 按需加载模块 loadChildren: () => import('./module').then(m => m.Module)
路由守卫 路由访问控制 canActivate: [AuthGuard]
路由数据 附加到路由的数据 data: { title: '首页', requiresAuth: true }
通配符路由 匹配所有路径 { path: '**', component: NotFoundComponent }

本章总结

通过本章学习,你应该掌握了:

  • 路由模块创建:配置路由规则和映射关系
  • 路由导航:使用routerLinkRouter服务进行导航
  • 路由参数:传递和获取路径参数、查询参数
  • 导航守卫:实现路由保护和权限控制
  • 懒加载:按需加载模块以提高性能
  • 嵌套路由:配置子路由和命名路由出口
  • 最佳实践:编写高效、安全的路由配置