Angular 路由常用进阶任务实战:组件输入绑定、404 页面、链接参数数组与 URL 策略
本篇技术指南围绕 Angular 官方文档 common-router-tasks.md 展开,系统讲解日常开发中使用 Angular Router 时最常遇到的一批“公共任务”:通过组件输入(input)直接读取路由数据、为未知 URL 配置 404 兜底页面、使用链接参数数组驱动 RouterLink 导航,以及通过 LocationStrategy 控制浏览器 URL 风格。同时结合本仓库 packages/router 下的真实源码,说明每个功能开关在 Router 内部究竟如何工作。读完本文,你将能够直接复制上述能力到自己的 Angular 应用中,并能准确理解各配置项的默认值与副作用。
背景与适用范围
本文内容与当前仓库中的以下文件直接对应,建议阅读时对照查看:
- 指南主体:adev/src/content/guide/routing/common-router-tasks.md
- 路由定义与通配符:adev/src/content/guide/routing/define-routes.md
- 路由配置选项与自定义行为:adev/src/content/guide/routing/customizing-route-behavior.md
- 读取路由状态(含 matrix 参数):adev/src/content/guide/routing/read-route-state.md
涉及的核心实现位于 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,并考虑给出默认值。默认值有两种推荐写法:
- 用
input的transform选项转换; - 或借助本地状态(如
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 参数。
绑定数据的来源与优先级
从源码注释看,输入绑定的数据来源有四种:
- query 参数;
- 路径参数与 matrix 参数;
- 静态路由
data; - Resolver 解析出的
data。
不同来源出现同名 key 时,优先级由低到高,即 Resolver 数据的优先级最高,会覆盖其余来源。
这一实现可以在 packages/router/src/directives/router_outlet.ts 中看到:RoutedComponentInputBinder 用 combineLatest 同时订阅 queryParams、params、data(以及可选的资源绑定),再通过对象展开的先后顺序合并覆盖:
data = {
...queryParams,
...params,
...data,
...(activatedRoute.resources || {}),
};
合并完成后,绑定器遍历 reflectComponentType(...) 得到的组件输入列表,对每个输入调用 ComponentRef.setInput(templateName, value)(见 router_outlet.ts),从而实现“把路由数据直接推送到组件输入”。
关闭 query 参数绑定
如果你在应用里用别的方式(如独立的服务/状态)单独管理 query 参数,不希望它们也被写进组件输入,可以传入 ComponentInputBindingOptions 关闭 query 参数来源:
provideRouter(appRoutes, withComponentInputBinding({queryParams: false}));
ComponentInputBindingOptions 在 packages/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 接受 RouterConfigOptions(paramsInheritanceStrategy?: 'emptyOnly' | 'always' 定义于 packages/router/src/router_config.ts),它会把选项以 ROUTER_CONFIGURATION token 的形式提供给 Router。除了参数继承策略,RouterConfigOptions 还包含如 onSameUrlNavigation、urlUpdateStrategy、defaultQueryParamsHandling、canceledNavigationResolution、scrollPositionRestoration 等可配置项,完整清单见 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 的具体实现(含 queryParams、fragment、queryParamsHandling、relativeTo、preserveFragment、state 等扩展输入)可参阅 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 的
id(1)作为数组第二项传入; - 最终生成的路径为
/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.forRoot 的 ExtraOptions 中设置 useHash: true(useHash 字段定义于 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 时四项高频任务及其配置要点:
- 组件输入绑定:通过
withComponentInputBinding()(或bindToComponentInputs)把 query 参数、路径/matrix 参数、静态 data 与 Resolver 数据自动注入组件input;可用queryParams: false关闭 query 来源、用unmatchedInputBehavior: 'undefinedIfStale'避免从未匹配过的输入被置空、用paramsInheritanceStrategy: 'emptyOnly'恢复旧式参数继承。 - 404 页面:在路由表末尾放置
path: '**'的通配路由指向自定义 404 组件。 - 链接参数数组:掌握
['/path', id, {optional: true}]的数组语法,理解其可表示任意深度嵌套路由,与Router.navigate/createUrlTree共用同一套命令模型。 - 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。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00