11_路由

路由:构建单页应用导航系统

路由是单页应用(SPA)的核心机制,负责管理不同视图之间的导航,而无需重新加载整个页面。Angular提供了强大的路由模块,支持动态路由、嵌套路由、路由守卫、延迟加载等高级特性。Angular v20进一步优化了路由系统,强化了与独立组件的集成,并简化了配置方式。本章将详细讲解路由的核心概念与实战技巧。

1. 路由基础:配置与导航

1.1 路由配置核心要素

Angular路由通过配置路由规则(Routes数组)定义URL与组件的映射关系。v20推荐使用provideRouter函数替代传统的RouterModule,实现更简洁的路由配置。

基本路由配置

// app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
import { ContactComponent } from './contact/contact.component';

// 路由规则数组
export const routes: Routes = [
  { path: '', component: HomeComponent }, // 首页(默认路由)
  { path: 'about', component: AboutComponent }, // 关于页
  { path: 'contact', component: ContactComponent }, // 联系页
  { path: '**', redirectTo: '' } // 通配符路由(404页面)
];

main.ts中注册路由配置:

// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { provideRouter } from '@angular/router'; // 导入路由配置函数
import { routes } from './app.routes';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes) // 配置路由服务
  ]
}).catch(err => console.error(err));

1.2 路由出口与导航链接

路由出口(Router Outlet)

router-outlet是路由视图的占位符,用于显示当前激活路由对应的组件:

// app.component.ts
import { Component } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, RouterLink], // 导入路由相关指令
  template: `
    <!-- 导航链接 -->
    <nav>
      <a routerLink="/">首页</a> |
      <a routerLink="/about">关于我们</a> |
      <a routerLink="/contact">联系我们</a>
    </nav>
    
    <!-- 路由出口:显示匹配的组件 -->
    <router-outlet></router-outlet>
  `
})
export class AppComponent {}

routerLink指令用于创建导航链接,支持相对路径和绝对路径:

  • 绝对路径:以/开头,从根路由开始匹配(如routerLink="/about"
  • 相对路径:不以/开头,相对于当前路由(如在/user页面中,routerLink="profile"等价于/user/profile

还可以通过routerLinkActive指令高亮当前激活的导航项:

<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">首页</a>
<a routerLink="/about" routerLinkActive="active">关于我们</a>
  • routerLinkActive="active":当链接对应的路由激活时,添加active
  • { exact: true }:精确匹配(仅当URL完全匹配时才激活,适用于根路由)

2. 动态路由与参数传递

实际应用中经常需要根据动态参数(如ID)加载不同内容(如用户详情、商品详情),这需要使用动态路由。

2.1 定义动态路由

在路由配置中使用:参数名定义动态片段:

// app.routes.ts
import { UserDetailComponent } from './user-detail/user-detail.component';

export const routes: Routes = [
  // ...其他路由
  { path: 'users/:id', component: UserDetailComponent } // 动态路由:id为参数
];

2.2 获取路由参数

通过ActivatedRoute服务获取动态路由参数,v20推荐结合toSignal将参数转换为Signal:

// user-detail.component.ts
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { UserService } from './user.service';

@Component({
  selector: 'app-user-detail',
  standalone: true,
  template: `
    @if (user()) {
      <h2>用户详情 #{{ user()?.id }}</h2>
      <p>姓名:{{ user()?.name }}</p>
      <p>邮箱:{{ user()?.email }}</p>
    } @else if (error()) {
      <p class="error">{{ error() }}</p>
    } @else {
      <p>加载中...</p>
    }
  `
})
export class UserDetailComponent {
  private route = inject(ActivatedRoute);
  private userService = inject(UserService);
  
  // 获取路由参数(id)并转换为Signal
  userId = toSignal(this.route.paramMap.pipe(
    map(params => params.get('id'))
  ));
  
  // 根据id加载用户数据
  user = toSignal(
    this.userId.pipe(
      switchMap(id => id ? this.userService.getUserById(+id) : of(null)),
      catchError(err => {
        this.error.set('加载用户失败');
        return of(null);
      })
    ),
    { initialValue: null }
  );
  
  error = signal('');
}

2.3 路由查询参数

除了动态片段,还可以通过查询参数(如?page=1&limit=10)传递额外信息:

// 导航时添加查询参数
<a [routerLink]="['/users']" [queryParams]="{ page: 1, limit: 10 }">用户列表</a>

// 在组件中获取查询参数
import { ActivatedRoute } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';

@Component({ /* ... */ })
export class UserListComponent {
  private route = inject(ActivatedRoute);
  
  // 获取查询参数
  queryParams = toSignal(this.route.queryParamMap, { initialValue: new ParamMap({}) });
  
  ngOnInit() {
    const page = this.queryParams().get('page'); // 获取page参数
    const limit = this.queryParams().get('limit'); // 获取limit参数
  }
}

3. 嵌套路由:构建复杂页面结构

嵌套路由允许在一个组件中嵌套另一个路由视图,适合构建具有层级关系的页面(如仪表板、带侧边栏的页面)。

3.1 配置嵌套路由

使用children属性定义子路由:

// app.routes.ts
import { DashboardComponent } from './dashboard/dashboard.component';
import { DashboardHomeComponent } from './dashboard/home/home.component';
import { DashboardStatsComponent } from './dashboard/stats/stats.component';
import { DashboardSettingsComponent } from './dashboard/settings/settings.component';

export const routes: Routes = [
  // ...其他路由
  { 
    path: 'dashboard', 
    component: DashboardComponent,
    // 子路由配置
    children: [
      { path: '', redirectTo: 'home', pathMatch: 'full' }, // 默认子路由
      { path: 'home', component: DashboardHomeComponent },
      { path: 'stats', component: DashboardStatsComponent },
      { path: 'settings', component: DashboardSettingsComponent }
    ]
  }
];

3.2 实现嵌套路由视图

在父组件(DashboardComponent)中添加router-outlet显示子路由组件:

// dashboard.component.ts
import { Component } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-dashboard',
  standalone: true,
  imports: [RouterOutlet, RouterLink],
  template: `
    <div class="dashboard">
      <aside>
        <h2>仪表盘</h2>
        <nav>
          <a routerLink="home">首页</a>
          <a routerLink="stats">统计数据</a>
          <a routerLink="settings">设置</a>
        </nav>
      </aside>
      
      <main>
        <!-- 子路由出口:显示子组件 -->
        <router-outlet></router-outlet>
      </main>
    </div>
  `,
  styles: [`
    .dashboard { display: flex; gap: 20px; }
    aside { width: 200px; background: #f5f5f5; padding: 1rem; }
    main { flex: 1; padding: 1rem; }
  `]
})
export class DashboardComponent {}

