mausconi / nestjs-tips-tricks-ru

NestJS Tips and Tricks ( RU )

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Содержание

Как заинжектить класс динамически ?

В NestJS существует ModuleRef.
Этот провайдер позволяет инжектить сервисы на ходу в приложении.
Например

const service = this.moduleRef.get<Service>(Service);

Осталось только заинжектить ModuleRef у ваш провайдер.

Обратите внимание на то что если вы хотите заинжектить провайдер, не только с текущего модуля, то вторым параметром передаем опцию { strict: false }.

Зачем это нужно ?
В случаях кодга нужно получить класс взависимости от условия, что бы не инжектить в конструктор кучу провайдеров. Допустим у нас есть несколько провайдеров для разных API сервисов.
Нужно использовать один из адаптеров взависимости от параметра который прилетел с Request.
Дабы не инжектить все адаптеры.

Можно ли полностью отключать модуля взависимости от конфига ConfigService ?

Ответ: нет.

Можно это сделать с помощью environment variables.
На примере псевдокода:

const modules = [ modules ]

if (process.env.ENABLE_SOME_MODULE) {
  modules.push(SomeModule)
}

@Module({
  imports: modules
})

Как заинжектить класс в main.ts ?

Это делается очень просто:

const service = app.get<ServiceName>(ServiceName);

service.callMethod();

Как сделать на минималках cli ?

Есть такая штука в несте как Application Context.
Эта вещь позволяет инициализировать только провайды у ваших модулях без инициализации HTTP части, то есть без контроллеров.

Создаем отдельно файл в src с названием cli.ts.

import { NestFactory, ModuleRef } from '@nestjs/core';
import { CLIModule } from './cli.module';
import { cliCommandInvoker } from './cli-command-invoker';
import { cliCommands } from './cli-commands';

async function bootstrapCLI() {
    const cli = await NestFactory.createApplicationContext(CLIModule);
    const moduleRef = cli.get(ModuleRef);
    cli.enableShutdownHooks();
    const [,,command] = process.argv;

    cliCommandInvoker(moduleRef, cliCommands, command);
}

bootstrapCLI();

Создаем CLIModule который будет содержать в себе cli команды.

import { Module } from '@nestjs/common';
import { HelloCommand } from './hello.command';

@Module({
    imports: [],
    providers: [
        HelloCommand,
    ]
})
export class CLIModule {
}

Создаем interface для наших cli комманд.

export interface CLICommand {
    invoke(...args);
}

Создаем нашу первую cli команду HelloCommand.

import { CLICommand } from './command';
import { Injectable } from '@nestjs/common';

@Injectable()
export class HelloCommand implements CLICommand {
    public invoke() {
        console.log('Hello world');
    }
}

Следующим шагом замапим наши команды на наши классы. Сделаем это с помощью Map.
Создадим файл cli-commands.ts.

export const cliCommands = new Map<string, Type<CLICommand>>();
cliCommands.set('hello', HelloCommand);

Теперь нужно создать функцию для вызова cli команд cliCommandInvoker.
Создадим файл cli-command-invoker.ts

import { Type } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { CLICommand } from './command';

export function cliCommandInvoker(moduleRef: ModuleRef, commands: Map<string, Type<CLICommand>>, command: string) {
    const commandHandlerClass = commands.get(command);
    const commandHandler = moduleRef.get<CLICommand>(commandHandlerClass);
    commandHandler.invoke(process.argv);
}

Пользуемся так

npx ts-node ./src/cli.ts hello

Выхлоп

[Nest] 548231   - 2020-04-17 6:45 PM   [NestFactory] Starting Nest application...
[Nest] 548231   - 2020-04-17 6:45 PM   [InstanceLoader] CLIModule dependencies initialized +23ms
Hello world

Как сделать поиск классов и методов класса по metadata если вы захотите сделать что-то в стиле nest-way с декоратоми и куртизанками ?

Данная статья является примером и не претендует на production ready код.
Пример является реальным поскольку на момент разработки бекенда на NestJS в начале 2019 года не было модуля для очередей.
На момент написания статьи в NestJS уже сущестует данный функционал - Queue.

Например нужно нужно сделать обработчик очереди и он должен выглядеть как класс с методом invoke который и будет выполнять таску
К примеру прикрутим сюда bulljs.

Напишем прототип чего мы хотим

class SomePayload {
  constructor(public text: string) {}
}

@Queue('QueueName', jobOptions)
@Injectable()
export class QueueHandler implements Consumer {

    public async invoke(bullJob: Bull.Job<SomePayload>) {
        console.log(bullJob.data)
    }
}

И как будет выглядеть вызов этой таски из вашего сервиса:

this.bull.queue('QueueName').add(queuePayload, options);

