首页
/ Angular 路由常用进阶任务实战:组件输入绑定、404 页面、链接参数数组与 URL 策略

Angular 路由常用进阶任务实战:组件输入绑定、404 页面、链接参数数组与 URL 策略

2026-09-06 18:33:36作者:秋泉律Samson

本篇技术指南围绕 Angular 官方文档 common-router-tasks.md 展开,系统讲解日常开发中使用 Angular Router 时最常遇到的一批“公共任务”:通过组件输入(input)直接读取路由数据、为未知 URL 配置 404 兜底页面、使用链接参数数组驱动 RouterLink 导航,以及通过 LocationStrategy 控制浏览器 URL 风格。同时结合本仓库 packages/router 下的真实源码,说明每个功能开关在 Router 内部究竟如何工作。读完本文,你将能够直接复制上述能力到自己的 Angular 应用中,并能准确理解各配置项的默认值与副作用。

背景与适用范围

本文内容与当前仓库中的以下文件直接对应,建议阅读时对照查看:

涉及的核心实现位于 packages/router/src 目录,关键文件包括 provide_router.ts(提供各类 with* 路由特性)、router_config.ts(路由配置接口定义)与 directives/router_outlet.ts(输入绑定器 RoutedComponentInputBinder 的落地实现)。

下文所述代码以两种主流注册方式为前提:

  • Standalone / 函数式 API:在 bootstrapApplication 或根配置中通过 provideRouter(routes, ...features) 组合 Router 特性;
  • NgModule API:通过 RouterModule.forRoot(routes, extraOptions) 注册路由模块。

从路由获取信息:把路由数据绑定到组件输入

在应用中,路由的一个重要职责是传递参数。典型场景是:一个购物列表页中每条商品都有唯一 id,用户点击“编辑”后进入 EditGroceryItem 组件,该组件需要拿到当前商品 id 才能展示正确内容。

传统做法是在组件里注入 ActivatedRoute,手动订阅 params/queryParams/data。而 Angular Router 提供了一种更简洁的机制——组件输入绑定(Component Input Binding):让 Router 把“当前激活路由”中的各项数据,以 key-value 形式自动写入路由对应组件的 input() 属性。

启用该能力有两种等价方式:

注册方式 配置入口
provideRouter(appRoutes, withComponentInputBinding()) 函数式 Router 特性,见 packages/router/src/provide_router.ts
RouterModule.forRoot(routes, {bindToComponentInputs: true}) NgModule 风格,ExtraOptions.bindToComponentInputs,见 packages/router/src/router_config.ts

其中 bindToComponentInputs 的完整类型为 boolean | ComponentInputBindingOptions(见 router_config.ts),即它既可传 true,也可以传一个配置对象,含义与下文的 withComponentInputBinding(options) 完全一致。

三步启用组件输入绑定

第 1 步:在 provideRouter 中加入 withComponentInputBinding 特性

providers: [provideRouter(appRoutes, withComponentInputBinding())];

第 2 步:为组件添加与参数同名的 input

路由参数名与组件 input() 属性名一一对应。以下示例中路由若包含路径参数 :id,则组件的 id 输入会自动收到该值:

id = input.required<string>();
hero = computed(() => this.service.getHero(id()));

第 3 步(可选):处理“未匹配到数据”的情况,提供默认值

启用 withComponentInputBinding 后,Router 会根据当前路由把所有输入逐一赋值:如果路由数据中不存在与输入同名的键(例如某个可选 query 参数缺失),Router 会赋 undefined。因此当输入“可能不被路由匹配到”时,类型上应包含 undefined,并考虑给出默认值。默认值有两种推荐写法:

  • inputtransform 选项转换;
  • 或借助本地状态(如 linkedSignal)做兜底派生。
id = input.required({
  transform: (maybeUndefined: string | undefined) => maybeUndefined ?? '0',
});
// 或
id = input<string | undefined>();
internalId = linkedSignal(() => this.id() ?? getDefaultId());

注意:启用该特性后,绑定到组件输入的不仅是路径参数,而是当前路由中全部 key-value 数据,包括:静态或由 Resolver 解析出的路由 data、路径参数、matrix 参数以及 query 参数。

绑定数据的来源与优先级

