Angular 6+中的依赖注入和UrlMatcher中的异步

7
我该如何为路由的UrlMatcher注入依赖项并执行异步路由匹配?
我需要调用后端API,以便通过解析重写规则和在WordPress中运行查询来找到每个URL的正确路由。
因此我需要一个单例服务来为UrlMatcher获取数据并使用它来确定路由(然后将其注入到带有已获取数据的组件中)。
我创建了一个UrlMatcher工厂:
      {
        component: PostComponent,
        matcher: wpApiUrlMatcherFactory('post')
      }

但我不知道如何将相同的服务注入到所有匹配器中,以及如何使其正常工作,因为该服务是异步的,而UrlMatcher无法返回一个PromiseObservable


你有没有找到解决办法? - mait.taim
@mait.taim 不,我最终使用了一个单一的顶级组件,并动态创建组件,而没有使用内置路由器。 - orelby
2个回答

2

1.In main.ts:

export let appInjector: Injector;


platformBrowserDynamic().bootstrapModule(AppModule)
      .then(m => appInjector = m.injector)

2. 在你的路由模块中:

import {appInjector} from '../../main';

将"Original Answer"翻译成"最初的回答"。
const routes: Routes = [
  {
     matcher: (segments: UrlSegment[]): UrlMatchResult => {
       if (appInjector.get(AccessService).hasAccess('test')) {
         return {consumed: [segment]};
       }
   },
   component: TestComponent,
];

这不是循环依赖吗? - Edward
不,我成功地使用它了。 - Антон Ефанов
收到循环依赖警告:客户端:135 检测到循环依赖: src/app/app.module.ts -> src/app/app.router.ts -> src/main.ts -> src/app/app.module.ts - Anos K. Mhazo
我在应用程序模块或应用程序路由模块中没有使用来自主文件的导入。在您的情况下,请尝试将应用注入器保存在不同的文件中。 - Антон Ефанов

0

简短回答

使用 UrlMatcher 无法直接实现,但是在 Angular 14.1 中引入了 CanMatch 路由守卫,可以用于此目的。它允许您控制是否应使用路由,即使其 pathmatcher 匹配,也可以跳过该路由并匹配其他路由。它支持依赖注入并且可以是异步的(返回 PromiseObservable)。它还可以返回一个 UrlTree,以取消导航并重定向到另一个路由。

或者,对于异步操作,您可以选择以下方法之一:

  1. 使用像 CanActivateChild 这样的守卫 在需要时执行异步操作。然后,您可以通过重定向(通过返回一个 UrlTree 来取消导航)让路由器再次匹配路由,这次在任何 UrlMatcher 中都可以同步使用更新后的数据。

  2. 通过在组件中动态加载组件,将一些路由处理放在 Angular 路由器之外。

至于 UrlMatcher 中的依赖项,您可以将数据本身保存在全局变量中,或者捕获注入器并将其保存为全局变量,然后在 UrlMatcher 中使用它来解析服务。无论哪种方式,数据访问都将是同步的。

有关更多详细信息和代码示例,请参见下文。

背景

多年来,DI-capable的UrlMatcher和路由匹配阶段的异步性一直是Angular社区讨论的热点话题(甚至可以追溯到Angular 2.0),其中最著名的问题在Angular的GitHub上提出:根据异步条件在路由中加载组件(#12088)将UrlMatcher作为服务(#17145)。这些问题以及其他问题都在CanMatch pull request之后被标记为已解决。

之前存在的路由守卫(例如CanActivateCanLoadResolve)仅在选择/识别路由后(包括子路由在内的整个路由)运行,因此不适合根据来自服务的某些数据决定导航的位置,至少不能直接进行(无需重定向)。此外,它们无法解决UrlMatcher中的DI问题,因此您需要采用将数据保存在全局变量中并异步更新该变量,或者捕获注入器的方法。

CanMatch守卫(Angular 14.1+)

尽管UrlMatcher仍然无法返回Promise或Observable,但CanMatch可用于此目的。

CanMatch 在使用 pathmatcher 匹配路由之后,但在其被认为是已识别的并且其他守卫运行之前进行保护。如果对于给定路由(从根路由到最内部子路由),所有这些守卫都解析为 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> {
        // 可能需要等待API调用
        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;
      }
    }
    
    // 通配符路由(**)似乎不允许后续路由
    // 因此我们将使用一个UrlMatcher来消耗整个路径
    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' },
          },
          // ...
        ],
      },
      // ...
    ];
    
  • 替代方案

    1. 结合其他守卫(特别是 CanActivateCanActivateChild)和重定向。请记住,为了运行这些守卫,路由需要首先匹配。这一点可能一开始看起来不直观,因此请考虑所有竞争的路由都将具有该守卫,并且当检测到用于匹配当前路由的数据不正确时,它将进行重定向。重定向将导致重新运行路由匹配,更新后的 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;
          }
      
          // 否则获取正确的数据以匹配路由
          // (利用 UrlMatcher 没有的异步性)
      
          const navId = this.router.getCurrentNavigation().id; // Angular 7.2+
          let newWpData = await this.wp.getByUrl(url);
      
          // 如果此导航已过时,则中止
          if (navId !== this.router.getCurrentNavigation()?.id) {
            return false;
          }
      
          // 更新路由重新匹配的数据
          wpData = newWpData;
          wpDataUrl = url;
      
          // 重新匹配路由的首选选项:
          return this.router.parseUrl(url); // Angular 7.1+
      
          // Hacky 选项:
          // 首先重定向到不同的路由
          await this.router.navigateByUrl('/loading', { skipLocationChange: true });
          // 现在路由器将重新匹配原始 URL 的路由
          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: 'loading',
        //  component: LoadingComponent
        // },
        // ...
        {
          path: '',
          canActivateChild: [CanActivateChildWpApi],
          children: [
            {
              matcher: wpRouteMatcher,
              component: PostComponent,
              data: { wpContentType: 'post' },
            },
            {
              matcher: wpRouteMatcher,
              component: PageComponent,
              data: { wpContentType: 'page' },
            },
            // ...
          ]
        },
      ];
      
    2. 使用标准路由内部的自定义逻辑来动态加载组件

    3. 将模块注入器公开为一个变量,通过在引导应用程序时捕获它,如前面的答案所示。这将使直接使用 Angular 的 DI 解析依赖项成为可能,但仍然无法异步匹配路由

      // app-injector.ts 或其他文件
      
      import { Injector } from '@angular/core';
      
      export let appInjector: Injector = null;
      
      export function setAppInjector(newAppInjector: Injector) {
        appInjector = newAppInjector;
      }
      
      // main.ts<br><div class="h-2"></div>import { setAppInjector } from

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接