動的モジュール負荷によるNESTJS


動的モジュール負荷によるNESTJS



説明


NestJSは、Dependency Injection Principleのような重要なデザインパターンを実装するよく構築されたサーバー側のtypescriptフレームワークです.
NestjsはNodeJSを使用して一貫したマイクロサービスやモノリシックサーバを構築するために必要なすべてのテクノロジを中心に.
NESTJSは、3つの主なビルドブロックを使用してアプリケーションを構成します.
  • コントローラ
  • プロバイダー
  • モジュール
  • Controllers in NestJS are responsible for handling any incoming requests and returning responses to the client side of the application.

    Providers (also called services) can be created and injected into controllers or other providers. Providers are designed to abstract any form of complexity and logic.

    Modules let you group related files. Providers and controllers are referenced through modules. In NestJS, modules encapsulate providers by default. In other words, it is not possible to inject providers into a module that are not part of the module or exported from another module. Modules can import other modules - basically, this enables sharing of providers across modules.


    以下はNESTJSのモジュールの概念を示す図です.

    ご覧のように、すべてのアプリケーションは少なくとも1つのルートモジュール(アプリケーションモジュール)を持っています.ルートモジュールは基本的にNSTEJSがアプリケーショングラフを構築するために使用する出発点です.
    NESTJSモジュールでは、モジュール、プロバイダー、コントローラ間の関係を作成するためのセクション(プロパティ)を持つ入力としてオブジェクトを受け取る@ module - decoratorを持つクラスとして定義されています.

    Providers It takes a list of providers as input. These providers will be instantiated by the NestJS injector. By default, a Provider belonging to a Module will be available within the module.

    Controllers This array specifies the set of controllers in the module. Basically, NestJS will automatically instantiate them during startup.

    Imports In this section, we can specify the list of imported modules. Basically, this enables sharing of providers across modules

    Exports This specifies the providers that are provided by this module. In other words, we specify the providers that are exported by this module.


    事前の要件


    システムにインストールされているgit - cli、nodejs、nestjs cliがあると仮定します.このチュートリアルはLinux環境で実行されていましたが、あなたの好みのオペレーティングシステムについて簡単に調整できます.

    単純なNESTJSアプリケーション


    擬似データベース内の2つのエンティティ(表)の内容にアクセスするAPIの2つのバージョンを作成します.
    最初のバージョンでは、従来の形式では、AppModuleは、エンティティをカプセル化する2つのモジュールのそれぞれにインポートされます.
    2番目のバージョンでは、/src/db/entityサブディレクトリに存在するすべてのモジュールを動的にインポートします
    これは、このサブディレクトリの新しいエンティティのための新しいモジュールを作成するように、これらのモジュールを動的にインポートすることなく、それらを参照する必要がインポートされます.それらを作成し、自動的にシステムの一部になります
    このようにしてシステムは次のようになります.

    エンティティ単位


    各エンティティに対しては、HTTPリクエスト、エンティティのデータを検索するサービス(コントローラによって使用される)と、対応するコントローラとサービスを参照するモジュール(エンティティ図やムービーに対応するユニットがある)に対応するコントローラがあります.

    動的モジュール読み込みのないバージョン


    理論の十分な、仕事に生地を入れましょう!
    以下に、APIアプリケーションを使用したサブディレクトリがあります.

    最も関連したソースコードを調べましょう

    アプリ。モジュールです。TS


    ルートモジュールアプリケーション.モジュールです.TSは単に本と映画エンティティモジュールをシステムにインポートします.
    import { Module } from '@nestjs/common';
    import { BookModule } from './db/entity/book/book.module';
    import { MovieModule } from './db/entity/movie/movie.module';
    
    @Module({
      imports: [BookModule, MovieModule],
    })
    export class AppModule {}
    
    DB/Entityサブディレクトリには、それぞれのモジュール、プロバイダー、およびコントローラファイルを持つ実体(本と映画)のリストが含まれます.類似性のため、ブックエンティティのソースコードのみを調べます.

    ブック.モジュールです。TS


    モジュール帳.モジュールです.TSも非常に簡単です.ちょうどコントローラの本をロードします.コントララー.TSとプロバイダブックを作成します.サービス利用できるTS.
    import { Module } from '@nestjs/common';
    import { BookService } from './book.service';
    import { BookController } from './book.controller';
    
    @Module({
      controllers: [BookController],
      providers: [BookService],
    })
    export class BookModule {}
    

    ブック.コントローラ。TS


    コントローラブック.コントローラ.tsは、パラメータを受け入れる経路/本の上でHTTP GET Requeststのために反応して、ちょうど
    対応ID .
    コントローラは本を使います.サービスTSサービスのFindByIdメソッドを使用して、IDに対応する本を検索して返します.
    import { Controller, Get, ParseIntPipe, Query } from '@nestjs/common';
    import { BookService, Book } from './book.service';
    
    @Controller('book')
    export class BookController {
      constructor(private readonly bookService: BookService) {}
    
      @Get()
      getBook(@Query('id', ParseIntPipe) id: number): Book {
        return this.bookService.findById(id);
      }
    }
    

    ブック.サービスTS


    最後に、本.サービスTSサービスでは、単にFindoByIdメソッドを提供します.このメソッドは、本の擬似テーブルからそのIDを返します.
    import { Injectable } from '@nestjs/common';
    
    export interface Book {
      id: number;
      title: string;
    }
    
    @Injectable()
    export class BookService {
      private static _books: Array<Book> = [
        {
          id: 1,
          title: 'Nest.js: A Progressive Node.js Framework (English Edition)',
        },
        { id: 2, title: 'NestJS Build a RESTFul CRUD API' },
        { id: 3, title: 'Pratical Nest.js' },
      ];
    
      findById(id: number): Book {
        return BookService._books.find((book) => book.id === id);
      }
    }
    

    APIの実行


    コマンドでAPIを実行するとき:
    # Run the NestJS server application
    $ nest start
    
    コンソールに次のメッセージが表示されます.

    簡単でクリーン.さあ、ちょっと複雑にしましょう.

    動的モジュール読み込みによるバージョン


    NSTESTは、起動時にすべてのモジュールをロードする必要がないlazy-loading of modulesの実装を持っています.
    ここで提案しているのはこれではない.我々は、手動で各モジュールをロードせずに起動時にそれをロードします.
    モジュールを動的にロードするには、GLOBsを使用して、特定のサブディレクトリ内のモジュールを見つけ、ネイティブの関数import ()を使用して、対応するJSファイルからモジュールをロードして抽出します.
    オリジナルプロジェクトを変更した後に、次の変更があります.

    アプリ。モジュールです。TS


    ルートモジュールアプリケーションへの主な変更点.モジュールです.tsは以下の通りです.
  • はもはや実体単位モジュール(本と映画)を明示的にロードしません.
  • 我々はアプリを提供します.サービス動的モジュールがロードされてコンソールに表示されたときにイベントを受け取るだけです.
  • 私たちは動的モジュールモジュール(ModuleloAderModule)のレジスタメソッドを使用して、モジュールを動的にロードする場所に関する情報を指定します.
  • 次のソースコードがあります.
    import { Module } from '@nestjs/common';
    import { EventEmitterModule } from '@nestjs/event-emitter';
    import * as path from 'path';
    import { AppService } from './app.service';
    import { ModuleLoaderModule } from './common/module-loader.module';
    
    @Module({
      imports: [
        EventEmitterModule.forRoot({ wildcard: true }),
        /**
         * Load all entity unit modules in subdirectory /db/entity
         */
        ModuleLoaderModule.register({
          name: 'db-entities',
          /**
           * Make sure the path resolves to the **DIST** subdirectory, (we are no longer in TS land but JS land!)
           */
          path: path.resolve(__dirname, './db/entity'),
          fileSpec: '**/*.module.js',
        }),
      ],
      providers: [AppService],
    })
    export class AppModule {}
    

    動的モジュールローダ


    我々は、/一般的なsubrirectoryで動的ローディングサービスを行う3つの新しいソースを持っています
  • モジュールローダdefs.ts -このファイルには動的負荷モジュールで使用される定義が含まれます.
  • モジュールローダ.サービスTS -モジュールの動的ロードの最後にイベントを発行します.
  • モジュールローダ.モジュールです.TS -実際に汚い仕事をします.
  • モジュールローダdefs。TS


    import { ModuleRef } from '@nestjs/core';
    
    /**
     * Constants used in ModuleLoader implementation
     */
    export const MODULE_LOADER_OPTIONS = 'MODULE_LOADER_OPTIONS';
    export const MODULE_LOADER_NAMES = 'MODULE_LOADER_NAMES';
    export const MODULE_LOADER = 'MODULE_LOADER';
    export const EV_MODULE_DYN_LOADER = 'EV_MODULE_DYN_LOADER.';
    
    /**
     * Options interface for ModuleLoaderModule.register
     */
    export interface IModuleLoaderOptions {
      /**
       * Name of modules
       */
      name: string;
      /**
       * Path's modules to load
       */
      path: string;
      /**
       * Depth to search modules inside of directories's path
       * default: -1 (INFINITY) - searches in root path only
       */
      depht?: number;
      /**
       * File spec to match - accepts globs and list of globs/file names
       * default: '*.module.ts'
       */
      fileSpec?: string | Array<string>;
      /**
       * File spec to ignore - accepts globs and list of globs/file names
       */
      ignoreSpec?: string | Array<string>;
    }
    
    /**
     * Event type fired when modules are loaded
     */
    export interface IModuleDynLoaderEvent {
      name: string;
      moduleNames?: Array<string>;
      error?: Error | string;
    }
    

    モジュールローダ。サービスTS


    import { Injectable, Inject, Scope, OnModuleInit } from '@nestjs/common';
    import {
      MODULE_LOADER_OPTIONS,
      MODULE_LOADER_NAMES,
      EV_MODULE_DYN_LOADER,
      IModuleLoaderOptions,
    } from './module-loader-defs';
    import { EventEmitter2 } from '@nestjs/event-emitter';
    import { nextTick } from 'process';
    
    @Injectable({
      scope: Scope.TRANSIENT,
    })
    export class ModuleLoaderService implements OnModuleInit {
      constructor(
        @Inject(MODULE_LOADER_OPTIONS) private _options: IModuleLoaderOptions,
        @Inject(MODULE_LOADER_NAMES) private _moduleNames: Array<string>,
    
        private eventEmitter: EventEmitter2,
      ) {}
    
      /**
       * @description Emmits as events when modules are loaded
       */
      onModuleInit() {
        nextTick(() => {
          const eventName = EV_MODULE_DYN_LOADER + this._options.name;
          this.eventEmitter.emit(eventName, {
            name: this._options.name,
            moduleNames: this._moduleNames,
          });
        });
      }
    }
    

    モジュールローダ。サービスTS


    import { Logger, Module, DynamicModule } from '@nestjs/common';
    import * as fb from 'fast-glob';
    import * as path from 'path';
    import { ModuleLoaderService } from './module-loader.service';
    import {
      MODULE_LOADER,
      MODULE_LOADER_OPTIONS,
      MODULE_LOADER_NAMES,
      IModuleLoaderOptions,
    } from './module-loader-defs';
    
    export const moduleLoaderFactory = {
      provide: MODULE_LOADER,
      useFactory: (moduleLoaderService: ModuleLoaderService) => {},
      inject: [ModuleLoaderService],
    };
    
    interface IModuleInfo {
      name: string;
      module: DynamicModule;
    }
    
    /**
     * @description helper static class to load modules dynamically.
     */
    class InternalModuleLoader {
      static readonly logger = new Logger(InternalModuleLoader.name);
    
      /**
       * @param _options for GLOB searches
       * @returns a Promise thats resolves to a list of name and module references based on _options filespec
       */
      static async loadModules(
        _options: IModuleLoaderOptions,
      ): Promise<Array<IModuleInfo>> {
        return new Promise((resolve, reject) => {
          this.getModuleFileNames(_options).then((filePaths: Array<string>) => {
            if (filePaths.length == 0) {
              resolve([]);
            } else {
              const loadedModules: Array<Promise<any>> = filePaths.map((filePath) =>
                this.loadModule(filePath),
              );
              if (loadedModules.length === 0) {
                resolve([]);
              } else {
                const moduleInfos: Array<IModuleInfo> = new Array();
                Promise.all(loadedModules).then((modules: Array<any>) => {
                  for (let i = 0; i < modules.length; i++) {
                    let module = modules[i];
                    const moduleField = Object.keys(module).find(
                      (key) => key.indexOf('Module') >= 0,
                    );
                    if (moduleField) {
                      moduleInfos.push({
                        name: moduleField,
                        module: module[moduleField],
                      });
                    }
                  }
                  resolve(moduleInfos);
                });
              }
            }
          });
        });
      }
    
      /**
       * @description Uses native import() to dynamicly load a module
       * @param modulePath
       * @returns a Promise thats resolves to module loaded
       */
      private static async loadModule(modulePath: string): Promise<any> {
        return import(modulePath);
      }
    
      /**
       * @description Uses FatsGlob to load the filenames for the modules
       * @param _options for GLOB searches
       * @returns a list of module's file paths
       */
      private static async getModuleFileNames(
        _options: IModuleLoaderOptions,
      ): Promise<Array<string>> {
        const spec: Array<string> = (
          typeof _options.fileSpec === 'string'
            ? [_options.fileSpec]
            : _options.fileSpec
        ).map((fileSpec) => path.join(_options.path, fileSpec));
        let options: fb.Options = {
          onlyFiles: true,
        };
        if (_options.depht) {
          options.deep = _options.depht < 0 ? Infinity : _options.depht;
        }
        if (_options.ignoreSpec) {
          options.ignore = Array.isArray(_options.ignoreSpec)
            ? _options.ignoreSpec
            : [_options.ignoreSpec];
        }
        this.logger.log(`**Module Loader FileSpec**: "${spec}"`);
    
        return fb(spec, options);
      }
    }
    
    @Module({})
    export class ModuleLoaderModule {
      /**
       * @description Load Modules dynamically via GLOBs and native import() function.
       * @param moduleLoaderOptions options for GLOB searches
       */
      public static async register(
        moduleLoaderOptions: IModuleLoaderOptions,
      ): Promise<DynamicModule> {
        const moduleInfos = await InternalModuleLoader.loadModules(
          moduleLoaderOptions,
        );
        const modules = moduleInfos.map((moduleInfo) => moduleInfo.module);
        const moduleNames = moduleInfos.map((moduleInfo) => moduleInfo.name);
    
        return {
          module: ModuleLoaderModule,
          imports: [...modules],
          providers: [
            {
              provide: MODULE_LOADER_OPTIONS,
              useValue: moduleLoaderOptions,
            },
            {
              provide: MODULE_LOADER_NAMES,
              useValue: moduleNames,
            },
            ModuleLoaderService,
            moduleLoaderFactory,
          ],
        };
      }
    }
    
    コマンドでAPIを実行するとき:
    # Run the NestJS server application
    $ nest start
    
    コンソール上に次のメッセージが表示されます(ブックと映画の各モジュールのモジュールが動的にロードされていることに注意してください).

    このチュートリアルからの例の実行


    インストール


    # Clone tutorial repository
    $ git clone https://github.com/maceto2016/NestJSDynLoad
    
    # access the project folder through the terminal
    $ cd NestJSDynLoad
    
    # Install dependencies
    $ npm install
    

    アプリケーションを実行する


    # Run the NestJS server application
    $ nest start
    

    アプリケーションのテスト


    # Get book with id = 1
    $ curl http://localhost:3000/book?id=1 | json_pp
    

    結論


    このチュートリアルでは、よく構築されたNESTJSフレームワークへの小さな導入を行いました.
    指定されたサブディレクトリに存在するすべてのモジュールを、NestJjsアプリケーションに動的にロードする方法を示します.コード内でそのようなモジュールを手動で参照する必要はありません.これはいくつかの状況で実際的でありえます.
    この記事で使用される原則は、モジュールlazy loadの独自のバージョンを含む様々な実装を可能にします.
    NestJS ConfigServiceを使用してRegisterAsyncバージョンを実装し、Envファイルで定義されているFileSpecからファイルリストを読み込むことができますか?
    それはあなた次第です!
    読書ありがとうございます.私はあなたのフィードバックを聞いて満足している!