UrlMatcher 中的 Angular 6 依赖注入

Or'*_*'el 6 routing dependency-injection single-page-application wp-api angular

如何为路由的 注入依赖项UrlMatcher

我需要调用后端 API,以便为每个 URL 找出正确的路由(通过解析重写规则并在 WordPress 中运行查询)。

这就是为什么我需要一个 Singleton 服务UrlMatcher来获取数据一次,然后使用它来确定路由(然后将它注入到具有获取数据的组件中)。

我创建了一个 UrlMatcher 工厂:

      {
        component: PostComponent,
        matcher: wpApiUrlMatcherFactory('post')
      }
Run Code Online (Sandbox Code Playgroud)

但我不知道如何在所有这些服务中使用相同的服务,以及如何在不使用不良实践代码的情况下创建它。

Or'*_*'el 0

简答

直接使用 是不可能的UrlMatcher,但在 Angular 14.1 中CanMatch为此目的引入了路由防护它可以让您控制是否应该使用某个路由,即使它pathmatcher匹配,从而允许跳过该路由并匹配其他路由。它支持依赖注入并且可以是异步的(返回 aPromise或 an Observable)。它还可以返回 aUrlTree以取消导航并重定向到另一条路线。

或者,对于异步性,您可以:

  1. 在需要时使用守卫CanActivateChild来执行异步操作。然后,您可以重定向(通过返回 a 取消导航UrlTree)并让路由器再次匹配路线,这次更新的数据在any中同步可用,UrlMatcher如下所述。

  2. 通过在组件内动态加载组件来处理 Angular 路由器之外的一些路由。

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

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

背景

路由匹配阶段的DI 功能UrlMatcher和异步性已被要求和讨论多年(甚至早在 Angular 2.0 中),最引人注目的是 Angular GitHub 上的以下问题:根据异步条件在路由中加载组件 (#12088 )UrlMatcher 作为服务(#17145)在拉CanMatch取请求之后,这些和其他都被标记为已解决。

先前存在的路由器防护(例如CanActivateCanLoad和)仅在选择/识别路由(整体,包括子路由)Resolve运行,因此不适合根据来自服务的某些数据来决定导航到哪里,至少不直接(不重定向)。此外,它们没有解决 DI 问题,因此您需要将数据保存在异步更新的全局变量中,或者捕获注入器。UrlMatcher

CanMatch防护罩(角度 14.1+)

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

CanMatch守卫在使用path或匹配路线后运行matcher,但在该路线被视为已识别之前且在其他守卫运行之前运行。如果所有这样的守卫都解析为true给定的路由(从根路由到最里面的子路由),它将被识别,并且其他类型的守卫将被调用。如果CanMatch守卫解析到false该路由将被跳过,并且路由器将尝试匹配下一个路由。它还可以解析UrlTree为导航取消和重定向。

在撰写本文时,CanMatch每个导航都会多次调用守卫,例如UrlMatcherUrlMatcher 被调用两次 (#26081))。这只是考虑在重复的后续请求中重用异步操作的结果并可能管理一些缓存的另一个原因。

  1. 使用以下命令为同一 URL 提供不同路由的示例CanMatch

    @Injectable()
    class CanMatchAdmin implements CanMatch {
      constructor(private auth: AuthService) {}
    
      canMatch(route: Route, segments: UrlSegment[]): Observable<boolean> {
        // might wait for an API call
        return this.auth.user$.pipe(
          map(user => user.isAdmin)
        );
      }
    }
    
    Run Code Online (Sandbox Code Playgroud)
    const routes = [
      {
        path: '',
        component: AdminDashboardComponent
        canMatch: [CanMatchAdmin]
      },
      {
        path: '',
        component: UserDashboardComponent,
        canMatch: [CanMatchLoggedIn]
      },
      {
        path: '',
        pathMatch: 'full',
        redirectTo: 'login'
      },
      // ...
    ];
    
    Run Code Online (Sandbox Code Playgroud)
  2. 特定问题用例的示例:

    @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;
      }
    }
    
    Run Code Online (Sandbox Code Playgroud)
    // Wildcard routes (**) do not seem to allow subsequent routes
    // so we'll use a UrlMatcher that consumes the entire path
    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' },
          },
          // ...
        ],
      },
      // ...
    ];
    
    Run Code Online (Sandbox Code Playgroud)

备择方案

  1. CanActivate其他防护(特别是 或)和重定向的组合CanActivateChild。请记住,为了让这些守卫运行,首先需要匹配路线。乍一看这似乎不直观,因此请考虑所有竞争路由都将具有该保护,并且当它检测到用于匹配当前路由的数据不正确时,它将重定向。重定向将导致重新运行路由匹配,更新的 URL 数据现在可在路由匹配器中同步使用。问题中的示例可以大致如下实现:

    let wpData = null;
    let wpDataUrl = null;
    
    Run Code Online (Sandbox Code Playgroud)
    @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;
    
        // Pass if the data used to match the route was right
        if (url === wpDataUrl) {
          return true;
        }
    
        // Otherwise get the right data to match the routes
        // (Utilize the asynchronicity here that UrlMatcher does not have)
    
        const navId = this.router.getCurrentNavigation().id; // Angular 7.2+
        let newWpData = await this.wp.getByUrl(url);
    
        // Abort if this navigation is obsolete
        if (navId !== this.router.getCurrentNavigation()?.id) {
          return false;
        }
    
        // Update the data for the route re-matching
        wpData = newWpData;
        wpDataUrl = url;
    
        // Preferred option for re-matching the routes:
        return this.router.parseUrl(url); // Angular 7.1+
    
        // Hacky option:
        // First redirect to a different route
        await this.router.navigateByUrl('/loading', { skipLocationChange: true });
        // Now the router will re-match the routes for the original URL
        await this.router.navigateByUrl(url);
        return false;
      }
    }
    
    Run Code Online (Sandbox Code Playgroud)
    const wpRouteMatcher: UrlMatcher = (
      segments: UrlSegment[],
      group: UrlSegmentGroup,
      route: Route
    ) => {
      return (wpData == null // match any route the first time
        || 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' },
          },
          // ...
        ]
      },
    ];
    
    Run Code Online (Sandbox Code Playgroud)
  2. 使用具有标准路由的组件内的自定义逻辑动态加载组件。

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

    // app-injector.ts or some other file
    
    import { Injector } from '@angular/core';
    
    export let appInjector: Injector = null;
    
    export function setAppInjector(newAppInjector: Injector) {
      appInjector = newAppInjector;
    }
    
    Run Code Online (Sandbox Code Playgroud)
    // main.ts
    
    import { setAppInjector } from './app-injector';
    
    platformBrowserDynamic().bootstrapModule(AppModule).then(ref => {
      // ...
      setAppInjector(ref.injector);
    });
    
    Run Code Online (Sandbox Code Playgroud)
    import { appInjector } from './app-injector';
    import { FooService } from './foo.service';
    
    // Anywhere else, including inside a UrlMatcher
    const fooService = appInjector.get<FooService>(FooService);
    
    Run Code Online (Sandbox Code Playgroud)