Skip to content

09 - 路由高级功能

📖 学习目标

通过本章节学习,您将掌握:

  • 高级路由守卫
  • 路由解析器
  • 懒加载策略
  • 预加载策略
  • 路由动画
  • 面包屑导航
  • 路由元数据

🎯 核心概念

1. 高级路由特性

  • 路由守卫:控制路由访问权限
  • 路由解析器:预加载数据
  • 懒加载:按需加载模块
  • 预加载:提前加载模块
  • 路由动画:页面切换动画
  • 元数据:路由配置信息

2. 路由生命周期

路由匹配 → 守卫检查 → 解析器执行 → 组件加载 → 动画执行

🛡️ 高级路由守卫

1. CanActivate守卫

typescript
// guards/auth.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from '../services/auth.service';

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

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> | Promise<boolean> | boolean {
    return this.authService.isAuthenticated().then(isAuth => {
      if (isAuth) {
        return true;
      } else {
        this.router.navigate(['/login'], {
          queryParams: { returnUrl: state.url }
        });
        return false;
      }
    });
  }
}

2. CanActivateChild守卫

typescript
// guards/admin.guard.ts
import { Injectable } from '@angular/core';
import { CanActivateChild, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AuthService } from '../services/auth.service';

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

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

3. CanDeactivate守卫

typescript
// guards/unsaved-changes.guard.ts
import { Injectable } from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { Observable } from 'rxjs';

export interface CanComponentDeactivate {
  canDeactivate(): Observable<boolean> | Promise<boolean> | boolean;
}

@Injectable({
  providedIn: 'root'
})
export class UnsavedChangesGuard implements CanDeactivate<CanComponentDeactivate> {
  canDeactivate(
    component: CanComponentDeactivate
  ): Observable<boolean> | Promise<boolean> | boolean {
    return component.canDeactivate ? component.canDeactivate() : true;
  }
}

// 在组件中实现
@Component({...})
export class UserEditComponent implements CanComponentDeactivate {
  hasUnsavedChanges = false;

  canDeactivate(): boolean {
    if (this.hasUnsavedChanges) {
      return confirm('您有未保存的更改,确定要离开吗?');
    }
    return true;
  }
}

4. CanLoad守卫

typescript
// guards/module-load.guard.ts
import { Injectable } from '@angular/core';
import { CanLoad, Route, Router } from '@angular/router';
import { AuthService } from '../services/auth.service';

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

  canLoad(route: Route): boolean {
    if (this.authService.isAuthenticated()) {
      return true;
    } else {
      this.router.navigate(['/login']);
      return false;
    }
  }
}

🔍 路由解析器

1. 基本解析器

typescript
// resolvers/user.resolver.ts
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { UserService } from '../services/user.service';
import { User } from '../models/user';

@Injectable({
  providedIn: 'root'
})
export class UserResolver implements Resolve<User> {
  constructor(private userService: UserService) {}

  resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<User> | Promise<User> | User {
    const userId = route.paramMap.get('id');
    if (userId) {
      return this.userService.getUser(+userId);
    } else {
      throw new Error('User ID is required');
    }
  }
}

2. 复杂解析器

typescript
// resolvers/user-detail.resolver.ts
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable, forkJoin, of } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { UserService } from '../services/user.service';
import { PostService } from '../services/post.service';

export interface UserDetailData {
  user: any;
  posts: any[];
  comments: any[];
}

@Injectable({
  providedIn: 'root'
})
export class UserDetailResolver implements Resolve<UserDetailData> {
  constructor(
    private userService: UserService,
    private postService: PostService
  ) {}

  resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<UserDetailData> {
    const userId = route.paramMap.get('id');
    
    if (!userId) {
      throw new Error('User ID is required');
    }

    return forkJoin({
      user: this.userService.getUser(+userId),
      posts: this.postService.getUserPosts(+userId),
      comments: this.postService.getUserComments(+userId)
    }).pipe(
      catchError(error => {
        console.error('Error loading user detail:', error);
        return of({
          user: null,
          posts: [],
          comments: []
        });
      })
    );
  }
}

