Router 是 Angular 内建能力中复杂度最高的模块之一:URL 解析、路由匹配、守卫、数据预取、懒加载、参数继承,一条导航链路要经过完整的状态机。本文按”配置 → 匹配 → 激活 → 绑定”的顺序拆解原理,并覆盖 v22 的若干新变化。
路由配置基础
路由配置就是一个 Routes 数组,每项声明”URL 片段如何映射到组件”:
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'users',
component: UserListComponent,
title: '用户列表',
children: [
{ path: ':id', component: UserDetailComponent }, // /users/42
{ path: '', redirectTo: '1', pathMatch: 'full' },
],
},
{
path: 'settings',
loadChildren: () => import('./settings/settings.routes').then((m) => m.SETTINGS_ROUTES),
},
{ path: '**', redirectTo: '' },
];
在 bootstrap 中用 provideRouter 注册(Standalone 时代没有 AppRoutingModule):
bootstrapApplication(AppComponent, {
providers: [provideRouter(routes)],
});
组件模板中放置出口:
<nav>
<a routerLink="/users">用户</a>
<a routerLink="/settings">设置</a>
</nav>
<router-outlet />
路由匹配是怎么发生的
一次导航的完整生命周期:
graph TD
U[点击 RouterLink 或调用 router.navigate] --> A[NavigationStart]
A --> M[URL 段与 Routes 配置匹配]
M --> G[canMatch 逐条评估候选路由]
G --> AC[canActivate / canActivateChild 评估]
AC --> R[resolve 预取数据]
R --> L[惰性加载组件或子路由]
L --> AC2[激活组件 - 输入绑定生效]
AC2 --> E[NavigationEnd - 更新 URL]
G -- 拒绝 --> C[NavigationCancel]
AC -- 拒绝 --> C
几个匹配规则要点:
- 按特异性排序而不是配置顺序。Router 会先匹配更具体的路由(静态片段优先于参数,参数优先于通配符),所以
users/new放在users/:id前后都能正确匹配 **通配路由通常放最后做 404 兜底pathMatch: 'full'要求整个 URL 完全匹配,重定向路由(尤其空路径'')必须显式指定- 嵌套路由通过
children定义,子组件渲染在父组件的<router-outlet />中
懒加载:loadComponent 与 loadChildren
v22 应用的默认形态是 Standalone + 按路由分包:
export const routes: Routes = [
// 单组件级懒加载
{
path: 'profile',
loadComponent: () =>
import('./profile/profile.component').then((m) => m.ProfileComponent),
},
// 整个子树懒加载:一个功能区的路由 + 组件打进一个 chunk
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes').then((m) => m.ADMIN_ROUTES),
},
];
admin.routes.ts 导出一个子 Routes:
export const ADMIN_ROUTES: Routes = [
{ path: '', component: AdminLayoutComponent, children: [
{ path: 'audit', loadComponent: () => import('./audit/audit.component').then((m) => m.AuditComponent) },
]},
];
懒加载的收益由构建切分(per-route chunk)与首屏体积直接体现。经验法则:首屏路由之外的配置都写 loadComponent/loadChildren。
预加载策略
懒加载的代价是导航延迟,预加载策略在这之间做权衡:
import { PreloadAllModules, withPreloading } from '@angular/router';
provideRouter(routes, withPreloading(PreloadAllModules)),
PreloadAllModules:空闲时预取所有懒加载块(管理后台类应用常用)NoPreloading:默认,不预取- 自定义
PreloadingStrategy:按带宽/用户行为决定,例如只预取首屏可达的模块
守卫:canActivate 与 canMatch
守卫从 v14.2 起全面函数式。canActivate 在”路由即将激活”时评估:
import { CanActivateFn } from '@angular/router';
export const authGuard: CanActivateFn = (_route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isLoggedIn()) {
return true;
}
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url },
});
};
{ path: 'admin', canActivate: [authGuard], loadChildren: () => ... }
返回 UrlTree(推荐)会触发重定向;返回 false 则导航直接取消。
canMatch 与 v22 的第三参数
canMatch 更靠前:它在匹配阶段评估,可以决定”同一路径由哪个路由配置命中”。经典用法是按权限或特性开关分流到不同版本的模块:
import { CanMatchFn } from '@angular/router';
export const betaUserGuard: CanMatchFn = (route, segments, currentSnapshot) => {
// v22 起可用的第三个参数:当前 RouterStateSnapshot
// 适合需要结合当前路由状态的分流决策
const experiments = inject(ExperimentsService);
return experiments.isEnabled('new-settings', currentSnapshot);
};
export const routes: Routes = [
{
path: 'settings',
canMatch: [betaUserGuard],
loadComponent: () => import('./settings/settings-v2.component').then((m) => m.SettingsV2Component),
},
{
path: 'settings',
loadComponent: () => import('./settings/settings.component').then((m) => m.SettingsComponent),
},
];
canMatch 与 canActivate 的分工:canMatch 决定”这条路由配置是否参与匹配”(不匹配则继续尝试后面的配置,不会取消导航);canActivate 决定”匹配上了能不能进”(拒绝就是取消/重定向)。
withComponentInputBinding:参数即输入
这是现代 Angular 路由最优雅的能力:把路径参数、查询参数直接绑定到组件 input,告别在组件里注入 ActivatedRoute 订阅快照:
bootstrapApplication(AppComponent, {
providers: [
provideRouter(
routes,
withComponentInputBinding({
queryParams: true, // 查询参数也绑定到同名 input
unmatchedInputBehavior: 'preserve', // 路由未提供某 input 时保留组件现值
}),
),
],
});
@Component({...})
export class UserDetailComponent {
// 绑定 /users/:id 中的 id
readonly id = input.required<number>();
// queryParams: true 时,查询参数 ?tab=notes 同样绑定
readonly tab = input<string>('overview');
// id 变化时重新加载(用户在详情页间跳转)
readonly user = httpResource<UserDetail>(() => `/api/users/${this.id()}`);
}
注意这个模式与 httpResource 是天作之合:id input 变化自动触发重新请求,整条链路无订阅。
paramsInheritanceStrategy 默认 ‘always’
v22 中 paramsInheritanceStrategy 默认为 'always':子路由自动继承祖先路由的路径参数与数据(data/resolve),无论父路由是否有自己的路径片段。旧版默认 'emptyOnly' 只在父路径为空时继承。这意味着:
{ path: 'team/:teamId', children: [
{ path: 'member/:memberId', component: MemberComponent },
]}
MemberComponent 的输入绑定可以直接拿到 teamId 与 memberId 两个参数,无需逐层透传。若依赖旧行为,可在 provideRouter 的 withRouterConfig({ paramsInheritanceStrategy: 'emptyOnly' }) 中显式回退。
RouterLink 的 browserUrl 输入
某些交互场景下,希望”应用内部导航到的状态”与”浏览器地址栏展示的 URL”解耦,例如从弹窗打开详情但不希望刷新后落在弹窗态。v22 为 RouterLink 新增 browserUrl 输入,允许指定写入浏览器地址栏与历史的 URL:
<a
routerLink="/items/42"
browserUrl="/items?selected=42"
>
查看详情
</a>
导航目标仍是 /items/42,但地址栏展示 /items?selected=42,刷新后落在更合理的落地页状态。常规链接不需要关心它,仅在需要精细控制可分享 URL 时使用。
数据预取:resolve 的函数式写法
resolve 在激活前完成数据预取,函数式签名:
export const userResolver: ResolveFn<User> = (route) => {
const api = inject(UsersApi);
return api.getUser(Number(route.paramMap.get('id')));
};
{ path: 'users/:id', resolve: { user: userResolver }, loadComponent: () => ... }
一个判断:在 httpResource 时代,组件内声明式加载通常比 resolve 更简单(且自带缓存与重载能力);resolve 更适合”守卫就要用到数据”或”同一路由复用同一数据”的场景。
常见配置速查
| 需求 | 配置 |
|---|---|
| 首页路由 | { path: '', component: HomeComponent } |
| 参数路由 | { path: 'users/:id', ... } |
| 404 兜底 | { path: '**', redirectTo: '' } |
| 整块懒加载 | loadChildren: () => import(...) |
| 单组件懒加载 | loadComponent: () => import(...) |
| 登录保护 | canActivate: [authGuard] |
| 灰度分流 | canMatch: [experimentGuard] |
| 参数绑定到 input | withComponentInputBinding() |
| 空闲预加载 | withPreloading(PreloadAllModules) |
| 自定义 404 页标题 | title: '页面不存在'(支持 TitleStrategy) |