0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ソースコードで理解するNestJS③: RoutesResolver, RouterExplorer

Posted at

前回のまとめなど

これまでで、NestJSにおけるデコレータ(@Controllerなど)の内部的な動作と、指定された情報がどのように利用されているかをみました。今回は、前回の記事で触れた二つの重要なクラスであるRoutesResolverRouterExplorerについて詳しく掘り下げます。

RoutesResolver

RoutesResolverはNestアプリケーションのルーティングの中核をなすクラスです。NestJSアプリケーションを作成する際NestFactory.createによって生成されるNestApplicationインスタンスのコンストラクタ内で、RoutesResolverは生成されます。 

export class NestApplication
  extends NestApplicationContext<NestApplicationOptions>
  implements INestApplication
{
  // 省略
  constructor(
    ...
  ) {
  
    // 省略 
    
    this.routesResolver = new RoutesResolver(
      this.container,
      this.config,
      this.injector,
      this.graphInspector,
    );
  }

RoutesResolverResolverインターフェースを実装しています。

export interface Resolver {
  resolve(instance: any, basePath: string): void;
  registerNotFoundHandler(): void;
  registerExceptionHandler(): void;
}

resolveの実装

  public resolve<T extends HttpServer>(
    applicationRef: T,
    globalPrefix: string,
  ) {
    ...
  }

ジェネリクスが使われており、resolveの第一引数であるinstance: anyHttpServerインターフェースを実装することがわかります(Expressのexpress.Routerなど)。

実際の処理を見てみると、現在のNestJSアプリケーションに存在するModuleの全体を取得し、それぞれのコントローラを取得してthis.registerRoutersを実行しています。


const modules = this.container.getModules();
    modules.forEach(({ controllers, metatype }, moduleName) => {
      const modulePath = this.getModulePathMetadata(metatype);
      this.registerRouters(
        controllers,
        moduleName,
        globalPrefix,
        modulePath,
        applicationRef,
      );
    });

this.getModulePathMetadataについては省略して、先に進みます。

public registerRouters(
 // 対象のModuleに登録されたController群 ("ルートマップ")
 routes: Map<string | symbol | Function, InstanceWrapper<Controller>>, 
 // @Moduleを付与したクラスの名称 
 moduleName: string,
 // アプリケーション全体のパスに対するprefix ('v1'など)
 globalPrefix: string,
 // getModulePathMetadataでMODULE_PATHメタデータから取得された、
 // AppModuleの`@Module`において`RouterModule.register`で指定するモジュールパス
 modulePath: string,
 // express.Routerなど
 applicationRef: HttpServer,
) {
 // ルートマップ内の各コントローラをループ処理
 routes.forEach(instanceWrapper => {

   //省略
   
   // コントローラのルートパスを抽出 (一般には複数の@Getや@Postなどがあるのでパスが複数)
   const routerPaths = this.routerExplorer.extractRouterPath(
     metatype as Type<any>,
   );

   // 省略

   //各ルートパスについての処理を実行
   routerPaths.forEach(path => {

    // 省略
    
     // ルートの探索と登録を実行
     this.routerExplorer.explore(
           instanceWrapper,  // 個々のコントローラのインスタンス
           moduleName,       // コントローラが属するモジュール名
           applicationRef,   // express.Routerなど(HttpServer)
           host,             // @Controller(...)で指定されたホスト情報
           routePathMetadata,// 関連メタ情報群
     );
   });
 });
}

routePathMetadataについては、modulePathglobalPrefixなどを含んでおり、各種デコレータで指定された情報をまとめたものになります。インターフェース定義は以下を参照。

RoutesResolverAppModuleに登録された各モジュールについて、各コントローラの個別のルートパス(@Get()@Post()などで指定される)単位まで処理を分解することが責務

RouterExplorer

Resolverによって解決されたパス情報をもとに、RouterExplorerapplicationRef、すなわちexpress.Routerのようなルーティングオブジェクトにパスそのハンドラを登録していきます。これにより、NestJSのデコレータで宣言されたルートが、実際のHTTPリクエストに応じて正しく処理されるようになります。前回で軽く内容を見ましたが、改めて追っていきます。


public explore<T extends HttpServer = any>(
    instanceWrapper: InstanceWrapper,
    moduleKey: string,
    applicationRef: T,
    host: string | RegExp | Array<string | RegExp>,
    routePathMetadata: RoutePathMetadata,
  ) {
    const { instance } = instanceWrapper;
    const routerPaths = this.pathsExplorer.scanForPaths(instance);
    this.applyPathsToRouterProxy(
      applicationRef,
      routerPaths,
      instanceWrapper,
      moduleKey,
      routePathMetadata,
      host,
    );
  }

ここで、this.pathExplorer.scanForPathsというメソッドが登場し、RouteDefinition[]型のroutesPathsを返しています。ここには、パスやメソッド名などの情報、そしてコールバックが含まれます。

export interface RouteDefinition {
  path: string[];
  requestMethod: RequestMethod;
  targetCallback: RouterProxyCallback;
  methodName: string;
  version?: VersionValue;
}

RouterProxyCallback型は以下で定義されており、expressにおけるハンドラ関数=ルートが一致した時に実行される関数です。

export type RouterProxyCallback = <TRequest, TResponse>(
  req?: TRequest,
  res?: TResponse,
  next?: () => void,
) => void;

前回の記事で見た通り、applyPathsToRouterProxy以降の処理はシンプルで、基本はRouteDefinitionのそれぞれについて、applicationRef = express.Routerにパスとハンドラを登録していくだけです。ということで、あとはこのRouteDefinitionを返すメソッド(を提供するクラスPathsExplorer)を理解すれば十分でしょう。

RouterExplorerは「ルート定義情報」をもとに、コントローラクラスで定義された各ルートハンドラをexpress.Routerのような実際のルーティングオブジェウト(HTTPサーバー)に登録する。

PathsExplorer

PathsExplorer.scanForPathsの実装は以下のようになっています。

export class PathsExplorer {
  constructor(private readonly metadataScanner: MetadataScanner) {}

  public scanForPaths(
    instance: Controller,
    prototype?: object,
  ): RouteDefinition[] {
    const instancePrototype = isUndefined(prototype)
      ? Object.getPrototypeOf(instance)
      : prototype;

    return this.metadataScanner
      .getAllMethodNames(instancePrototype)
      .reduce((acc, method) => {
        const route = this.exploreMethodMetadata(
          instance,
          instancePrototype,
          method,
        );

        if (route) {
          acc.push(route);
        }

        return acc;
      }, []);
  }

ここでMetadataScacnner#getAllMethodNamesControllerのプロトタイプを受け、そこに定義されているメソッドの一覧を取得します。

取得されたメソッドのそれぞれについて、exploreMethodMetadataでメソッドを取得し、RouteDefinitionを組み立てます。


 public exploreMethodMetadata(
    instance: Controller,   // Controllerクラスのインスタンス
    prototype: object,      // 上記のプロトタイプ
    methodName: string,     // Controllerクラスで定義されたあるメソッドの名前
  ): RouteDefinition | null {

    // コールバックメソッドを取得
    const instanceCallback = instance[methodName];

    // そのメソッドのPATH_METADATAから、@Get(routePath)などで指定したルートパスを取得
    const prototypeCallback = prototype[methodName];
    const routePath = Reflect.getMetadata(PATH_METADATA, prototypeCallback);
    if (isUndefined(routePath)) {
      return null;
    }

    // @Getや@Postなどで指定しMETHOD_METADATAで管理されたHTTPメソッド名を取得
    const requestMethod: RequestMethod = Reflect.getMetadata(
      METHOD_METADATA,
      prototypeCallback,
    );

    // @Versionデコレータで指定したバージョン値を取得
    const version: VersionValue | undefined = Reflect.getMetadata(
      VERSION_METADATA,
      prototypeCallback,
    );

    // ルートパスを必要に応じて加工
    const path = isString(routePath)
      ? [addLeadingSlash(routePath)]
      : routePath.map((p: string) => addLeadingSlash(p));


    // まとめて返却
    return {
      path,
      requestMethod,
      targetCallback: instanceCallback,
      methodName,
      version,
    };
  }
}

PathsExplorerはコントローラクラスのインスタンスから、ルートパスやハンドラ関数の実体などを取得し、ルート定義情報を生成する。

まとめ

今回の記事では、NestJSのルーティング処理における2つの重要なクラス、RoutesResolverRouterExplorerについて詳しく見てきました。

まず、RoutesResolverは、アプリケーション内のすべてのモジュールとコントローラを取得し、それらのルート定義を整理する役割を果たします。このクラスは、各コントローラが持つルートパスやHTTPメソッドの情報を集約し、RouterExplorerに渡す準備を整えます。

次に、RouterExplorerは、RoutesResolverから受け取ったルート定義をもとに、実際のルーティング処理を行います。具体的には、express.RouterなどのHTTPサーバーに対して、ルートとそのハンドラを登録します。このプロセスにより、デコレータを通じて宣言されたルートが正しくHTTPサーバーへ反映されるようになります。

①-③でNestJSのルーティング動作については大体見えてきました。次回からは、@Injectableデコレータ関連の実装を起点にして、NestJSのもう一つの重要な要素である依存性注入(DI)の理解を深めることにします。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?