简短回答
使用 UrlMatcher
无法直接实现,但是在 Angular 14.1 中引入了 CanMatch
路由守卫,可以用于此目的。它允许您控制是否应使用路由,即使其 path
或 matcher
匹配,也可以跳过该路由并匹配其他路由。它支持依赖注入并且可以是异步的(返回 Promise
或 Observable
)。它还可以返回一个 UrlTree
,以取消导航并重定向到另一个路由。
或者,对于异步操作,您可以选择以下方法之一:
使用像 CanActivateChild
这样的守卫 在需要时执行异步操作。然后,您可以通过重定向(通过返回一个 UrlTree
来取消导航)让路由器再次匹配路由,这次在任何 UrlMatcher
中都可以同步使用更新后的数据。
通过在组件中动态加载组件,将一些路由处理放在 Angular 路由器之外。
至于 UrlMatcher
中的依赖项,您可以将数据本身保存在全局变量中,或者捕获注入器并将其保存为全局变量,然后在 UrlMatcher
中使用它来解析服务。无论哪种方式,数据访问都将是同步的。
有关更多详细信息和代码示例,请参见下文。
背景
多年来,DI-capable的UrlMatcher和路由匹配阶段的异步性一直是Angular社区讨论的热点话题(甚至可以追溯到Angular 2.0),其中最著名的问题在Angular的GitHub上提出:
根据异步条件在路由中加载组件(#12088)和
将UrlMatcher作为服务(#17145)。这些问题以及其他问题都在
CanMatch
pull request之后被标记为已解决。
之前存在的路由守卫(例如CanActivate
、CanLoad
和Resolve
)仅在选择/识别路由后(包括子路由在内的整个路由)运行,因此不适合根据来自服务的某些数据决定导航的位置,至少不能直接进行(无需重定向)。此外,它们无法解决UrlMatcher
中的DI问题,因此您需要采用将数据保存在全局变量中并异步更新该变量,或者捕获注入器的方法。
CanMatch
守卫(Angular 14.1+)
尽管UrlMatcher
仍然无法返回Promise或Observable,但CanMatch
可用于此目的。
CanMatch
在使用 path
或 matcher
匹配路由之后,但在其被认为是已识别的并且其他守卫运行之前进行保护。如果对于给定路由(从根路由到最内部子路由),所有这些守卫都解析为 true
,则该路由将被识别,并调用其他类型的守卫。如果 CanMatch
守卫解析为 false
,则将跳过该路由,并尝试匹配下一个路由。它还可以解析为 UrlTree
以进行导航取消和重定向。
在撰写本文时,CanMatch
守卫会在每次导航时被多次调用,就像 UrlMatcher
一样(UrlMatcher is called twice (#26081))。这只是考虑在重复的后续请求中跨异步操作重用结果并管理一些缓存的另一个原因。
使用CanMatch
为相同URL提供不同路由的示例:
@Injectable()
class CanMatchAdmin implements CanMatch {
constructor(private auth: AuthService) {}
canMatch(route: Route, segments: UrlSegment[]): Observable<boolean> {
return this.auth.user$.pipe(
map(user => user.isAdmin)
);
}
}
const routes = [
{
path: '',
component: AdminDashboardComponent
canMatch: [CanMatchAdmin]
},
{
path: '',
component: UserDashboardComponent,
canMatch: [CanMatchLoggedIn]
},
{
path: '',
pathMatch: 'full',
redirectTo: 'login'
},
];
特定问题用例的示例:
@Injectable({ providedIn: 'root' })
class CanMatchWpApi implements CanMatch {
constructor(private wp: WpApiService) {}
async canMatch(route: Route, segments: UrlSegment[]): Promise<boolean> {
const data = await this.wp.getByUrl(segments.join('/'));
return data.contentType === route.data.wpContentType;
}
}
const anyUrlMatcher: UrlMatcher = (segments) => ({ consumed: segments });
const routes: Routes = [
{
path: '',
children: [
{
path: ':categorySlug/:postSlug',
component: PostComponent,
canMatch: [CanMatchWpApi],
data: { wpContentType: 'post' },
},
{
path: ':parentPageSlug/:pageSlug',
component: PageComponent,
canMatch: [CanMatchWpApi],
data: { wpContentType: 'page' },
},
{
matcher: anyUrlMatcher,
component: SomeFunkyComponent,
canMatch: [CanMatchWpApi],
data: { wpContentType: 'some_funky_content_type' },
},
],
},
];
替代方案
结合其他守卫(特别是 CanActivate
或 CanActivateChild
)和重定向。请记住,为了运行这些守卫,路由需要首先匹配。这一点可能一开始看起来不直观,因此请考虑所有竞争的路由都将具有该守卫,并且当检测到用于匹配当前路由的数据不正确时,它将进行重定向。重定向将导致重新运行路由匹配,更新后的 URL 数据现在可以同步地在路由匹配器中使用。问题中的示例可以大致实现如下:
let wpData = null;
let wpDataUrl = null;
@Injectable({ providedIn: 'root' })
class CanActivateChildWpApi implements CanActivateChild {
constructor(private router: Router, private wp: WpApiService) {}
async canActivateChild(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Promise<boolean> {
const url = state.url;
if (url === wpDataUrl) {
return true;
}
const navId = this.router.getCurrentNavigation().id;
let newWpData = await this.wp.getByUrl(url);
if (navId !== this.router.getCurrentNavigation()?.id) {
return false;
}
wpData = newWpData;
wpDataUrl = url;
return this.router.parseUrl(url);
await this.router.navigateByUrl('/loading', { skipLocationChange: true });
await this.router.navigateByUrl(url);
return false;
}
}
const wpRouteMatcher: UrlMatcher = (
segments: UrlSegment[],
group: UrlSegmentGroup,
route: Route
) => {
return (wpData == null
|| wpData.contentType === (route.data as any).wpContentType)
? { consumed: segments }
: null;
};
const routes: Routes = [
{
path: '',
canActivateChild: [CanActivateChildWpApi],
children: [
{
matcher: wpRouteMatcher,
component: PostComponent,
data: { wpContentType: 'post' },
},
{
matcher: wpRouteMatcher,
component: PageComponent,
data: { wpContentType: 'page' },
},
]
},
];
使用标准路由内部的自定义逻辑来动态加载组件。
将模块注入器公开为一个变量,通过在引导应用程序时捕获它,如前面的答案所示。这将使直接使用 Angular 的 DI 解析依赖项成为可能,但仍然无法异步匹配路由。
import { Injector } from '@angular/core';
export let appInjector: Injector = null;
export function setAppInjector(newAppInjector: Injector) {
appInjector = newAppInjector;
}