2.6 路由与导航 路由把 URL 映射到组件,管理页面切换与参数传递。变更检测视角下,路由是"最高频的整树变更源"之一:一次导航销毁旧视图、实例化新组件,然后以一轮完整检查收尾。理解路由的生命周期顺序,就理解了"切页面时界面为什么这么动"。 表达层(绑定、指令、管道)已经讲完,本节起讲数据与视图的三个入口:路由(用户换页)、表单(用户输入)、HTTP(服务器数据)。 先定位,再出发 视图内的世界到此已经齐备,本节把镜头拉到视图之外——URL 如何驱动整棵组件树的切换: 配置路由表:基础路径、参数、懒加载与守卫。 说明一次导航的完整时序:守卫 → 序列化解析 → 激活组件 → 检查。 用路由参数与查询参数做列表-详情联动。 解释懒加载边界对服务实例范围与首屏检查成本的影响。
路由把 URL 映射到组件,管理页面切换与参数传递。变更检测视角下,路由是"最高频的整树变更源"之一:一次导航销毁旧视图、实例化新组件,然后以一轮完整检查收尾。理解路由的生命周期顺序,就理解了"切页面时界面为什么这么动"。
表达层(绑定、指令、管道)已经讲完,本节起讲数据与视图的三个入口:路由(用户换页)、表单(用户输入)、HTTP(服务器数据)。
视图内的世界到此已经齐备,本节把镜头拉到视图之外——URL 如何驱动整棵组件树的切换:
// 路由表:路径、组件、守卫、懒加载的组合 import { Routes } from '@angular/router'; import { inject } from '@angular/core'; import { AuthGuard, RoleGuard } from './guards'; export const routes: Routes = [ { path: '', redirectTo: 'orders', pathMatch: 'full' }, // 空路径重定向 { path: 'orders', loadChildren: () => import('./orders/orders.routes') // 惰性加载:切到才拉代码 .then(m => m.ORDERS_ROUTES) }, { path: 'report', canActivate: [AuthGuard], // 进入前拦截 loadComponent: () => import('./report/report.component') .then(m => m.ReportComponent) }, { path: '**', redirectTo: 'orders' } // 兜底:未知路径回首页 ]; // orders 子路由(orders.routes.ts 内) export const ORDERS_ROUTES: Routes = [ { path: '', component: OrderListComponent }, // 列表页 { path: ':orderNo', component: OrderDetailComponent }, // 详情页,路径参数 { path: 'edit/:orderNo', canDeactivate: [RoleGuard], // 离开前拦截 component: OrderEditComponent } ];
导航不是瞬间跳变,而是一条流水线。从用户点击 routerLink 到新页面渲染:
守卫链的顺序细节排错时会用到:canActivate 按声明顺序执行,任何一个返回false或可观察的假值则整个导航取消;canDeactivate 先于 canActivate(先问"能不能离开",再问"能不能进入");resolve(旧机制,现多由组件内拉取取代)在激活前预取数据。
背景:订单列表页点击一行跳详情,详情按单号拉数据;返回列表要保留搜索关键词与页码。
操作:
// 列表页:带查询参数导航,参数不进路由表所以不触发组件重建 import { Router } from '@angular/router'; export class OrderListComponent { private router = inject(Router); openDetail(orderNo: string, keyword: string, page: number) { this.router.navigate(['/orders', orderNo], { queryParams: { kw: keyword, p: page } // 挂在问号后:/orders/A-1?kw=手机&p=2 }); } } // 详情页:订阅参数流,参数变化复用同一组件实例 import { ActivatedRoute } from '@angular/router'; export class OrderDetailComponent { private route = inject(ActivatedRoute); ngOnInit() { this.route.paramMap.subscribe(pm => { const no = pm.get('orderNo'); if (no) this.load(no); // A-1 切到 A-2 时组件不销毁,只重新拉数据 }); } load(orderNo: string) { /* HTTP 拉取,2.8 节展开 */ } }
结果:列表到详情正常跳转;详情页内用"上一单/下一单"切换单号时组件不重建,仅数据刷新;返回列表时关键词与页码保留,列表状态复原。
解读:路径参数决定"哪个组件",查询参数决定"组件什么状态"——前者变化可能换组件,后者永不换。订阅 paramMap 而非快照,是复用实例组件的必需写法。
变式:若详情页间希望保留滚动位置,需要自己存取(路由不保管);若希望返回列表不重新拉数据,把列表数据放进 providedIn root 的服务(回到 2.3 节共享半径的取舍)。
⚠️ 坑:路由跳转后界面没变,先查三处——路由表没挂到应用配置;路径参数与查询参数用反;守卫返回了可观察但没 complete。
惰性边界(loadChildren)首次命中时才下载并执行模块代码,直接收益是首屏包体。站在变更检测角度还有第二层收益:首屏不实例化的组件根本不进入组件树,首轮检查的绑定数量随之减少。首屏每少一个组件,首轮检查就少一整棵子树的表达式求值。