从源码注释看,输入绑定的数据来源有四种:

  1. query 参数;
  2. 路径参数与 matrix 参数;
  3. 静态路由 data
  4. Resolver 解析出的 data

不同来源出现同名 key 时,优先级由低到高,即 Resolver 数据的优先级最高,会覆盖其余来源。

这一实现可以在 packages/router/src/directives/router_outlet.ts 中看到:RoutedComponentInputBindercombineLatest 同时订阅 queryParamsparamsdata(以及可选的资源绑定),再通过对象展开的先后顺序合并覆盖:

data = {
  ...queryParams,
  ...params,
  ...data,
  ...(activatedRoute.resources || {}),
};

合并完成后,绑定器遍历 reflectComponentType(...) 得到的组件输入列表,对每个输入调用 ComponentRef.setInput(templateName, value)(见 router_outlet.ts),从而实现“把路由数据直接推送到组件输入”。

关闭 query 参数绑定

如果你在应用里用别的方式(如独立的服务/状态)单独管理 query 参数,不希望它们也被写进组件输入,可以传入 ComponentInputBindingOptions 关闭 query 参数来源:

provideRouter(appRoutes, withComponentInputBinding({queryParams: false}));

ComponentInputBindingOptionspackages/router/src/router_config.ts 中定义,queryParams 字段默认为 true。从 router_outlet.ts 的构造逻辑可见,未显式传入时该选项会统一回填为 true;而当其为 false 时,绑定器的数据流中 query 参数会被替换为空的 of({})

配置“路由数据中不存在”的输入行为:unmatchedInputBehavior

默认情况下(行为名为 'alwaysUndefined'),一次导航中如果某个输入在当前路由数据里找不到同名键,Router 仍会把它设为 undefined。这样做的目的是防止旧数据残留——例如上一次导航的 query 参数被移除后,组件不会继续显示上一次的值。

但如果某个输入“从未”在当前激活路由的数据中出现过(例如组件的本地开关并非来自路由),你并不希望它被 Router 重置为 undefined。此时可把 unmatchedInputBehavior 设为 'undefinedIfStale'

provideRouter(appRoutes, withComponentInputBinding({unmatchedInputBehavior: 'undefinedIfStale'}));

router_outlet.ts 的实现可见其判定逻辑:绑定器为每个 outlet 维护一个 outletSeenKeys(记录当前组件实例生命周期内曾被路由数据“看到过”的 key)。当行为为 'undefinedIfStale' 时,只有该输入键在 seenKeys 中“曾经出现过”,才会被写入(当前值为 undefined 除外),从而保留从未被路由喂过数据的输入初值:

const behavior = this.options.unmatchedInputBehavior ?? 'alwaysUndefined';
for (const {templateName} of currentMirror.inputs) {
  ...
  const value = data[templateName];
  if (value !== undefined || behavior === 'alwaysUndefined' || seenKeys.has(templateName)) {
    outlet.activatedComponentRef.setInput(templateName, value);
  }
}

有一个需要留意的特例:当把 unmatchedInputBehavior: 'undefinedIfStale'queryParams: false 组合使用时,输入会保持初始值、不会被 Router 主动置空,matrix 参数除外——如果某次导航提供了某个 matrix 参数、后续导航又把它移除,Router 仍会将该输入设为 undefined,以避免残留过期数据。组合写法如下:

provideRouter(
  appRoutes,
  withComponentInputBinding({
    queryParams: false,
    unmatchedInputBehavior: 'undefinedIfStale',
  }),
);

继承父路由的数据

默认情况下,子路由会继承父路由的参数与数据(等价于 paramsInheritanceStrategy: 'always')。这意味着在子组件中可以直接访问父路由的参数信息,无需逐层传递。

如果你希望恢复 Angular 早期的行为——只有“空路径(empty path)路由”才会向子路由传递参数,可将 paramsInheritanceStrategy 设置为 'emptyOnly'

provideRouter(routes, withRouterConfig({paramsInheritanceStrategy: 'emptyOnly'}));

