CHARLIE SAYS

查理如是说
DATE 2026-08-24
THEME
SERIES / ANGULAR / P-190 · Angular 高级教程

Angular 22+ 教程 24:路由原理篇

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),
  },
];

canMatchcanActivate 的分工: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 的输入绑定可以直接拿到 teamIdmemberId 两个参数,无需逐层透传。若依赖旧行为,可在 provideRouterwithRouterConfig({ paramsInheritanceStrategy: 'emptyOnly' }) 中显式回退。

某些交互场景下,希望”应用内部导航到的状态”与”浏览器地址栏展示的 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]
参数绑定到 inputwithComponentInputBinding()
空闲预加载withPreloading(PreloadAllModules)
自定义 404 页标题title: '页面不存在'(支持 TitleStrategy)

系列导航

← 算法 024:图:拓扑排序(Topological sort) 目录 设计模式 024:策略(Strategy) →
← 返回文章列表