访问/dashboard会自动重定向到/dashboard/home,并在父组件的router-outlet中显示DashboardHomeComponent

4. 路由守卫:控制路由访问

路由守卫用于在导航发生前、发生中或发生后执行特定逻辑,如验证用户权限、确认表单未保存、加载必要数据等。

4.1 常用路由守卫类型

守卫接口 作用时机 典型用途
CanActivate 进入路由前 验证用户是否有权限访问
CanDeactivate 离开路由前 确认是否放弃未保存的更改
Resolve 进入路由前 预加载路由所需数据
CanLoad 加载懒加载模块前 控制是否允许加载模块

4.2 实现权限守卫(CanActivate)

创建认证守卫,只允许登录用户访问特定路由:

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

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
  constructor(
    private authService: AuthService,
    private router: Router
  ) {}
  
  // 决定是否允许激活路由
  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): boolean {
    if (this.authService.isLoggedIn()) {
      return true; // 已登录,允许访问
    } else {
      // 未登录,重定向到登录页,并记录目标URL
      this.router.navigate(['/login'], { 
        queryParams: { returnUrl: state.url } 
      });
      return false; // 阻止访问
    }
  }
}

在路由配置中应用守卫:

// app.routes.ts
import { AuthGuard } from './auth.guard';

export const routes: Routes = [
  { 
    path: 'dashboard', 
    component: DashboardComponent,
    children: [/* ... */],
    canActivate: [AuthGuard] // 应用认证守卫
  }
];

4.3 实现离开确认守卫(CanDeactivate)

防止用户意外离开未保存的表单页面:

// unsaved-changes.guard.ts
import { Injectable } from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { FormComponent } from './form/form.component';

// 定义表单组件需实现的接口
export interface CanComponentDeactivate {
  canDeactivate: () => boolean | Promise<boolean>;
}

@Injectable({ providedIn: 'root' })
export class UnsavedChangesGuard implements CanDeactivate<CanComponentDeactivate> {
  canDeactivate(component: CanComponentDeactivate): boolean | Promise<boolean> {
    // 调用组件的canDeactivate方法
    return component.canDeactivate ? component.canDeactivate() : true;
  }
}

在表单组件中实现接口:

// form.component.ts
import { Component } from '@angular/core';
import { FormGroup, FormControl, ReactiveFormsModule } from '@angular/forms';
import { CanComponentDeactivate } from './unsaved-changes.guard';

@Component({
  selector: 'app-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `<!-- 表单内容 -->`
})
export class FormComponent implements CanComponentDeactivate {
  form = new FormGroup({
    name: new FormControl('')
  });
  isSaved = false;
  
  // 实现CanComponentDeactivate接口
  canDeactivate(): boolean | Promise<boolean> {
    if (this.form.dirty && !this.isSaved) {
      // 表单已修改且未保存,询问用户
      return confirm('您有未保存的更改,确定要离开吗?');
    }
    return true;
  }
  
  onSave() {
    this.isSaved = true;
    // 保存逻辑...
  }
}

应用离开守卫:

// app.routes.ts
import { UnsavedChangesGuard } from './unsaved-changes.guard';

export const routes: Routes = [
  { 
    path: 'form', 
    component: FormComponent,
    canDeactivate: [UnsavedChangesGuard] // 应用离开守卫
  }
];

5. 延迟加载:优化应用性能