3. 使用解析器

typescript
// 路由配置
const routes: Routes = [
  {
    path: 'user/:id',
    component: UserDetailComponent,
    resolve: { 
      user: UserResolver,
      userDetail: UserDetailResolver 
    }
  }
];

// 在组件中获取解析的数据
@Component({...})
export class UserDetailComponent implements OnInit {
  user: any;
  userDetail: any;

  constructor(private route: ActivatedRoute) {}

  ngOnInit() {
    // 获取解析的数据
    this.user = this.route.snapshot.data['user'];
    this.userDetail = this.route.snapshot.data['userDetail'];

    // 或者监听数据变化
    this.route.data.subscribe(data => {
      this.user = data['user'];
      this.userDetail = data['userDetail'];
    });
  }
}

🚀 懒加载策略

1. 基本懒加载

typescript
// 路由配置
const routes: Routes = [
  {
    path: 'dashboard',
    loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule)
  },
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
  },
  {
    path: 'profile',
    loadChildren: () => import('./profile/profile.module').then(m => m.ProfileModule)
  }
];

2. 自定义预加载策略

typescript
// strategies/custom-preloading.strategy.ts
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class CustomPreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
    // 只预加载标记为preload的路由
    if (route.data && route.data['preload']) {
      console.log('Preloading: ' + route.path);
      return load();
    }
    return of(null);
  }
}

// 使用自定义策略
@NgModule({
  imports: [RouterModule.forRoot(routes, {
    preloadingStrategy: CustomPreloadingStrategy
  })],
  exports: [RouterModule]
})
export class AppRoutingModule {}

3. 条件预加载策略

typescript
// strategies/conditional-preloading.strategy.ts
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';
import { AuthService } from '../services/auth.service';

@Injectable({
  providedIn: 'root'
})
export class ConditionalPreloadingStrategy implements PreloadingStrategy {
  constructor(private authService: AuthService) {}

  preload(route: Route, load: () => Observable<any>): Observable<any> {
    // 根据用户权限决定是否预加载
    if (route.data && route.data['preload']) {
      const requiredRole = route.data['role'];
      
      if (!requiredRole || this.authService.hasRole(requiredRole)) {
        console.log('Preloading: ' + route.path);
        return load();
      }
    }
    return of(null);
  }
}

🎨 路由动画

1. 基本路由动画

typescript
// animations/route.animations.ts
import { trigger, transition, style, query, animate, group } from '@angular/animations';

export const routeAnimations = trigger('routeAnimations', [
  transition('* <=> *', [
    query(':enter, :leave', [
      style({
        position: 'absolute',
        left: 0,
        width: '100%',
        opacity: 0,
        transform: 'translateY(20px)'
      })
    ], { optional: true }),
    
    query(':enter', [
      animate('300ms ease-in', style({
        opacity: 1,
        transform: 'translateY(0)'
      }))
    ], { optional: true })
  ])
]);

2. 复杂路由动画

typescript
// animations/slide.animations.ts
import { trigger, transition, style, query, animate, group } from '@angular/animations';

export const slideInAnimation = trigger('slideInAnimation', [
  transition('* <=> *', [
    query(':enter, :leave', [
      style({
        position: 'absolute',
        top: 0,
        left: 0,
        width: '100%',
        height: '100%'
      })
    ], { optional: true }),
    
    group([
      query(':enter', [
        style({ transform: 'translateX(100%)' }),
        animate('0.5s ease-in-out', style({ transform: 'translateX(0%)' }))
      ], { optional: true }),
      
      query(':leave', [
        style({ transform: 'translateX(0%)' }),
        animate('0.5s ease-in-out', style({ transform: 'translateX(-100%)' }))
      ], { optional: true })
    ])
  ])
]);

3. 使用路由动画

typescript
// app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { slideInAnimation } from './animations/slide.animations';