withRouterConfig 接受 RouterConfigOptionsparamsInheritanceStrategy?: 'emptyOnly' | 'always' 定义于 packages/router/src/router_config.ts),它会把选项以 ROUTER_CONFIGURATION token 的形式提供给 Router。除了参数继承策略,RouterConfigOptions 还包含如 onSameUrlNavigationurlUpdateStrategydefaultQueryParamsHandlingcanceledNavigationResolutionscrollPositionRestoration 等可配置项,完整清单见 customizing-route-behavior.md

显示 404 页面

当用户访问一个未定义的 URL 时,通常需要展示一个“页面不存在”的友好提示。实现方式是配置一条通配符路由(wildcard route):路由 path 写成 **component 指向 404 页面组件。通配符路由的详细概念参见 define-routes.md

const routes: Routes = [
  {path: 'first-component', component: First},
  {path: 'second-component', component: Second},
  {path: '**', component: PageNotFound}, // Wildcard route for a 404 page
];

规则要点:

  • path: '**' 的路由必须放在最后
  • Router 会按数组顺序自上而下匹配 URL,只有当请求的 URL 无法命中列表中任何更靠前的路径时,才会选中这条通配路由,并把用户导向 PageNotFound 组件;
  • 如果你的 404 页面还需要读取 URL 信息用于提示,可与上文“组件输入绑定”特性结合使用。

链接参数数组:驱动 RouterLink 的导航命令

链接参数数组(link parameters array)RouterLink 指令接收的导航命令格式,它包含两类要素:

  • 到达目标组件的路由路径
  • 要放进路由 URL 的必选与可选路由参数

把数组绑定给 RouterLink 即可触发导航。RouterLink 的具体实现(含 queryParamsfragmentqueryParamsHandlingrelativeTopreserveFragmentstate 等扩展输入)可参阅 packages/router/src/directives/router_link.ts

单层路由的基本用法

只有路径、不带参数的写法:

<a [routerLink]="['/heroes']">Heroes</a>

包含路径参数(两元素数组,hero.id 作为 :id 参数值)的写法:

<a [routerLink]="['/hero', hero.id]">
  <span class="badge">{{ hero.id }}</span
  >{{ hero.name }}
</a>

在数组中以对象字面量形式提供可选参数,如 {foo: 'foo'}

<a [routerLink]="['/crisis-center', {foo: 'foo'}]">Crisis Center</a>

这种语法向 URL 传递的是 matrix 参数——与某个特定 URL 段关联的可选参数,区别于作用于整条 URL 的 query 参数。matrix 参数使用分号语法(如 /crisis-center;foo=foo),更完整的说明见 read-route-state.md

关于链接数组中的路径段,还有两点实用细节(源于 router_link.ts 的文档说明):

  • 若首段以 / 开头,Router 从应用根路由开始解析(绝对路径);
  • 若首段以 ./ 开头或不带斜杠,则相对当前激活路由的子路由解析;../ 则向上一级路由查找。

多级(嵌套)路由的数组组合

以上三个例子足以覆盖“单层路由”应用的导航需求;当存在子路由(例如 Crisis Center 的嵌套路由)时,链接参数数组可以表达更多组合。

以下最小示例导航到危机中心默认指定的子路由:

<a [routerLink]="['/crisis-center']">Crisis Center</a>

逐项解读这个数组:

  • 数组第一项标识父路由(/crisis-center);
  • 该父路由没有参数;
  • 未指定子路由的默认路径,因此需要选择一条;
  • 实际要导航到的 CrisisList 子路由路径是 /(空路径),但无需显式写出末尾斜杠

再看从应用根部一路导航到 “Dragon Crisis”(危机详情)的链接:

<a [routerLink]="['/crisis-center', 1]">Dragon Crisis</a>

逐项解读:

  • 第一项标识父路由(/crisis-center);
  • 父路由没有参数;
  • 第二项标识关于某个特定危机的详情子路由(/:id);
  • 该详情子路由需要一个 id 路径参数;
  • 把 Dragon Crisis 的 id1)作为数组第二项传入;
  • 最终生成的路径为 /crisis-center/1

甚至可以在根组件模板中只用危机中心相关的路由来演示这种组合(对象参数可紧跟路径段):

@Component({
  template: `
    <h1 class="title">Angular Router</h1>
    <nav>
      <a [routerLink]="['/crisis-center']">Crisis Center</a>
      <a [routerLink]="['/crisis-center/1', {foo: 'foo'}]">Dragon Crisis</a>
      <a [routerLink]="['/crisis-center/2']">Shark Crisis</a>
    </nav>
    <router-outlet />
  `,
})
export class App {}

