Angular 路由行为深度定制指南:RouterConfigOptions、路由复用/预加载策略与自定义匹配器
Angular Router 是 Angular 应用的核心导航基础设施,其默认行为已针对大多数场景做了优化。但当应用需要跨导航保留组件状态、按需决定懒加载时机、与旧系统共享 URL 空间,或依据运行时条件做超出普通路径模式的动态匹配时,就必须借助 Router 提供的高级扩展点。本文以 Angular 官方路由文档为主线,结合本仓库 packages/router 的真实源码,系统梳理 RouterConfigOptions 各项配置、RouteReuseStrategy、预加载策略、URL 边界策略与自定义 matcher 的底层原理与落地写法,读完即可在自己的应用中安全、精确地定制路由行为。
重要前提:动手定制前请先确认默认路由行为确实无法满足需求。Angular 默认路由在性能与简洁性之间取得了最佳平衡,自定义策略会引入额外的代码复杂度,若内存管理不当还会带来性能隐患。
Router 配置选项(RouterConfigOptions)
withRouterConfig(基于 provideRouter 的函数式配置)或 RouterModule.forRoot 的 ExtraOptions 都允许提供额外的 RouterConfigOptions,用于微调 Router 的整体行为。在源码中,这一选项集的类型定义位于 router_config.ts,withRouterConfig 的实现则通过 ROUTER_CONFIGURATION 注入令牌将配置注入 Router(见 provide_router.ts)。下面逐一展开每个选项。
处理被取消的导航(canceledNavigationResolution)
canceledNavigationResolution 控制导航被取消时 Router 如何恢复浏览器历史。默认值是 'replace':导航被取消(例如被路由守卫拒绝)时,用 location.replaceState 把地址栏回退到导航前的 URL。实际操作中,只要地址栏已经为此次导航更新过(典型场景是浏览器前进/后退按钮触发的 popstate 导航),失败或守卫拒绝时这次历史记录就会被"回滚"覆盖。
切换为 'computed' 后,Router 会始终让浏览器的历史索引与 Angular 导航保持同步:取消"后退按钮"触发的导航,就会触发一次"前进"导航(反之亦然),从而回到最初页面。
provideRouter(routes, withRouterConfig({canceledNavigationResolution: 'computed'}));
适合场景:应用使用 urlUpdateStrategy: 'eager',或守卫经常取消由浏览器触发的 popstate 导航时,本选项最有用。
源码注释中还提醒了一个重要限制:'computed' 与"只处理 URL 一部分"的任何 UrlHandlingStrategy 都不兼容,因为历史恢复会导航到浏览器历史中的前一个位置,而不只是重置 URL 的某个片段(见 router_config.ts)。
响应"相同 URL"导航(onSameUrlNavigation)
onSameUrlNavigation 决定当用户导航到当前 URL 时发生什么。默认值 'ignore' 会跳过一切工作;'reload' 则让 Router 把该 URL 重新放入导航管线处理,而不是跳过。守卫与解析器是否重跑由 runGuardsAndResolvers 控制,组件是否复用则由路由复用策略(见下文"路由复用策略")决定。
provideRouter(routes, withRouterConfig({onSameUrlNavigation: 'reload'}));
适合场景:希望反复点击列表筛选、左侧导航项或刷新按钮时,即使 URL 不变也能触发新的数据拉取。
你还可以在单次导航层面控制该行为,而不是全局生效。这样既能保持全局默认 'ignore',又能为特定用例选择性开启重载:
router.navigate(['/some-path'], {onSameUrlNavigation: 'reload'});
从源码看,OnSameUrlNavigation 类型就是 'reload' | 'ignore'(见 models.ts),并且它既可以作为 RouterConfigOptions 的全局默认值,也可以作为单条路由的 Route 配置项(单条路由上的配置优先于全局默认值,见 models.ts)。
控制参数继承(paramsInheritanceStrategy)
paramsInheritanceStrategy 定义父路由的参数与数据如何向子路由流动。默认值 'always' 意味着子路由自动继承父路由的参数、路由 data 与解析值。
provideRouter(routes, withRouterConfig({paramsInheritanceStrategy: 'emptyOnly'}));
设置成 'emptyOnly' 则是保留旧版行为:仅当路由自身是空路径、或父路由没有设置组件时才继承父路由参数。源码层面,参数合并发生在路由识别阶段:recognize.ts 在构建每个快照时调用 getInherited(snapshot, parentRoute, this.paramsInheritanceStrategy),随后把继承结果冻结为快照的 params 与 data(见 recognize.ts)。
考虑下面这条嵌套路由:不同业务领域共用一组上下文标识符(组织 / 项目 / 客户)。
export const routes: Routes = [
{
path: 'org/:orgId',
component: Organization,
children: [
{
path: 'projects/:projectId',
component: Project,
children: [
{
path: 'customers/:customerId',
component: Customer,
},
],
},
],
},
];
URL 形态为 /org/:orgId/projects/:projectId/customers/:customerId。在 'emptyOnly' 下,Customer 组件必须逐级向上手动取值:
@Component({/* ... */})
export class Customer {
private route = inject(ActivatedRoute);
orgId = this.route.parent?.parent?.snapshot.params['orgId'];
projectId = this.route.parent?.snapshot.params['projectId'];
customerId = this.route.snapshot.params['customerId'];
}
而在默认的 'always' 策略下,矩阵参数(matrix parameters)、路由 data 与解析值会顺着路由树一路向下可用,所有祖先参数都能直接从当前路由快照读取:
/org/:orgId/projects/:projectId/customers/:customerId
@Component({/* ... */})
export class Customer {
private route = inject(ActivatedRoute);
// 所有父级参数均可直接读取
orgId = this.route.snapshot.params['orgId'];
projectId = this.route.snapshot.params['projectId'];
customerId = this.route.snapshot.params['customerId'];
}
补充一个矩阵参数细节(源码注释中亦有说明):这里的"父级"指配置树中的父
Route,并不必然是"URL 左侧的段"。当某条Route的path含多个段时,矩阵参数必须出现在最后一段上,例如{path: 'a/b', component: MyComp}对应的矩阵参数应写成a/b;foo=bar,而不是a;foo=bar/b。
决定 URL 何时更新(urlUpdateStrategy)
urlUpdateStrategy 决定 Angular 何时写入浏览器地址栏。默认值 'deferred' 等待导航成功后才会变更 URL;'eager' 则在导航一开始就立即更新地址栏。
provideRouter(routes, withRouterConfig({urlUpdateStrategy: 'eager'}));
'eager' 的好处是:当导航被守卫拦截或因错误失败时,仍然能在地址栏看到"尝试过的 URL",便于分析管线捕获被阻止的路由;代价是如果守卫执行时间较长,地址栏会短暂显示"进行中"的 URL。
选择默认的 query 参数处理(defaultQueryParamsHandling)
defaultQueryParamsHandling 为 Router.createUrlTree 设置兜底行为,适用于调用方没有显式传 queryParamsHandling 的场景。'replace'(默认)会整体替换现有查询串;'merge' 会把传入值与当前查询参数合并;'preserve' 则保留现有查询参数,除非显式传入新值。
provideRouter(routes, withRouterConfig({defaultQueryParamsHandling: 'merge'}));
适合场景:搜索与筛选页面,当新增参数时希望自动保留既有筛选条件。
结合源码可以补充两点机制性说明(见 router_config.ts):createUrlTree 在内部被 Router.navigate 与 RouterLink 使用,但 QueryParamsHandling 对 Router.navigateByUrl 不生效;QueryParamsHandling 的合法值全集为 'merge' | 'preserve' | 'replace' | ''(见 models.ts)。
配置末尾斜杠处理(TrailingSlash / NoTrailingSlash 策略)
默认情况下,Location 服务在读取 URL 时会剥掉末尾斜杠。你可以在应用中提供 TrailingSlashPathLocationStrategy,强制所有写入浏览器地址栏的 URL 都带上末尾斜杠:
import {LocationStrategy, TrailingSlashPathLocationStrategy} from '@angular/common';
bootstrapApplication(App, {
providers: [{provide: LocationStrategy, useClass: TrailingSlashPathLocationStrategy}],
});
也可以提供 NoTrailingSlashPathLocationStrategy,强制所有写入地址栏的 URL 一律不含末尾斜杠:
import {LocationStrategy, NoTrailingSlashPathLocationStrategy} from '@angular/common';
bootstrapApplication(App, {
providers: [{provide: LocationStrategy, useClass: NoTrailingSlashPathLocationStrategy}],
});
这两个策略都只影响写入浏览器的 URL。Location.path() 与 Location.normalize() 在读取 URL 时仍会继续剥离末尾斜杠。源码中 TrailingSlashPathLocationStrategy 正是通过重写 prepareExternalUrl 方法、在路径末尾补齐 / 后交由父类完成写入实现的(见 location_strategy.ts)。
到此,Angular Router 主要提供了四大可定制区域:路由复用策略、预加载策略、URL 处理策略与自定义路由匹配器。下文逐一深入。
路由复用策略(Route reuse strategy)
路由复用策略决定导航期间 Angular 是销毁并重建组件,还是把组件保留下来以供复用。默认情况下,Angular 在离开某条路由时会销毁组件实例,返回时再创建新实例——这正是默认类 DefaultRouteReuseStrategy(继承自 BaseRouteReuseStrategy)的行为:它只在路由配置对象完全相同时复用当前组件,不存储任何路由供将来复用(见 route_reuse_strategy.ts)。
何时需要自定义路由复用
符合以下需求的应用值得实现自定义复用策略:
- 表单状态保留:用户离开再返回时,保留尚未填完的表单;
- 昂贵数据保留:避免重新抓取大数据集或重做复杂计算;
- 滚动位置维护:长列表或无限滚动场景中保持滚动位置;
- 类标签页界面:在标签页之间切换时维持组件状态。
编写自定义路由复用策略
Angular 的 RouteReuseStrategy 抽象类通过"脱离的路由句柄(detached route handle)"这一概念让你定制导航行为(类定义见 route_reuse_strategy.ts)。
所谓"脱离的路由句柄",是 Angular 保存组件实例及其整个视图层级的方式。当一条路由被"脱离"时,Angular 会把组件实例、其子组件及全部关联状态保留在内存中,导航回来时这份被保留的状态可以再次"接回"。
RouteReuseStrategy 提供下列方法,共同控制路由组件的生命周期:
| 方法 | 说明 |
|---|---|
shouldDetach |
决定导航离开时,路由是否应被存储以备后续复用 |
store |
当 shouldDetach 返回 true 时,存储脱离的路由句柄 |
shouldAttach |
决定导航到某路由时,是否应接回已存储的路由 |
retrieve |
返回此前存储、用于接回的路由句柄 |
shouldReuseRoute |
决定导航期间 Router 是复用当前路由实例,还是将其销毁 |
shouldDestroyInjector |
(实验性)决定不再存储某条脱离路由时,Router 是否销毁该路由的注入器 |
下面的示例演示了一个基于路由元数据选择性保留组件状态的自定义复用策略:
import {
RouteReuseStrategy,
Route,
ActivatedRouteSnapshot,
DetachedRouteHandle,
} from '@angular/router';
import {Injectable} from '@angular/core';
@Injectable()
export class CustomRouteReuseStrategy implements RouteReuseStrategy {
private handlers = new Map<Route | null, DetachedRouteHandle>();
shouldDetach(route: ActivatedRouteSnapshot): boolean {
// 决定是否应存储路由以备复用
return route.data['reuse'] === true;
}
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void {
// 当 shouldDetach 返回 true 时存储脱离的路由句柄
if (handle && route.data['reuse'] === true) {
const key = this.getRouteKey(route);
this.handlers.set(key, handle);
}
}
shouldAttach(route: ActivatedRouteSnapshot): boolean {
// 检查是否有存储的路由需要接回
const key = this.getRouteKey(route);
return route.data['reuse'] === true && this.handlers.has(key);
}
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
// 返回用于接回的存储句柄
const key = this.getRouteKey(route);
return route.data['reuse'] === true ? (this.handlers.get(key) ?? null) : null;
}
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
// 决定 Router 是否复用当前路由实例
return future.routeConfig === curr.routeConfig;
}
private getRouteKey(route: ActivatedRouteSnapshot): Route | null {
return route.routeConfig;
}
}
手动销毁脱离的路由句柄
实现自定义 RouteReuseStrategy 时,如果你决定丢弃某个 DetachedRouteHandle 而不再接回它,就必须手动销毁它。例如策略设有缓存大小上限或句柄过期时间时,必须确保组件及其状态被正确销毁以避免内存泄漏。
由于 DetachedRouteHandle 是不透明类型,你无法直接对它调用销毁方法,应改用 Router 提供的 destroyDetachedRouteHandle 函数(其定义见 route_reuse_strategy.ts,实现中会销毁 componentRef,同时销毁随 ActivatedRoute 被保留的 _localInjector——这正是防止内存泄漏的关键):
import {destroyDetachedRouteHandle} from '@angular/router';
// ... 在策略内部
if (this.handles.size > MAX_CACHE_SIZE) {
const handle = this.handles.get(oldestKey);
if (handle) {
destroyDetachedRouteHandle(handle);
this.handles.delete(oldestKey);
}
}
注意:当涉及
canMatch守卫时,避免使用路由路径(route path)作为缓存键,否则可能产生重复条目。
(实验性)未使用路由注入器的自动清理
默认情况下,Angular 不会销毁脱离路由的注入器,即使它们已不再被 RouteReuseStrategy 存储。这主要是因为在大多数应用里这种级别的内存管理并不必要。
要启用未使用路由注入器的自动清理,可在路由配置中使用 withExperimentalAutoCleanupInjectors 特性(源码见 provide_router.ts)。该特性在每次导航后检查当前被策略存储的路由,并销毁那些已被策略脱离但当前并未被复用策略存储的路由的注入器:
import {provideRouter, withExperimentalAutoCleanupInjectors} from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes, withExperimentalAutoCleanupInjectors())],
};
如果你没有提供自定义 RouteReuseStrategy,或自定义策略继承自 BaseRouteReuseStrategy,那么启用本特性后,路由在变为非活动状态时其注入器即会被销毁。
与自定义 RouteReuseStrategy 组合使用时的清理规则
如果你的应用使用了自定义 RouteReuseStrategy,且该策略没有继承 BaseRouteReuseStrategy,则必须实现 shouldDestroyInjector 来告诉 Router 哪些路由应销毁注入器:
@Injectable()
export class CustomRouteReuseStrategy implements RouteReuseStrategy {
// ... 其他方法
shouldDestroyInjector(route: Route): boolean {
return !route.data['retainInjector'];
}
}
如果策略曾存储过 DetachedRouteHandle,还需要通过 retrieveStoredRouteHandles 告诉 Router 这些句柄的存在,避免销毁该脱离句柄所依赖的注入器:
@Injectable()
export class CustomRouteReuseStrategy implements RouteReuseStrategy {
private readonly handles = new Map<Route, DetachedRouteHandle>();
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null) {
this.handles.set(route.routeConfig!, handle);
}
retrieveStoredRouteHandles(): DetachedRouteHandle {
return Array.from(this.handles.values());
}
// ... 其他方法
}
补充说明:在默认/
BaseRouteReuseStrategy语义中,shouldDestroyInjector对任何路由都返回true(见 route_reuse_strategy.ts),这就是"未提供自定义策略或继承 Base 时注入器随路由非活动即被销毁"的代码来源。
配置路由使用自定义复用策略
路由可通过路由配置元数据"选择加入"复用行为。这种做法把复用逻辑与组件代码分离,不修改组件即可调整行为:
export const routes: Routes = [
{
path: 'products',
component: ProductList,
data: {reuse: true}, // 组件状态在导航间保持
},
{
path: 'products/:id',
component: ProductDetail,
// 无 reuse 标记 —— 每次导航都会重建组件
},
{
path: 'search',
component: Search,
data: {reuse: true}, // 保留搜索结果与筛选状态
},
];
你也可以通过 Angular 的依赖注入体系在应用级配置自定义复用策略。此时 Angular 会创建该策略的单一实例,由它管理整个应用的所有路由复用决策:
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
{provide: RouteReuseStrategy, useClass: CustomRouteReuseStrategy},
],
};
RouteReuseStrategy 在源码中是一个默认注入 DefaultRouteReuseStrategy 的可注入抽象类,因此直接替换其提供者即可整体接管复用行为(见 route_reuse_strategy.ts)。
预加载策略(Preloading strategy)
预加载策略决定 Angular 何时在后台加载懒加载路由模块。懒加载通过延迟模块下载改善了首屏加载时间,但用户首次导航到懒加载路由时仍会感到延迟;预加载策略通过在用户请求之前先加载模块来消除这段延迟。
内置预加载策略
Angular 开箱即用地提供两种预加载策略(实现见 router_preloader.ts):
| 策略 | 说明 |
|---|---|
NoPreloading |
默认策略,禁用一切预加载。换句话说,模块只在用户导航到它们时才加载 |
PreloadAllModules |
初次导航完成后立即加载所有懒加载模块 |
PreloadAllModules 的配置方式如下:
import {ApplicationConfig} from '@angular/core';
import {provideRouter, withPreloading, PreloadAllModules} from '@angular/router';
import {routes} from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes, withPreloading(PreloadAllModules))],
};
PreloadAllModules 适用于中小型应用——把所有模块下载完不会显著影响性能。但对于拥有大量功能模块的大型应用,更具选择性的预加载往往更合适。
机制细节:源码中
withPreloading(strategy)会注册RouterPreloader并在PreloadingStrategy令牌上提供所选策略;RouterPreloader监听NavigationEnd事件,在每次导航结束(初始导航结束即可触发)时调用策略的preload(见 router_preloader.ts 与 provide_router.ts)。PreloadAllModules的实现会对fn()的执行结果附加catchError,即使个别模块加载失败也不会中断整个预加载流程。
编写自定义预加载策略
自定义预加载策略实现 PreloadingStrategy 接口(见 router_preloader.ts),该接口要求实现唯一的 preload 方法。方法接收路由配置和一个真正触发模块加载的函数;策略返回一个在预加载完成时发值的 Observable,或返回空 Observable 表示跳过预加载:
import {Injectable} from '@angular/core';
import {PreloadingStrategy, Route} from '@angular/router';
import {Observable, of, timer} from 'rxjs';
import {mergeMap} from 'rxjs/operators';
@Injectable()
export class SelectivePreloadingStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
// 只预加载标记了 data: { preload: true } 的路由
if (route.data?.['preload']) {
return load();
}
return of(null);
}
}
这个选择性策略通过路由元数据判断是否预加载。路由可以在配置中选择加入预加载:
import {Routes} from '@angular/router';
export const routes: Routes = [
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.routes'),
data: {preload: true}, // 初始导航后立即预加载
},
{
path: 'reports',
loadChildren: () => import('./reports/reports.routes'),
data: {preload: false}, // 仅当用户导航到 reports 时才加载
},
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes'),
// 无 preload 标记 —— 不会被预加载
},
];
预加载的性能考量
预加载同时影响网络使用与内存占用。每个被预加载的模块都会消耗带宽、增大应用内存足迹。按流量计费的移动端用户可能更青睐最小化预加载,而处于快速网络下的桌面用户则能承受更激进的预加载策略。
预加载的时机同样重要。初始加载后立刻预加载,可能与图片或 API 调用等关键资源争抢带宽。策略应结合应用加载后的实际行为,与后台任务协调以避免性能劣化。
浏览器资源限制也会影响预加载。浏览器对并发 HTTP 连接数有限制,激进的预加载可能排在其它请求之后排队。Service Worker 可以通过对缓存与网络请求提供细粒度控制来补足预加载策略的短板。
URL 处理策略(URL handling strategy)
URL 处理策略划定"哪些 URL 由 Angular Router 处理、哪些被忽略"的边界。默认情况下 Angular 试图处理应用内一切导航事件,但真实应用常常需要与其他系统共存、处理外部链接,或与自行管理路由的遗留系统集成。
UrlHandlingStrategy 抽象类让你控制 Angular 管理 URL 与外部 URL 之间的分界(接口见 url_handling_strategy.ts,默认实现 DefaultUrlHandlingStrategy 对所有 URL 均返回"应处理",见同文件 L50-L60)。这在把应用增量迁移到 Angular、或 Angular 应用需与其他框架共享 URL 空间时至关重要。
实现自定义 URL 处理策略
自定义策略继承 UrlHandlingStrategy 并实现三个方法:shouldProcessUrl 决定 Angular 是否应处理给定 URL;extract 返回 Angular 应当处理的那部分 URL;merge 把处理后的 URL 片段与 URL 其余部分重新组合:
import {Injectable} from '@angular/core';
import {UrlHandlingStrategy, UrlTree} from '@angular/router';
@Injectable()
export class CustomUrlHandlingStrategy implements UrlHandlingStrategy {
shouldProcessUrl(url: UrlTree): boolean {
// 只处理以 /app 或 /admin 开头的 URL
return url.toString().startsWith('/app') || url.toString().startsWith('/admin');
}
extract(url: UrlTree): UrlTree {
// 若应处理则原样返回 URL
return url;
}
merge(newUrlPart: UrlTree, rawUrl: UrlTree): UrlTree {
// 组合 URL 片段与 URL 其余部分
return newUrlPart;
}
}
该策略在 URL 空间上划出了清晰的边界:Angular 处理 /app 与 /admin 路径,忽略其余一切。这一模式很适合迁移遗留应用:Angular 控制特定区块,遗留系统继续维护其他区块。
源码提示:当
shouldProcessUrl返回false时,Router 会把路由状态置为空状态,结果是所有活动组件都被销毁(见 url_handling_strategy.ts)。设计"忽略"边界时需留意这一点。
配置自定义 URL 处理策略
通过 Angular 依赖注入系统注册自定义策略:
import {ApplicationConfig} from '@angular/core';
import {provideRouter} from '@angular/router';
import {UrlHandlingStrategy} from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
{provide: UrlHandlingStrategy, useClass: CustomUrlHandlingStrategy},
],
};
自定义路由匹配器(Custom route matchers)
默认情况下,Angular Router 按路由定义顺序遍历,把 URL 路径与每条路由的 path 模式逐一比对。它支持静态段、参数段(:id)与通配符(**)。第一个匹配成功的路由获胜,Router 随即停止搜索。
当应用需要基于运行时条件、复杂 URL 模式或其他自定义规则做更复杂的匹配时,自定义 matcher 提供了灵活性,同时不牺牲标准路由的简洁性。
Router 在路由匹配阶段、进行 path 匹配之前评估自定义 matcher。matcher 返回成功匹配时,还能从 URL 中提取参数,使其像标准路由参数一样对激活组件可用。
编写自定义 matcher
自定义 matcher 是一个函数:接收 URL 段,返回包含"已消费段(consumed)与参数(posParams)"的匹配结果,或不返回任何值(null)表示不匹配。matcher 函数在 Angular 评估路由的 path 属性之前执行:
import {Route, UrlSegment, UrlSegmentGroup, UrlMatchResult} from '@angular/router';
export function customMatcher(
segments: UrlSegment[],
group: UrlSegmentGroup,
route: Route,
): UrlMatchResult | null {
// 此处编写匹配逻辑
if (matchSuccessful) {
return {
consumed: segments,
posParams: {
paramName: new UrlSegment('paramValue', {}),
},
};
}
return null;
}
实战:基于版本号的动态路由
设想一个需要按 URL 中版本号路由的 API 文档站。不同版本可能具有不同的组件结构或功能集合:
import {Routes, UrlSegment, UrlMatchResult} from '@angular/router';
export function versionMatcher(segments: UrlSegment[]): UrlMatchResult | null {
// 匹配形如 /v1/docs、/v2.1/docs、/v3.0.1/docs 的模式
if (segments.length >= 2 && segments[0].path.match(/^v\d+(\.\d+)*$/)) {
return {
consumed: segments.slice(0, 2), // 消费版本段与 'docs' 段
posParams: {
version: segments[0], // 把版本作为参数暴露出来
section: segments[1], // section 同样作为参数暴露
},
};
}
return null;
}
// 路由配置
export const routes: Routes = [
{
matcher: versionMatcher,
component: Documentation,
},
{
path: 'latest/docs',
redirectTo: 'v3/docs',
},
];
组件通过路由输入接收提取出的参数——matcher 提取的 posParams 名称会自动与组件中的路由输入绑定,配合 Angular 信号化 resource 可以做到参数变化时自动重新加载文档:
import {Component, input, inject} from '@angular/core';
import {resource} from '@angular/core';
@Component({
selector: 'app-documentation',
template: `
@if (documentation.isLoading()) {
<div>Loading documentation...</div>
} @else if (documentation.error()) {
<div>Error loading documentation</div>
} @else if (documentation.value(); as docs) {
<article>{{ docs.content }}</article>
}
`,
})
export class Documentation {
// 路由参数自动绑定到信号输入
version = input.required<string>(); // 接收 version 参数
section = input.required<string>(); // 接收 section 参数
private docsService = inject(DocumentationService);
// version 或 section 变化时 resource 自动加载文档
documentation = resource({
params: () => {
if (!this.version() || !this.section()) return;
return {
version: this.version(),
section: this.section(),
};
},
loader: ({params}) => {
return this.docsService.loadDocumentation(params.version, params.section);
},
});
}
实战:支持本地化的语言感知路由
国际化应用常把地区码编码进 URL。自定义 matcher 可以提取地区码并路由到合适组件,同时把地区码作为参数暴露出来:
// 支持的语言
const locales = ['en', 'es', 'fr', 'de', 'ja', 'zh'];
export function localeMatcher(segments: UrlSegment[]): UrlMatchResult | null {
if (segments.length > 0) {
const potentialLocale = segments[0].path;
if (locales.includes(potentialLocale)) {
// 这是语言前缀,消费它并继续匹配
return {
consumed: [segments[0]],
posParams: {
locale: segments[0],
},
};
} else {
// 无语言前缀,使用默认语言
return {
consumed: [], // 不消费任何段
posParams: {
locale: new UrlSegment('en', {}),
},
};
}
}
return null;
}
实战:复杂业务规则的匹配
自定义 matcher 擅长实现用路径模式难以表达的业务规则。设想一个电商站点,其商品 URL 因商品类型不同而遵循不同模式:
export function productMatcher(segments: UrlSegment[]): UrlMatchResult | null {
if (segments.length === 0) return null;
const firstSegment = segments[0].path;
// 图书:/isbn-1234567890
if (firstSegment.startsWith('isbn-')) {
return {
consumed: [segments[0]],
posParams: {
productType: new UrlSegment('book', {}),
identifier: new UrlSegment(firstSegment.substring(5), {}),
},
};
}
// 电子产品:/sku/ABC123
if (firstSegment === 'sku' && segments.length > 1) {
return {
consumed: segments.slice(0, 2),
posParams: {
productType: new UrlSegment('electronics', {}),
identifier: segments[1],
},
};
}
// 服饰:/style/BRAND/ITEM
if (firstSegment === 'style' && segments.length > 2) {
return {
consumed: segments.slice(0, 3),
posParams: {
productType: new UrlSegment('clothing', {}),
brand: segments[1],
identifier: segments[2],
},
};
}
return null;
}
自定义 matcher 的性能考量
自定义 matcher 在找到匹配前,会为每一次导航尝试运行。因此,复杂匹配逻辑会拖慢导航,尤其是路由数量众多的应用。请让 matcher 保持聚焦且高效:
- 在不可能匹配时尽早返回;
- 避免昂贵的操作,例如 API 调用或过于复杂的正则;
- 对重复出现的 URL 模式考虑缓存结果。
自定义 matcher 能优雅地解决复杂路由需求,但过度使用会让路由配置难以理解与维护。请只在标准路径匹配确实力不能及的场景中使用它。
小结:如何选择定制手段
| 诉求 | 推荐扩展点 | 关键 API / 配置 |
|---|---|---|
| 取消导航时的历史恢复方式 | RouterConfigOptions |
canceledNavigationResolution |
| 同一 URL 重复导航触发重载 | RouterConfigOptions / NavigationBehaviorOptions |
onSameUrlNavigation |
| 让子路由直接读取父级参数 | RouterConfigOptions |
paramsInheritanceStrategy |
| 提前或推迟写入地址栏 | RouterConfigOptions |
urlUpdateStrategy |
| 统一 query 参数的默认处理 | RouterConfigOptions |
defaultQueryParamsHandling |
| 强制/禁止末尾斜杠 | 替换 LocationStrategy |
TrailingSlashPathLocationStrategy、NoTrailingSlashPathLocationStrategy |
| 跨导航保留组件状态 | RouteReuseStrategy |
自定义策略 + destroyDetachedRouteHandle |
| 后台预取懒加载模块 | PreloadingStrategy |
PreloadAllModules 或自定义实现 |
| 划分 Angular 与外部 URL 边界 | UrlHandlingStrategy |
shouldProcessUrl / extract / merge |
| 超越路径模式的动态匹配 | 自定义 matcher | matcher 路由字段 + UrlMatchResult |
所有上述扩展点都是依赖注入体系中的"可替换抽象",它们的默认实现与完整签名均可直接在仓库源码中查阅:RouterConfigOptions 见 router_config.ts,路由复用相关类型与函数见 route_reuse_strategy.ts,预加载策略见 router_preloader.ts,URL 边界策略见 url_handling_strategy.ts,匹配器与 URL 段类型定义见 Router 模块的公共导出。写作生产代码前,先在应用内以最小示例验证自定义策略的取舍,再逐步推广,是让定制路由保持健壮与可维护的最佳路径。
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