@Component({
  selector: 'app-root',
  template: `
    <div class="app-container">
      <nav class="navbar">
        <a routerLink="/home" routerLinkActive="active">首页</a>
        <a routerLink="/about" routerLinkActive="active">关于</a>
        <a routerLink="/contact" routerLinkActive="active">联系</a>
      </nav>
      
      <main class="main-content" [@slideInAnimation]="getRouteAnimationData()">
        <router-outlet></router-outlet>
      </main>
    </div>
  `,
  animations: [slideInAnimation]
})
export class AppComponent {
  getRouteAnimationData() {
    return this.router.routerState.root.firstChild?.snapshot.data?.['animation'];
  }
}

🧭 面包屑导航

1. 面包屑服务

typescript
// services/breadcrumb.service.ts
import { Injectable } from '@angular/core';
import { Router, NavigationEnd, ActivatedRoute } from '@angular/router';
import { BehaviorSubject, Observable } from 'rxjs';
import { filter, distinctUntilChanged } from 'rxjs/operators';

export interface BreadcrumbItem {
  label: string;
  url: string;
  icon?: string;
}

@Injectable({
  providedIn: 'root'
})
export class BreadcrumbService {
  private breadcrumbsSubject = new BehaviorSubject<BreadcrumbItem[]>([]);
  public breadcrumbs$ = this.breadcrumbsSubject.asObservable();

  constructor(
    private router: Router,
    private activatedRoute: ActivatedRoute
  ) {
    this.router.events
      .pipe(
        filter(event => event instanceof NavigationEnd),
        distinctUntilChanged()
      )
      .subscribe(() => {
        const breadcrumbs = this.buildBreadcrumbs(this.activatedRoute.root);
        this.breadcrumbsSubject.next(breadcrumbs);
      });
  }

  private buildBreadcrumbs(route: ActivatedRoute, url: string = '', breadcrumbs: BreadcrumbItem[] = []): BreadcrumbItem[] {
    const children: ActivatedRoute[] = route.children;

    if (children.length === 0) {
      return breadcrumbs;
    }

    for (const child of children) {
      const routeURL: string = child.snapshot.url.map(segment => segment.path).join('/');
      if (routeURL !== '') {
        url += `/${routeURL}`;
      }

      const label = child.snapshot.data['breadcrumb'];
      if (label) {
        breadcrumbs.push({
          label,
          url,
          icon: child.snapshot.data['icon']
        });
      }

      return this.buildBreadcrumbs(child, url, breadcrumbs);
    }

    return breadcrumbs;
  }
}

2. 面包屑组件

typescript
// components/breadcrumb.component.ts
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { BreadcrumbService, BreadcrumbItem } from '../services/breadcrumb.service';

@Component({
  selector: 'app-breadcrumb',
  template: `
    <nav class="breadcrumb" *ngIf="breadcrumbs$ | async as breadcrumbs">
      <ol class="breadcrumb-list">
        <li class="breadcrumb-item" *ngFor="let item of breadcrumbs; let last = last">
          <a *ngIf="!last" [routerLink]="item.url" class="breadcrumb-link">
            <i *ngIf="item.icon" [class]="item.icon"></i>
            {{ item.label }}
          </a>
          <span *ngIf="last" class="breadcrumb-current">
            <i *ngIf="item.icon" [class]="item.icon"></i>
            {{ item.label }}
          </span>
        </li>
      </ol>
    </nav>
  `,
  styleUrls: ['./breadcrumb.component.css']
})
export class BreadcrumbComponent implements OnInit {
  breadcrumbs$: Observable<BreadcrumbItem[]>;

  constructor(private breadcrumbService: BreadcrumbService) {}

  ngOnInit() {
    this.breadcrumbs$ = this.breadcrumbService.breadcrumbs$;
  }
}

3. 路由配置面包屑

typescript
// 路由配置
const routes: Routes = [
  {
    path: 'dashboard',
    component: DashboardComponent,
    data: { breadcrumb: '仪表板', icon: 'fas fa-tachometer-alt' }
  },
  {
    path: 'users',
    component: UserListComponent,
    data: { breadcrumb: '用户管理', icon: 'fas fa-users' },
    children: [
      {
        path: ':id',
        component: UserDetailComponent,
        data: { breadcrumb: '用户详情', icon: 'fas fa-user' }
      }
    ]
  }
];