延迟加载(Lazy Loading)允许在用户需要时才加载特定组件,减少初始加载时间,提升应用性能。Angular v20对独立组件的延迟加载支持更加完善。

5.1 延迟加载独立组件

使用loadComponent函数实现组件的延迟加载:

// app.routes.ts
import { Routes } from '@angular/router';

// 不直接导入组件,而是通过loadComponent动态加载
export const routes: Routes = [
  // ...其他路由
  { 
    path: 'admin',
    // 延迟加载AdminComponent
    loadComponent: () => import('./admin/admin.component')
      .then(m => m.AdminComponent),
    canActivate: [AuthGuard] // 结合守卫使用
  }
];

5.2 带路由的延迟加载模块

对于复杂功能模块,可创建包含子路由的延迟加载模块:

// admin.routes.ts
import { Routes } from '@angular/router';
import { AdminDashboardComponent } from './admin-dashboard/admin-dashboard.component';
import { UserManagementComponent } from './user-management/user-management.component';

export const adminRoutes: Routes = [
  { path: '', component: AdminDashboardComponent },
  { path: 'users', component: UserManagementComponent }
];

// app.routes.ts
export const routes: Routes = [
  // ...其他路由
  { 
    path: 'admin',
    // 延迟加载包含子路由的模块
    loadChildren: () => import('./admin/admin.module')
      .then(m => m.AdminModule)
  }
];

AdminModule的实现:

// admin.module.ts
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { adminRoutes } from './admin.routes';
import { AdminDashboardComponent } from './admin-dashboard/admin-dashboard.component';
import { UserManagementComponent } from './user-management/user-management.component';

@NgModule({
  imports: [
    RouterModule.forChild(adminRoutes), // 配置子路由
    AdminDashboardComponent,
    UserManagementComponent
  ]
})
export class AdminModule {}

6. 编程式导航与路由状态

除了使用routerLink指令进行声明式导航,还可以通过Router服务实现编程式导航。

6.1 基本编程式导航

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

@Component({
  selector: 'app-nav-buttons',
  standalone: true,
  template: `
    <button (click)="goToHome()">首页</button>
    <button (click)="goToUser(123)">用户123</button>
    <button (click)="goBack()">返回</button>
  `
})
export class NavButtonsComponent {
  private router = inject(Router);
  
  // 导航到首页
  goToHome() {
    this.router.navigate(['/']); // 等价于routerLink="/"
  }
  
  // 导航到带参数的路由
  goToUser(id: number) {
    this.router.navigate(['/users', id]); // 等价于routerLink="/users/123"
  }
  
  // 返回上一页
  goBack() {
    this.router.navigate(['../'], { relativeTo: this.route }); // 相对导航
    // 或使用history.back()
    // window.history.back();
  }
}

6.2 传递状态数据

导航时可以通过state属性传递不显示在URL中的状态数据:

// 导航时传递状态
this.router.navigate(['/user', 123], {
  state: { from: 'dashboard', timestamp: new Date().getTime() }
});

// 在目标组件中获取状态
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({ /* ... */ })
export class UserDetailComponent {
  constructor(private route: ActivatedRoute) {
    // 获取导航状态
    const navigationState = this.route.snapshot.navigationState;
    console.log('来源:', navigationState?.from);
    console.log('时间戳:', navigationState?.timestamp);
  }
}

7. Angular v20路由优化特性

7.1 独立组件路由增强

v20简化了独立组件的路由配置,无需模块即可直接配置路由,减少样板代码:

// 独立组件直接作为路由目标,无需模块包装
export const routes: Routes = [
  { path: 'profile', component: ProfileComponent } // ProfileComponent是独立组件
];

7.2 树摇优化

v20的路由系统更加支持树摇(Tree-shaking),未使用的路由功能(如特定守卫、解析器)会被从最终构建产物中移除,减小应用体积。

7.3 与Signals深度集成

通过toSignal将路由参数、查询参数等转换为Signals,实现更高效的响应式更新:

// 路由参数转换为Signal
const userId = toSignal(
  this.route.paramMap.pipe(map(params => params.get('id'))),
  { initialValue: null }
);

// 基于参数的响应式数据加载
const user = computed(() => {
  if (userId()) {
    return this.userService.getUser(+userId());
  }
  return null;
});

总结

路由是Angular单页应用的核心机制,通过本章学习,你已掌握:

  • 路由的基本配置与导航方式,包括provideRouterrouter-outletrouterLink
  • 动态路由与参数传递,以及如何获取路由参数和查询参数
  • 嵌套路由的配置与实现,构建复杂页面结构
  • 路由守卫的使用,控制路由访问和离开行为
  • 延迟加载的实现,优化应用性能
  • 编程式导航与路由状态管理

Angular v20的路由系统在独立组件支持、树摇优化和Signals集成方面的改进,使路由配置更加简洁高效。在实际开发中,合理设计路由结构、应用延迟加载和路由守卫,能显著提升应用的性能和用户体验。

posted @ 2025-09-21 18:19  S&L·chuck  阅读(10)  评论(0)    收藏  举报