综上:链接参数数组的表达力足以覆盖一层、两层乃至更多层的路由深度,任意合法序列都由“路由路径 +(必选)路由参数 +(可选)路由参数对象”组成。与 router.navigate/Router.createUrlTree 相比,RouterLink 使用的正是同一套命令语法——它把数组交给 createUrlTree 计算出目标 UrlTree 后再生成 href,因此两者语义完全一致。

LocationStrategy 与浏览器 URL 风格

Router 在导航到新组件视图时,会同步更新浏览器地址栏的 location 与 history。现代 HTML5 浏览器支持 history.pushState 技术,可以在不触发服务器整页请求的前提下改写地址与历史记录,因此 Router 能拼出与“真实页面请求”几乎无差别的 URL。

两种 URL 风格

HTML5 pushState 风格的 URL 示例:

localhost:3002/crisis-center

Hash 风格的 URL 示例(旧浏览器只有在 # 之后发生的变化才不会触发服务器请求,Router 借此在应用内路由 URL 中插入 hash):

localhost:3002/src/#/crisis-center

两种 LocationStrategy

Router 通过两个 LocationStrategy 提供者来支持以上两种风格:

提供者 说明
PathLocationStrategy 默认的 “HTML5 pushState” 风格
HashLocationStrategy “hash URL” 风格

RouterModule.forRoot() 默认采用 PathLocationStrategy,因此它是开箱即用的默认策略。你可以在应用引导(bootstrap)阶段覆盖为 HashLocationStrategy

如何在两种 API 下切换 Hash 风格

NgModule 风格:在 RouterModule.forRootExtraOptions 中设置 useHash: trueuseHash 字段定义于 packages/router/src/router_config.ts):

@NgModule({
  imports: [RouterModule.forRoot(routes, {useHash: true})],
})
export class AppModule {}

函数式/Standalone 风格:Router 提供了 withHashLocation() 路由特性,组合进 provideRouter 即可:

providers: [provideRouter(appRoutes, withHashLocation())];

生产部署提示

采用默认的 PathLocationStrategy 时,应用 URL 中不包含 #。这意味着部署到生产服务器时,需要配置服务器端重写规则:把所有前端路由路径(如 /crisis-center)都回退到应用入口 index.html,由 Angular 应用接管后续渲染;否则用户直接刷新或深链访问该路径时,服务器会返回 404。而选用 HashLocationStrategy 后,由于路由变化全部发生在 # 之后、不会发给服务器,因此对静态托管的配置要求更低(代价是 URL 不够“干净”)。

关于提供者与引导过程的基础概念,可参考 adev/src/content/guide/di/defining-dependency-providers.md

小结

本文覆盖了使用 Angular Router 时四项高频任务及其配置要点:

  1. 组件输入绑定:通过 withComponentInputBinding()(或 bindToComponentInputs)把 query 参数、路径/matrix 参数、静态 data 与 Resolver 数据自动注入组件 input;可用 queryParams: false 关闭 query 来源、用 unmatchedInputBehavior: 'undefinedIfStale' 避免从未匹配过的输入被置空、用 paramsInheritanceStrategy: 'emptyOnly' 恢复旧式参数继承。
  2. 404 页面:在路由表末尾放置 path: '**' 的通配路由指向自定义 404 组件。
  3. 链接参数数组:掌握 ['/path', id, {optional: true}] 的数组语法,理解其可表示任意深度嵌套路由,与 Router.navigate/createUrlTree 共用同一套命令模型。
  4. URL 风格控制:默认 PathLocationStrategy(pushState)与可选 HashLocationStrategy 的差异,以及两种注册 API 下的切换方法。

建议结合 router-reference.md(API 速查)与 customizing-route-behavior.md(更多 RouterConfigOptions)继续深入了解;对实现细节感兴趣的读者可直接研读 packages/router/src/provide_router.ts 中各个 with* 特性函数及 packages/router/src/directives/router_outlet.ts 中的 RoutedComponentInputBinder

登录后查看全文
热门项目推荐
相关项目推荐