🎮 实践练习

练习1:实现完整的权限系统

创建一个完整的权限系统,包括:

  • 基于角色的访问控制
  • 路由守卫保护
  • 动态菜单显示
  • 权限验证服务

练习2:实现复杂的导航系统

创建一个复杂的导航系统,包括:

  • 面包屑导航
  • 路由动画
  • 懒加载模块
  • 预加载策略

📚 详细示例

完整的路由配置

typescript
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes, PreloadAllModules } from '@angular/router';
import { AuthGuard } from './guards/auth.guard';
import { AdminGuard } from './guards/admin.guard';
import { UnsavedChangesGuard } from './guards/unsaved-changes.guard';
import { UserResolver } from './resolvers/user.resolver';
import { CustomPreloadingStrategy } from './strategies/custom-preloading.strategy';

const routes: Routes = [
  // 首页
  { 
    path: '', 
    redirectTo: '/home', 
    pathMatch: 'full' 
  },
  { 
    path: 'home', 
    component: HomeComponent,
    data: { 
      breadcrumb: '首页',
      icon: 'fas fa-home',
      animation: 'home'
    }
  },

  // 认证相关
  { 
    path: 'login', 
    component: LoginComponent,
    data: { 
      breadcrumb: '登录',
      animation: 'login'
    }
  },
  { 
    path: 'register', 
    component: RegisterComponent,
    data: { 
      breadcrumb: '注册',
      animation: 'register'
    }
  },

  // 用户相关(需要认证)
  { 
    path: 'profile', 
    loadChildren: () => import('./profile/profile.module').then(m => m.ProfileModule),
    canActivate: [AuthGuard],
    data: { 
      preload: true,
      breadcrumb: '个人资料'
    }
  },

  // 管理相关(需要管理员权限)
  { 
    path: 'admin', 
    loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
    canActivate: [AuthGuard, AdminGuard],
    canActivateChild: [AdminGuard],
    data: { 
      preload: false,
      role: 'admin',
      breadcrumb: '管理后台'
    }
  },

  // 用户详情(使用解析器)
  { 
    path: 'user/:id', 
    component: UserDetailComponent,
    canActivate: [AuthGuard],
    resolve: { user: UserResolver },
    data: { 
      breadcrumb: '用户详情',
      animation: 'user-detail'
    }
  },

  // 用户编辑(使用CanDeactivate守卫)
  { 
    path: 'user/:id/edit', 
    component: UserEditComponent,
    canActivate: [AuthGuard],
    canDeactivate: [UnsavedChangesGuard],
    resolve: { user: UserResolver },
    data: { 
      breadcrumb: '编辑用户',
      animation: 'user-edit'
    }
  },

  // 懒加载模块
  { 
    path: 'dashboard', 
    loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule),
    canActivate: [AuthGuard],
    canLoad: [AuthGuard],
    data: { 
      preload: true,
      breadcrumb: '仪表板'
    }
  },

  // 404页面
  { 
    path: '**', 
    component: NotFoundComponent,
    data: { 
      breadcrumb: '页面未找到'
    }
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes, {
    preloadingStrategy: CustomPreloadingStrategy,
    enableTracing: false, // 开发时启用路由跟踪
    scrollPositionRestoration: 'top', // 页面切换时滚动到顶部
    anchorScrolling: 'enabled', // 启用锚点滚动
    onSameUrlNavigation: 'reload' // 相同URL时重新加载
  })],
  exports: [RouterModule]
})
export class AppRoutingModule { }

✅ 学习检查

完成本章节后,请确认您能够:

  • [ ] 使用高级路由守卫
  • [ ] 创建和使用路由解析器
  • [ ] 实现懒加载和预加载
  • [ ] 添加路由动画
  • [ ] 实现面包屑导航
  • [ ] 配置路由元数据
  • [ ] 处理复杂的路由场景

🚀 下一步

完成本章节学习后,请继续学习10-模板驱动表单