第119课:Angular 路由——路由配置、导航、路由守卫、惰性加载模块
在单页应用中,路由负责将 URL 映射到具体的组件,实现无刷新页面切换。Angular 的路由系统(@angular/router)是一个功能完备的客户端导航库,它不仅提供了路径与组件的映射,还内置了导航守卫(控制用户能否进入或离开某个路由)、惰性加载(按需加载特性模块,减小首屏体积)、预加载策略、路由参数与查询参数的解析等企业级特性。与 React Router 和 Vue Router 相比,Angular Router 的设计更“面向模块”——它天然与 Angular 的 NgModule 体系集成,惰性加载的粒度是模块级别而非组件级别。理解 Angular 路由的配置方式、守卫的执行顺序和惰性加载的配置语法,是构建中大型 Angular 应用的基础。本节课将逐一拆解这些核心概念,并通过一个带权限验证和惰性加载的后台管理路由系统将所学知识串联。
1. 路由的基础配置:Routes、RouterModule 与 <router-outlet>
Angular 的路由是通过一个 Routes 数组定义的,每个元素是一个路由配置对象,至少包含 path 和 component(或 loadChildren)。路由模块通过 RouterModule.forRoot(routes)(根模块)或 RouterModule.forChild(routes)(特性模块)注册路由。
1.1 创建路由模块
1 | // app-routing.module.ts |
关键点:
RouterModule.forRoot(routes)仅在根模块中调用一次。它注册全局路由服务、启动路由监听、并初始化 Location 策略。RouterModule.forChild(routes)在特性模块中调用,用于注册该模块内部的子路由。path: '**'是通配符路由,匹配所有未被前面路由捕获的 URL,用于显示 404 页面。必须放在Routes数组的最后——Angular 按数组顺序匹配路由,通配符放前面会导致所有路径被它吞掉。
1.2 <router-outlet>:路由视图的占位符
<router-outlet> 是一个指令,标记了匹配到的路由组件应该渲染在 DOM 的什么位置。
1 | <!-- app.component.html --> |
**多个 <router-outlet>**:如果路由配置中使用了命名出口(outlet: 'sidebar'),你可以在同一个模板中放置多个 <router-outlet name="sidebar">,实现在不同区域同时切换不同组件。这对于复杂布局(如主内容区 + 侧边栏面板联动)非常有用。
2. 导航:routerLink、Router.navigate 与路由参数
2.1 声明式导航:routerLink
routerLink 是指令,绑定到 <a> 标签上,点击时触发路由导航而不会刷新页面。
1 | <!-- 静态路径 --> |
routerLinkActive 和 **routerLinkActiveOptions**:
routerLinkActive="className":当该链接的路径与当前 URL 匹配时,自动为元素添加指定的 CSS 类名。[routerLinkActiveOptions]="{ exact: true }":仅当精确匹配时才激活(默认为模糊匹配——当前 URL 以链接路径开头即为激活)。这对于根路径/特别重要,否则它在所有页面都会激活。
2.2 编程式导航:Router.navigate
在 TypeScript 代码中,通过 Router 服务进行编程式导航。
1 | import { Component } from '@angular/core'; |
navigate 与 navigateByUrl 的区别:
navigate(commands: any[], extras?):接收一个命令数组(类似routerLink的数组语法),Angular 将其拼接为 URL。navigateByUrl(url: string, extras?):接收一个完整的 URL 字符串(可以是绝对路径或相对路径),适用于需要动态构建完整 URL 或解析带查询参数和片段的字符串的场景。navigate更为常用。
2.3 读取路由参数与查询参数
在组件中,通过 ActivatedRoute 服务获取当前路由的信息。
1 | import { Component, OnInit } from '@angular/core'; |
**paramMap vs snapshot.paramMap**:
this.route.paramMap.subscribe(...)返回一个 Observable,当路由参数变化而组件被复用时(如从/users/1导航到/users/2),它会重新发出新值。这是推荐方式,因为它能正确处理同一组件内的参数切换。this.route.snapshot.paramMap仅在组件创建时获取一次参数快照。如果同一组件在参数变化时被复用,快照中的值不会更新,容易导致数据不同步的 Bug。
3. 路由守卫:控制导航的准入与离开
Angular 提供了五种路由守卫接口,允许你在路由导航的不同阶段插入逻辑——验证用户是否登录、检查权限、确认未保存的更改等。守卫返回 true(放行)、false(拒绝)或 UrlTree(重定向到另一个路由)。Angular v15+ 推荐使用函数式守卫(返回函数的函数),而非类式守卫。
3.1 守卫类型与执行顺序
| 守卫 | 触发时机 | 典型用途 |
|---|---|---|
canActivate |
进入路由之前。 | 登录验证、权限检查。 |
canActivateChild |
进入任意子路由之前。 | 统一保护一组子路由。 |
canDeactivate |
离开当前路由之前。 | 提示保存未提交的表单更改。 |
canMatch |
在路由匹配阶段(canActivate 之前)。 |
根据条件选择不同的路由配置(如移动端 vs 桌面端)。 |
resolve |
路由激活之前,用于预加载数据。 | 在组件渲染前获取必要数据。 |
执行顺序:
canDeactivate(离开当前路由)canMatch(匹配新路由)canActivateChild(父路由 → 子路由逐级检查)canActivate(目标路由)resolve(预加载数据)- 组件渲染
3.2 canActivate:登录验证(函数式守卫)
1 | // auth.guard.ts |
在路由配置中使用:
1 | const routes: Routes = [ |
类式守卫(旧版):在 Angular v14 及之前,守卫是实现 CanActivate 接口的类。虽然 v15+ 推荐函数式守卫,但类式守卫仍被支持,且在一些场景中更直观(如需要构造函数注入多个服务)。两种方式可以共存。
3.3 canDeactivate:防止未保存数据丢失
1 | // pending-changes.guard.ts |
组件实现接口:
1 | export class EditUserComponent implements CanComponentDeactivate { |
3.4 resolve:预加载数据
在路由激活之前获取数据,确保组件渲染时数据已就绪。
1 | // user.resolver.ts |
路由配置:
1 | const routes: Routes = [ |
组件中通过 ActivatedRoute.data 获取预加载的数据:
1 | ngOnInit(): void { |
4. 惰性加载:按需加载特性模块
默认情况下,Angular 将所有组件打包进主 bundle。当应用规模增大时,首屏加载体积会显著膨胀。惰性加载允许将特性模块拆分为独立的 JavaScript chunk,仅在用户导航到对应路由时才下载和执行。
4.1 loadChildren 语法
1 | const routes: Routes = [ |
关键机制:
loadChildren接收一个返回 Promise 的函数,该 Promise 解析为一个 NgModule 类(或 Standalone 路由配置数组)。Angular CLI 的打包工具(基于 Webpack 或 esbuild)会自动将import()的目标模块拆分为独立的 chunk。- 惰性加载的模块拥有独立的注入器(子注入器)。这意味着该模块中通过
providers注册的服务是该模块私有的单例,不会与根注入器或其他惰性模块中的同名服务互相干扰。 - 不要在急切加载的模块中导入惰性模块——这会破坏惰性加载,使其代码被打包进主 bundle。只有路由配置中的
loadChildren才能触发真正的按需加载。
4.2 standalone 组件的惰性加载(Angular v15+)
在 Standalone 模式下,不需要 NgModule——可以直接惰性加载路由配置数组:
1 | const routes: Routes = [ |
1 | // user/user.routes.ts |
4.3 预加载策略
惰性加载优化了首屏体积,但在用户导航到惰性模块时仍然会有短暂的下载延迟。预加载策略允许在首屏加载完成后,在浏览器空闲时段后台预先下载惰性模块,从而兼顾首屏速度和后续导航的即时响应。
Angular 内置了两种预加载策略:
- **
NoPreloading**(默认):不预加载任何惰性模块。 - **
PreloadAllModules**:首屏加载完成后,立即预加载所有惰性模块。
1 | import { PreloadAllModules } from '@angular/router'; |
你也可以实现自定义预加载策略,根据网络状况、用户权限或业务优先级选择性地预加载特定模块。
5. 综合实战:带权限验证和惰性加载的后台管理系统路由
以下代码展示了路由配置、守卫、惰性加载和预加载策略在一个实际场景中的协同工作。
1 | // auth.service.ts |
1 | // user/user-routing.module.ts |
架构要点:
authGuard和adminGuard是函数式守卫,通过inject获取所需服务,无需在@NgModule.providers中注册。users和admin模块使用loadChildren惰性加载——首屏不下载它们的代码。PreloadAllModules在首屏加载完成后预加载所有惰性模块,用户导航时秒开。userResolver在用户详情页激活前预加载用户数据,组件渲染时数据已就绪。UserModule中的RouterModule.forChild(routes)配置子路由,与根路由无缝衔接。
课后练习
一、概念自测(选择题 / 填空题)
(单选) Angular 中,
RouterModule.forRoot(routes)和RouterModule.forChild(routes)的区别是?
A.forRoot用于根模块,注册全局路由服务;forChild用于特性模块,配置子路由。
B.forRoot用于惰性加载模块,forChild用于急切加载模块。
C. 两者功能完全相同,仅命名不同。
D.forRoot仅用于 Standalone 组件。(单选) 当从
/users/1导航到/users/2时,为了正确获取新参数,应如何读取路由参数?
A. 使用this.route.snapshot.paramMap.get('userId')。
B. 使用this.route.paramMap.subscribe(...)。
C. 使用window.location.pathname手动解析。
D. 组件会被销毁并重建,ngOnInit会重新执行,因此快照即可。(填空) 要防止用户在未保存表单数据时意外离开当前页面,应使用
______路由守卫。(多选) 关于 Angular 惰性加载的描述,哪些是正确的?
A. 使用loadChildren可以按需加载特性模块。
B. 惰性加载的模块拥有独立的注入器。
C. 惰性加载可以减小首屏 JavaScript 体积。
D.PreloadAllModules可以在首屏后后台预加载所有惰性模块。
二、AI 编程任务:编写面向 AI 的提示词
场景:你需要为一个博客平台配置 Angular 路由。要求如下:
- 使用
RouterModule.forRoot配置根路由。 - 路由表包含:
/(首页)、/blog(博客列表,惰性加载)、/blog/:postId(博客详情,惰性加载,预解析文章数据)、/about(关于页)、/login(登录页)。 - 为
/blog路由添加canActivate守卫,要求用户登录后才能访问,未登录重定向到/login并携带redirect查询参数。 - 为博客详情页添加
resolve,在组件渲染前获取文章数据。 - 配置
PreloadAllModules预加载策略。 - 添加
**通配符路由显示 404 页面。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成完整的 Angular 路由配置文件(含守卫和解析器)。提示词中需明确指定路由结构、惰性加载语法、守卫函数式写法、解析器的数据获取方式。
三、Agent 模式下的提示词示例
你是一个资深前端开发 Agent。请为 Angular 博客平台创建路由配置、认证守卫和文章解析器。需要创建以下文件:
src/app/app-routing.module.ts:
- 导入
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })。- 路由表:
/→HomeComponent;/blog→loadChildren: () => import('./blog/blog.module').then(m => m.BlogModule),canActivate: [authGuard];/about→AboutComponent;/login→LoginComponent;**→NotFoundComponent。src/app/guards/auth.guard.ts:
- 函数式守卫
authGuard: CanActivateFn。使用inject(AuthService)检查登录状态。若未登录,返回router.createUrlTree(['/login'], { queryParams: { redirect: state.url } })。src/app/resolvers/post.resolver.ts:
- 函数式解析器
postResolver: ResolveFn<Post>。通过route.paramMap.get('postId')获取文章 ID,调用inject(PostService).getPostById(id)返回Observable<Post>。src/app/blog/blog-routing.module.ts:
forChild配置:''→BlogListComponent;':postId'→BlogDetailComponent,resolve: { post: postResolver }。- 所有代码添加 JSDoc 注释。完成后列出所有文件内容。
四、面试真题与参考答案
题目(腾讯前端面试题):
请解释 Angular 路由守卫的执行顺序。如果一个路由同时配置了
canActivate、canDeactivate和resolve,它们分别在什么时机触发?惰性加载模块在什么时候被下载和执行?如果路由守卫返回false,惰性模块还会被下载吗?
参考答案:
Angular 路由守卫的完整执行顺序为:离开当前路由时先触发 canDeactivate(提示保存未提交更改)。然后进入匹配阶段,canMatch 决定是否使用该路由配置。匹配确定后,从父到子逐级触发 canActivateChild,最后触发目标路由的 canActivate。所有这些守卫都通过后,执行 resolve 预加载数据。全部完成后,组件被创建并渲染。
惰性加载模块的下载时机在守卫执行之后——Angular 首先执行 canMatch 和 canActivate 守卫,仅当这些守卫都返回 true 时才触发 loadChildren 去下载对应的 JavaScript chunk。如果守卫返回 false(拒绝导航)或返回 UrlTree(重定向),惰性模块的代码不会被下载。这一设计避免了无意义的网络请求——用户没有权限访问的页面,其代码甚至不会被加载到浏览器中。
如果守卫返回 false,导航完全取消,URL 恢复到导航前的状态。如果守卫返回 UrlTree,Angular 会使用新的 URL 树重新开始导航(相当于内部重定向),同样不会下载原始目标路由的惰性模块。这一机制使得权限控制和代码加载天然解耦,既安全又高效。
课后练习答案
一、概念自测答案
A
- 解析:
forRoot在根模块中调用,注册全局路由服务;forChild在特性模块中调用,配置该模块的子路由。
- 解析:
B
- 解析:同一组件在参数变化时不会被销毁重建,必须使用
this.route.paramMap.subscribe(...)来响应参数变化。A 的快照模式仅在组件创建时读取一次,无法获取更新后的参数。
- 解析:同一组件在参数变化时不会被销毁重建,必须使用
canDeactivate- 解析:
canDeactivate在离开当前路由前触发,用于提示保存未提交的更改。
- 解析:
A、B、C、D
- 解析:四项均为惰性加载和预加载策略的正确描述。
loadChildren实现按需加载;惰性模块拥有独立注入器;惰性加载减小首屏体积;PreloadAllModules在首屏后后台预加载。
- 解析:四项均为惰性加载和预加载策略的正确描述。
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请为 Angular 博客平台配置路由。要求:
- 根路由使用 forRoot,配置 PreloadAllModules。
- 路由:/(首页)、/blog(惰性加载,auth 守卫)、/blog/:postId(惰性加载,post 解析器)、/about、/login、**(404)。
- auth 守卫:未登录重定向到 /login,携带 redirect 参数。
- post 解析器:从路由参数获取 postId,调用 PostService 获取文章。
- 输出 app-routing.module.ts、auth.guard.ts、post.resolver.ts、blog-routing.module.ts 的完整代码。”