Будем создавать постепенно недостающие части для нашей фичи.

Для начала давайте сделаем декоратор для обработчика очереди:

import { Injectable, SetMetadata } from '@nestjs/common';
import * as Bull from 'bull';

export const QUEUE_ARGS_METADATA = '__queue-args-metadata__';

export function Queue(name: string, options?: Bull.QueueOptions): ClassDecorator {
    return (target: object) => {
        SetMetadata(QUEUE_ARGS_METADATA, { name, options })(
            target,
        );
    };
}

Создадим интерфейс для класса который будет служить обработчиком очереди:
Мы же должны четко понимать какой метод класса должен быть вызван для обработки очереди.

export interface Consumer {
    invoke(job: Bull.Job<Dictionary>): unknown;
}

Теперь нужно просканировать NestContainer ( DI Container ) и выбрать наши классы "обработчики" для очереди.
Напишем так званый QueueExplorer задача которого прошелестеть NestContainer и отфильтровать классы.

import { flattenDeep, compact } from 'lodash';
import * as Bull from 'bull';
import { ModulesContainer } from '@nestjs/core/injector/modules-container';
import { Injectable } from '@nestjs/common';

interface QueueInfo {
    name: string;
    options?: Bull.QueueOptions;
    instance: Consumer;
}

@Injectable()
export class QueueExplorer {
    constructor(
        private modulesContainer: ModulesContainer,
    ) {
    }

    // преобразуем древовидную структуру NestContainer в плоскую и фильтруем инстансы
    public explore(): QueueInfo[] {
        const components = [
            ...this.modulesContainer.values(),
        ].map(module => module.providers);

        return compact(flattenDeep(
            components.map(component =>
                [...component.values()]
                    .map(({ instance }) => this.filterCommands(instance as Consumer)),
            ),
        ));
    }

    // фильтруем наши классы по наличии QUEUE_ARGS_METADATA метадаты в провайдере
    protected filterCommands(instance: Consumer) {
        if (!instance) {
            return;
        }

        const metadata = Reflect.getMetadata(QUEUE_ARGS_METADATA, instance.constructor);

        if (metadata === undefined) {
            return;
        }

        return { ...metadata, instance };
    }
}

Теперь нужно связать наши обработчики с bulljs.
Добавляем наш QueueExplorer в массив провайдеров, допустим, в AppModule.
А так же добавляем QueueHandler в массив провайдеров, допустим, в дочерний модуль для того что бы увидеть что сканируется полностью все дерево NestContainer.

В AppModule создадим onModuleInit для связывания обработчиков и bulljs.
Заинжектим в конструктор модуля наш QueueExplorer.
В хуке onModuleInit запустим сканирование наших обработчиков для очереди:

const queueHandlers = this.queueExplorer.explore();

А теперь давайте связывать:

for (const { instance, options, name } of queueHandlers) {
    // Logger.log(`Init '${name}' consumer`, QUEUE_LOGGER_CTX);
    // создание очереди
    const queue = this.bull.queue(name, options);
    // биндинг обработчика очереди до класса.
    queue.process(instance.invoke.bind(instance));
}

Теперь можно смело пользоваться этой удобной фичей.

this.bull.queue('QueueName').add(new Somepayload('text'), optionalOptions);

По сути мы сделали следующие вещи:

  • отсканировали наши обработчики
  • забиндили вызов очереди на наш обработчик

Что можно добавить ? Задача сделать уже для вас.

  • Можно добавить обработку ошибок: queue.on('failed', instance.failedAction.bind(instance));
  • добавить проверку существования метода invoke в обработчике событий и выдавать RuntimeExeception если метод не реализован.

Как запускать и отлаживать приложение через Nest CLI и зачем он нужен

В стандартном шаблоне проекта, созданном через nest create, скрипты запуска установлены как nest start с различными параметрами.

nest -- это небольшая прослойка, позволяющая легко запускать Nest приложение в разных режимах, просто добавляя аргументы, например, --watch, --debug и даже --webpack для сборки через Webpack.

Не работает отладка в WebStorm (JetBrains, Intellij IDEA)

При запуске через nest start --debug (он же npm run start:debug) по кнопке кнопка Debug в WebStorm отладка не запускается (точки остановки не срабатывают).

Требуется создать отдельную задачу Attach to Node.js и запускать параллельно start:debug (nest start --debug).
https://www.jetbrains.com/help/webstorm/run-debug-configuration-node-js-remote-debug.html

Пути в Stacktrace ошибок указывают на js файлы в dist, а не на исходные ts файлы

Требуется добавить --exec \"node -r source-map-support/register\" к параметрам запуска, например:
"start:dev": "nest start --watch --exec \"node -r source-map-support/register\"",

About

NestJS Tips and Tricks ( RU )