Pull to refresh

Пишем nest.js с нуля на typescript

Reading time28 min
Views19K

Цитата из документации:

Nest предоставляет готовую архитектуру приложений, которая позволяет разработчикам и командам создавать высокопроверяемые, масштабируемые, слабо связанные и легко обслуживаемые приложения. Архитектура в значительной степени вдохновлена Angular.

Nest построен на основе шаблона проектирования - dependency injection. Мы увидим как он реализован в Nest и как это влияет на весь остальной код.

Для начала посмотрим на самый простой код для запуска приложения nestjs из документации:

main.ts

import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
 
async function bootstrap() {
    const app = await NestFactory.create(AppModule);
    await app.listen(3000);
}
 
bootstrap();

Итак. NestFactory главный класс nest с чего все и начинается. Его метод create сканирует существующие модули, которые доступны в дереве зависимостей из корневого модуля AppModule, после чего сканирует зависимости полученных модулей в виде контроллеров, сервисов и других модулей, и добавляет все это в контейнер. После сканирования возвращает экземпляр приложения. При запуске метода listen происходит инициализация сервера и регистрация роутеров, созданных в контроллере, с необходимыми для каждого обратными вызовами, которые уже хранятся в контейнере. 

К созданию подобной функциональности мы придем чуть позже, а сначала посмотрим какие у нас есть контроллер, провайдер и модуль.

app.controller.ts

import { Controller, Get, Post, Body, Param } from 'nestjs/common';
import { AppService } from './app.service';
 
@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}
 
  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
 
  @Post('body/:id')
  recieveBody(@Body() data: { recieveData: string }, @Param('id') id: string) {
    return 'body: ' + data.recieveData + ' has been recieved and id: ' + id;
  }
}

Что интересно в этом коде, так это то, что создавая методы запроса, мы просто навешиваем декораторы на функции, которые и становятся callbacks для роутеров. Мы не знаем, когда они регистрируется и как именно, нам не нужно думать о реализации, все, что мы делаем, это следуем заданной архитектуре. Мы говорим, что мы хотим сделать, а не как, то есть, в качестве пользователей Nest, используем декларативный подход. Соответственно мы реализуем данную функциональность в этой статье.

Также здесь есть зависимость AppService, которую Nest внедряет самостоятельно. Для нас же, это рабочий код. Зависимости в Nest разрешаются по типу, и мы рассмотрим как именно.

app.service.ts

import { Injectable } from 'nestjs/common';
 
@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

Здесь мы видим декоратор Injectable, который, по описанию в документации, определяет AppService как класс, которым может управлять контейнер Nest, что является не совсем правдой. На самом деле все, что он делает, это добавляет метаданные о времени жизни класса. По умолчанию оно совпадает с временем жизни приложения, и сам Nest не рекомендует изменять это поведение. Поэтому если вы не хотите это изменить, то Injectable можно опустить. А управлять им контейнер Nest может только в том случае, если он будет присутствовать в providers модуля, в котором он используется.

app.module.ts

import { Module } from 'nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
 
@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Итак. Возвращаясь к main.ts, реализуем NestFactory класс.

Стоит оговориться, некоторые вспомогательные функции, вроде проверки на null и т.д., а также интерфейсы будут опущены в статье, но будут находиться в исходном коде.

./core/nest-factory.ts

import { NestApplication } from "./nest-application";
import { NestContainer } from "./injector/container";
import { InstanceLoader } from "./injector/instance-loader";
import { DependenciesScanner } from "./scanner";
import { ExpressAdapter } from '../platform-express/express.adapter';
 
export class NestFactoryStatic {
    public async create(module: any) {
        const container = new NestContainer();
	  // Сканирует зависимости, создает экземпляры
	  // и внедряет их
        await this.initialize(module, container);
 
	  // Инициализирует http сервер и регистрирует роутеры
 	  // при запуске instance.listen(3000)
        const httpServer = new ExpressAdapter()
        container.setHttpAdapter(httpServer);
        const instance = new NestApplication(
            container,
            httpServer,
            applicationConfig,
        );
       
        return instance;
    }
 
    private async initialize(
        module: any,
        container: NestContainer,
    ) {
        const instanceLoader = new InstanceLoader(container)
        const dependenciesScanner = new DependenciesScanner(container);
 
        await dependenciesScanner.scan(module);
        await instanceLoader.createInstancesOfDependencies();
    }
}
 
 
/**
 * Используйте NestFactory для создания экземпляра приложения.
 *
 * ### Указание входного модуля
 *
 * Передайте требуемый *root module* (корневой модуль) для приложения
 * через параметр модуля. По соглашению он обычно называется
 * `AppModule`. Начиная с этого модуля Nest собирает граф
 * зависимостей и создает экземпляры классов, необходимых для запуска
 * вашего приложения.
 *
 * @publicApi
 */
export const NestFactory = new NestFactoryStatic();

Итак. Из кода мы видим, что сперва сканируются все существующие модули, а также их зависимости, а после создается http сервер. Поэтому сейчас мы реализуем класс DependenciesScanner. Он получится чуть больше, чем предыдущий, но не будем пугаться, ведь ничего сложного, на самом деле, там нет.

./core/scanner.ts

import { MODULE_METADATA } from "../common/constants";
import { NestContainer } from "./injector/container";
import 'reflect-metadata';
import { Module } from "./injector/module";
 
export class DependenciesScanner {
 
    constructor(private readonly container: NestContainer) {}
 
    public async scan(module: any) {
        // Сначала сканирует все модули, которые есть в приложении, и добавляет их в контейнер
        await this.scanForModules(module);
        // После у каждого модуля сканирует зависимости, такие как Controllers и Providers
        await this.scanModulesForDependencies();
    }
 
    public async scanForModules(module: any) {
        // Добавляет модуль в контейнер и возвращает при этом его экземпляр
        const moduleInstance = await this.insertModule(module);
        // Получает модули, которые были импортированы в этот модуль в массив imports.
        // Так как AppModule - корневой модуль, то от него идет дерево модулей.
        const innerModules = [...this.reflectMetadata(moduleInstance, MODULE_METADATA.IMPORTS)];
 
        // Перебирает внутренние модули этого модуля, чтобы сделать с ними тоже самое.
        // То есть, происходит рекурсия.
        for (const [index, innerModule] of innerModules.entries()) {
            await this.scanForModules(innerModule)
        }
 
        return moduleInstance
    }
 
    /**
     * Добавляет модуль в контейнер
     */
    public async insertModule(module: any) {
        return this.container.addModule(module);
    }
 
 
    /**
     * Получает из контейнера все модули, и сканирует у них
     * зависимости, которые хранятся в reflect объекте.
     */
    public async scanModulesForDependencies() {
        const modules: Map<string, Module> = this.container.getModules();
 
        for (const [token, { metatype }] of modules) {
            await this.reflectAndAddImports(metatype, token);
            this.reflectAndAddProviders(metatype, token);
            this.reflectAndAddControllers(metatype, token);
        }
    }
 
    public async reflectAndAddImports(
        module: any,
        token: string,
    ) {
        // Получает по модулю imports зависимости и добавляет их в контейнер
        const modules = this.reflectMetadata(module, MODULE_METADATA.IMPORTS);
        for (const related of modules) {
            await this.container.addImport(related, token);
        }
    }
 
    public reflectAndAddProviders(
        module: any,
        token: string,
    ) {
        // Получает по модулю providers зависимости и добавляет их в контейнер
        const providers = this.reflectMetadata(module, MODULE_METADATA.PROVIDERS);
        providers.forEach((provider: any) =>
            this.container.addProvider(provider, token),
        );
    }
 
    public reflectAndAddControllers(module: any, token: string) {
        // Получает по модулю controllers зависимости и добавляет их в контейнер
        const controllers = this.reflectMetadata(module, MODULE_METADATA.CONTROLLERS);
        controllers.forEach((controller: any) =>
            this.container.addController(controller, token),
        );
    }
 
    /**
     * Метод, который получает нужные зависимости по модулю и ключу зависимостей.
     */
    public reflectMetadata(metatype: any, metadataKey: string) {
        return Reflect.getMetadata(metadataKey, metatype) || [];
    }
}

Смотря на код, мы видим, что модули и их зависимости добавляются в контейнер, и это возможно благодаря двум классам используемым здесь - NestContainer и Module. На самом деле в контейнере хранятся модули как экземпляры класса Module, и их зависимости, такие как другие модули, контроллеры и провайдеры, хранятся в Module, в таких структурах данных как Map и Set. А сами зависимости, контроллеры и провайдеры, являются экземплярами класса InstanceWrapper. 

Классы Module и InstanceWrapper в нашей реализации являются довольно простыми, особенно второй, поэтому сначала реализуем наш контейнер.

./core/injector/container.ts

import { Module } from "./module";
import { ModuleTokenFactory } from "./module-token-factory";
import { AbstractHttpAdapter } from "../adapters";
 
export class NestContainer {
    private readonly modules = new Map<string, Module>();
    private readonly moduleTokenFactory = new ModuleTokenFactory();
    private httpAdapter: AbstractHttpAdapter | undefined;
 
    /**
     * Создает экземпляр класса Module и сохраняет его в контейнер
     */
    public async addModule(module: any) {
        // Создает токен модуля, который будет являться его ключом Map,
        // который и будет использоваться для проверки и получения этого модуля.
        const token = this.moduleTokenFactory.create(module);
 
        if (this.modules.has(module.name)) {
            return;
        }
 
        const moduleRef = new Module(module);
        moduleRef.token = token;
        this.modules.set(token, moduleRef);
 
        return moduleRef;
    }
 
    /**
     * Возвращает все модули, для сканирования зависимостей,
     * создания экземпляров этих зависимостей, и для использования в качестве callbacks
     * при создании роутеров его контроллеров, с разрешенными зависимостями.
     */
    public getModules(): Map<string, Module> {
        return this.modules;
    }
 
    /**
     * Контейнер также устанавливает и хранит единственный экземпляр http сервера,
     * в нашем случае express. Этот метод вызывается в классе NestFactory.
     */
    public setHttpAdapter(httpAdapter: any) {
        this.httpAdapter = httpAdapter;
    }
 
    /**
     * Будет вызван при создании роутеров в классе RouterExplorer.
     */
    public getHttpAdapterRef() {
        return this.httpAdapter;
    }    
 
    /**
     * При сканировании зависимостей для полученных модулей в DependenciesScanner,
     * у них также берется токен, по которому здесь находится модуль,
     * и с помощью своего метода добавляет к себе импортированный модуль.
     */
    public async addImport(
        relatedModule: any,
        token: string,
    ) {
        if (!this.modules.has(token)) {
            return;
        }
        const moduleRef = this.modules.get(token);
        if (!moduleRef) {
            throw Error('MODULE NOT EXIST')
        }
 
        const related = this.modules.get(relatedModule.name);
        if (!related) {
            throw Error('RELATED MODULE NOT EXIST')
        }
        moduleRef.addRelatedModule(related);
    }
 
    /**
     * Также как и для импортированных модулей, подобная функциональность
     * работает и для провайдеров.
     */
    public addProvider(provider: any, token: string) {
        if (!this.modules.has(token)) {
            throw new Error('Module not found.');
        }
        const moduleRef = this.modules.get(token);
        if (!moduleRef) {
            throw Error('MODULE NOT EXIST')
        }
        moduleRef.addProvider(provider)
    }
 
    /**
     * Также как и для импортированных модулей, подобная функциональность
     * работает и для контроллеров.
     */
    public addController(controller: any, token: string) {
        if (!this.modules.has(token)) {
            throw new Error('Module not found.');
        }
        const moduleRef = this.modules.get(token);
        if (!moduleRef) {
            throw Error('MODULE NOT EXIST')
        }
        moduleRef.addController(controller);
    }
}

Мы увидели здесь также класс ModuleTokenFactory, который создаёт токен, по которому хранится модуль. На самом деле, здесь можно обойтись и обычным созданием уникального id, например с помощью пакета uuid. Поэтому вы можете сильно не обращать на это внимание, но, кому интересно, вот максимально приближенная реализация этого класса к реализации Nest, только несколько упрощенная.

./core/injector/module-token-factory.ts

import hash from 'object-hash';
import { v4 as uuid } from 'uuid';
import { Type } from '../../common/interfaces/type.interface';
 
export class ModuleTokenFactory {
    // Здесь хранятся данные о том, какие модули уже были отсканированы.
    // На случай того, если один модуль является зависимостью у нескольких,
    // чтобы не было дубликатов.
    private readonly moduleIdsCashe = new WeakMap<Type<unknown>, string>()
 
    public create(metatype: Type<unknown>): string {
        const moduleId = this.getModuleId(metatype);
        const opaqueToken = {
            id: moduleId,
            module: this.getModuleName(metatype),
        };
        return hash(opaqueToken, { ignoreUnknown: true });
    }
 
    public getModuleId(metatype: Type<unknown>): string {
        let moduleId = this.moduleIdsCashe.get(metatype);
        if (moduleId) {
            return moduleId;
        }
        moduleId = uuid();
        this.moduleIdsCashe.set(metatype, moduleId);
        return moduleId;
    }
 
    public getModuleName(metatype: Type<any>): string {
        return metatype.name;
    }
}

Теперь рассмотрим класс Module.

./core/injector/module.ts

import { InstanceWrapper } from "./instance-wrapper";
import { randomStringGenerator } from "../../common/utils/random-string-generator.util";
 
export class Module {
    private readonly _imports = new Set<Module>();
    private readonly _providers = new Map<any, InstanceWrapper>();
    private readonly _controllers = new Map<string, InstanceWrapper>();
 
    private _token: string | undefined;
 
    constructor(
        private readonly module: any,
    ) {}
 
    get providers(): Map<string, any> {
        return this._providers;
    }
 
    get controllers(): Map<string, any> {
        return this._controllers;
    }
 
    get metatype() {
        return this.module;
    }
 
    get token() {
        return this._token!;
    }
 
    set token(token: string) {
        this._token = token;
    }
 
    public addProvider(provider: any) {
        this._providers.set(
            provider.name,
            new InstanceWrapper({
              name: provider.name,
              metatype: provider,
              instance: null,
            }),
        )
    }
 
    public addController(controller: any) {
        this._controllers.set(
            controller.name,
            new InstanceWrapper({
              name: controller.name,
              metatype: controller,
              instance: null,
            }),
        );
 
        this.assignControllerUniqueId(controller);
    }
 
    public assignControllerUniqueId(controller: any) {
        Object.defineProperty(controller, 'CONTROLLER_ID', {
          enumerable: false,
          writable: false,
          configurable: true,
          value: randomStringGenerator(),
        });
    }
 
    public addRelatedModule(module: Module) {
        this._imports.add(module);
    }
}

Комментарии здесь излишни. Все, что он делает, это хранит зависимости определенного модуля, сам модуль и его токен.

Теперь рассмотрим еще более простой класс InstanceWrapper.

./core/injector/instance-wrapper.ts

import { Type } from '../../common/interfaces/type.interface';
 
export class InstanceWrapper<T = any> {
    public readonly name: string;
    public metatype: Type<T> | Function;
    public instance: any;
    public isResolved = false
 
    constructor(metadata: any) {
        Object.assign(this, metadata);
        this.instance = metadata.instance;
        this.metatype = metadata.metatype;
        this.name = metadata.name
    }
}

При его создании к instance присваивается null. В дальнейшем, например, если контроллер имеет зависимость в виде провайдера в его конструкторе, то при внедрении зависимостей, экземпляр этого провайдера будет уже создан, и при создании экземпляра контроллера, будет добавлен в его конструктор. Собственно так и разрешаются зависимости. Собственно этим мы дальше и займемся.

Сейчас у нас есть функциональность сканирования модулей и их зависимостей. Модули добавляются в контейнер, хранятся по созданным токеном в виде класса Module, в котором они все и представлены и хранят свои зависимости, которые находятся в объекте reflect, в структурах данных Map и Set.

А теперь снова вернемся к классу NestContainer и взглянем на его метод initialize

private async initialize(
        module: Module,
        container: NestContainer,
    ) {
        const instanceLoader = new InstanceLoader(container)
        const dependenciesScanner = new DependenciesScanner(container);
 
        await dependenciesScanner.scan(module);
        await instanceLoader.createInstancesOfDependencies();

на его метод initialize

Сейчас, когда мы просканировали модули, нам нужно создать экземпляры их зависимостей. Поэтому сейчас реализуем класс InstanceLoader.

./core/injector/instance-loader.ts

import { NestContainer } from "./container";
import { Injector } from "./injector";
import { Module } from "./module";
 
export class InstanceLoader {
    private readonly injector = new Injector();
 
    constructor(private readonly container: NestContainer) {}
 
    public async createInstancesOfDependencies() {
        const modules = this.container.getModules();
 
        await this.createInstances(modules);
    }
 
    /**
     * Сначала создаются экземпляры провайдеров,
     * потому что если они являются зависимостями контроллеров,
     * при создании экземпляров для контроллеров, они уже должны
     * существовать.
     */
    private async createInstances(modules: Map<string, Module>) {
        await Promise.all(
            [...modules.values()].map(async module => {
                await this.createInstancesOfProviders(module);
                await this.createInstancesOfControllers(module);
            })
        )
    }
 
    private async createInstancesOfProviders(module: Module) {
        const { providers } = module;
        const wrappers = [...providers.values()];
        await Promise.all(
            wrappers.map(item => this.injector.loadProvider(item, module)),
        )
    }
 
    private async createInstancesOfControllers(module: Module) {
        const { controllers } = module;
        const wrappers = [...controllers.values()];
        await Promise.all(
            wrappers.map(item => this.injector.loadControllers(item, module)),
        )
    }
}

Тоже не сложный класс. Все, что она делает вызывает методы класса Injector. Что здесь стоит отметить, что уже написано в комментарии к методу createInstances, это то, что созданные экземпляры провайдеров будут добавляться в конструкторы соответствующих контроллеров при создании их экземпляров. 

Сейчас рассмотрим класс Injector, который несколько интереснее остальных, и который и производит внедрение зависимостей.

./core/injector/injector.ts

import { Module } from "./module";
import { InstanceWrapper } from './instance-wrapper';
import { Type } from  '../../common/interfaces/type.interface';
 
export class Injector {
 
    public async loadInstance<T>(
        wrapper: InstanceWrapper<T>,
        collection: Map<string, InstanceWrapper>,
        moduleRef: Module,
    ) {
        const { name } = wrapper;
 
        const targetWrapper = collection.get(name);
        if (!targetWrapper) {
            throw Error('TARGET WRAPPER NOT FOUNDED')
        }
        const callback = async (instances: unknown[]) => {
            await this.instantiateClass(
                instances,
                wrapper,
                targetWrapper,
            );
        }
        await this.resolveConstructorParams<T>(
            wrapper,
            moduleRef,
            callback,
          );
    }
 
    public async loadProvider(
        wrapper: any,
        moduleRef: Module,
    ) {
        const providers = moduleRef.providers;
        await this.loadInstance<any>(
            wrapper,
            providers,
            moduleRef,
        );
    }
 
    public async loadControllers(
        wrapper: any,
        moduleRef: Module,
    ) {
        const controllers = moduleRef.controllers;
        await this.loadInstance<any>(
            wrapper,
            controllers,
            moduleRef,
        );
    }
 
    /**
     * design:paramtypes создается автоматически объектом reflect
     * для зависимостей, указанных в конструкторе класса.
     * Как видно, если провайдеру нужно разрешить зависимости,
     * то они также должны быть провайдерами.
     * callback, как видно из метода loadInstance, вызывает метод
     * instantiateClass для найденных зависимостей в виде провайдеров.
     */
    public async resolveConstructorParams<T>(
        wrapper: InstanceWrapper<T>,
        moduleRef: Module,
        callback: (args: unknown[]) => void | Promise<void>,
    ) {
        const dependencies = Reflect.getMetadata('design:paramtypes', wrapper.metatype)
   
        const resolveParam = async (param: Function, index: number) => {
          try {
            let providers = moduleRef.providers
            const paramWrapper = providers.get(param.name);
            return paramWrapper?.instance
          } catch (err) {
              throw err;
          }
        };
        const instances = dependencies ? await Promise.all(dependencies.map(resolveParam)) : [];
        await callback(instances);
    }
 
    /**
     * Создает экземпляр зависимости, которая хранится в InstanceLoader,
     * как metatype, с ее зависимостями, которые являются провайдерами,
     * и добавляет этот экземпляр в instance поле класса InstanceLoader,
     * для дальнейшего извлечения при создании роутеров.
     */
    public async instantiateClass<T = any>(
        instances: any[],
        wrapper: InstanceWrapper,
        targetMetatype: InstanceWrapper,
    ): Promise<T> {
        const { metatype } = wrapper;
 
        targetMetatype.instance = instances
            ? new (metatype as Type<any>)(...instances)
            : new (metatype as Type<any>)();
       
        return targetMetatype.instance;
    }
}

Отлично. Теперь у нас есть просканированные модули, и созданные экземпляры зависимостей. Идем дальше.

Сейчас еще раз вернемся к NestFactory, а именно к его методу create.

public async create(module: Module) {
        const applicationConfig = new ApplicationConfig();
        const container = new NestContainer();
        await this.initialize(module, container);
 
        const httpServer = new ExpressAdapter()
        container.setHttpAdapter(httpServer);
        const instance = new NestApplication(
            container,
            httpServer,
            applicationConfig,
        );
       
        return instance;

У нас здесь есть класс ExpressAdapter, который наследуется от класса AbstractHttpAdapter. То есть здесь используется паттерн проектирования известный как адаптер. При желании можно создать и класс FastifyAdapter для использования fastify вместо express. Так и сделано в nest, но здесь мы возьмем express из-за его большей распространенности.

Сначала рассмотрим AbstractHttpAdapter.

./core/adapters/http-adapter.ts

import { HttpServer } from "../../common/interfaces/http-server.interface";
 
export abstract class AbstractHttpAdapter<
  TServer = any,
  TRequest = any,
  TResponse = any
> implements HttpServer<TRequest, TResponse> {
  protected httpServer: TServer | undefined;
 
  constructor(protected readonly instance: any) {}
 
  public use(...args: any[]) {
    return this.instance.use(...args);
  }
 
  public get(...args: any[]) {
    return this.instance.get(...args);
  }
 
  public post(...args: any[]) {
    return this.instance.post(...args);
  }
 
  public listen(port: any) {
    return this.instance.listen(port);
  }
 
  public getHttpServer(): TServer {
    return this.httpServer as TServer;
  }
 
  public setHttpServer(httpServer: TServer) {
    this.httpServer = httpServer;
  }
 
  public getInstance<T = any>(): T {
    return this.instance as T;
  }
 
  abstract initHttpServer(): any;
  abstract reply(response: any, body: any, statusCode?: number): any;
  abstract registerBodyParser(prefix?: string): any;
}

Видим, что он реализует несколько обычных методов http сервера. Для упрощения кода, у нашего nest будет только два http метода, а именно post и get.

А это интерфейс, который реализует адаптер

interface HttpServer<TRequest = any, TResponse = any> {
    reply(response: any, body: any, statusCode?: number): any;
    get(handler: RequestHandler<TRequest, TResponse>): any;
    get(path: string, handler: RequestHandler<TRequest, TResponse>): any;
    post(handler: RequestHandler<TRequest, TResponse>): any;
    post(path: string, handler: RequestHandler<TRequest, TResponse>): any;
    listen(port: number | string): any;
 
    getInstance(): any;
    getHttpServer(): any;
    initHttpServer(): void;
    registerBodyParser(): void
}

Теперь посмотрим на класс ExpressAdapter

./platform-express/express.adapter.ts

import { AbstractHttpAdapter } from '../core/adapters';
import { isNil, isObject } from '../common/utils/shared.utils'
import express from 'express';
import * as http from 'http';
import {
  json as bodyParserJson,
  urlencoded as bodyParserUrlencoded,
} from 'body-parser';
 
export class ExpressAdapter extends AbstractHttpAdapter {
 
    constructor() {
      super(express());
    }
 
    /**
     * Является response методом. С помощью него отправляются все данные.
     */
    public reply(response: any, body: any) {
        if (isNil(body)) {
          return response.send();
        }
   
        return isObject(body) ? response.json(body) : response.send(String(body));
    }
 
    /**
     * Запускает сервер на выборном порте
     */
    public listen(port: any) {
        return this.httpServer.listen(port);
    }
 
 
    public registerBodyParser() {
      const parserMiddleware = {
        jsonParser: bodyParserJson(),
        urlencodedParser: bodyParserUrlencoded({ extended: true }),
      };
      Object.keys(parserMiddleware)
        .forEach((parserKey: any) => this.use((parserMiddleware as any)[parserKey]));
      }
 
    public initHttpServer() {
        this.httpServer = http.createServer(this.getInstance());
    }
}

Собственно здесь реализуется запуск и настройка express. В конструкторе, в методе super, экземпляр express передается в AbstractHttpAdapter, из которого и будут вызываться методы post, get и use.

Теперь, снова возвращаясь к NestFactory,

public async create(module: Module) {
        const container = new NestContainer();
        await this.initialize(module, container);
 
        const httpServer = new ExpressAdapter()
        container.setHttpAdapter(httpServer);
        const instance = new NestApplication(
            container,
            httpServer,
        );
       
        return instance;
    }

нам нужно реализовать класс NestApplication, который является экземпляром всего приложения Nest. Именно из него вызывается метод listen,

async function bootstrap() {
    const app = await NestFactory.create(AppModule);
    await app.listen(3000);
}

который запускает приложение.

./core/nest-application.ts

import { HttpServer } from '../common/interfaces/http-server.interface';
import { Resolver } from './router/interfaces/resolver.interface';
import { addLeadingSlash } from '../common/utils/shared.utils';
import { NestContainer } from './injector/container';
import { RoutesResolver } from './router/routes-resolver';
 
export class NestApplication {
    private readonly routesResolver: Resolver;
    public httpServer: any;
 
    constructor(
        private readonly container: NestContainer,
        private readonly httpAdapter: HttpServer,
    ) {
        this.registerHttpServer();
 
        this.routesResolver = new RoutesResolver(
            this.container,
        );
    }
 
    public registerHttpServer() {
        this.httpServer = this.createServer();
    }
 
    /**
     * Начинает процесс инициализации выбранного http сервера
     */
    public createServer<T = any>(): T {
        this.httpAdapter.initHttpServer();
        return this.httpAdapter.getHttpServer() as T;
    }
 
    public async init(): Promise<this> {
        this.httpAdapter.registerBodyParser();
        await this.registerRouter();
        return this;
    }
 
    /**
     * Метод, с помощью которого запускается приложение Nest.
     * Он запускает процесс инициализации http сервера, регистрации
     * созданных роутеров, и запуска сервера на выбранном порте.
     */
    public async listen(port: number | string) {
        await this.init();
        this.httpAdapter.listen(port);
        return this.httpServer;
    }
 
    /**
     * Метод, который запускает регистрацию роутеров,
     * которые были созданы с помощью декораторов http методов,
     * таких как post и get.
     */
    public async registerRouter() {
        const prefix = ''
        const basePath = addLeadingSlash(prefix);
        this.routesResolver.resolve(this.httpAdapter, basePath);
    }
}

И это подводит нас к роутерам, а именно к классу RoutesResolver.

./core/router/routes-resolver.ts

import { NestContainer } from '../injector/container';
import { Resolver } from '../router/interfaces/resolver.interface';
import { MODULE_PATH } from '../../common/constants';
import { HttpServer } from '../../common/interfaces/http-server.interface';
import { InstanceWrapper } from '../injector/instance-wrapper';
import { RouterExplorer } from './router-explorer';
 
export class RoutesResolver implements Resolver {
    private readonly routerExplorer: RouterExplorer;
 
    constructor(
        private readonly container: NestContainer,
      ) {
        this.routerExplorer = new RouterExplorer(
            this.container,
        );
    }
 
    /**
     * Для каждого модуля сначала находит базовый путь, который
     * указывается в декораторе Module,
     * и передает его и контроллеры в метод registerRouters
     */
    public resolve(applicationRef: any, basePath: string): void {
        const modules = this.container.getModules();
        modules.forEach(({ controllers, metatype }) => {
            let path = metatype ? this.getModulePathMetadata(metatype) : undefined;
            path = path ? basePath + path : basePath;
            this.registerRouters(controllers, metatype.name, path, applicationRef);
        });
    }
 
    /**
     * Для каждого контроллера в модуле, запускает метод explore
     * класса routerExplorer, который отвечает за всю логику
     * регистрации роутеров
     */
    public registerRouters(
        routes: Map<string, InstanceWrapper<any>>,
        moduleName: string,
        basePath: string,
        applicationRef: HttpServer,
      ) {
        routes.forEach(instanceWrapper => {
          const { metatype } = instanceWrapper;
   
          // Находит путь для декоратора контроллера, например @Controller('cats')
          const paths = this.routerExplorer.extractRouterPath(
            metatype as any,
            basePath,
          );
   
          // Если путь был передан как @Controllers('cats'), то будет вызвано один раз.
          // Дело в том, что reflect возвращает массив
          paths.forEach(path => {
            this.routerExplorer.explore(
              instanceWrapper,
              moduleName,
              applicationRef,
              path,
            );
          });
        });
      }
 
    private getModulePathMetadata(metatype: object): string | undefined {
        return Reflect.getMetadata(MODULE_PATH, metatype);
    }
}

Код выше делает так, чтобы для каждого контроллера был вызван метод explore класса RouterExplorer. Класс RouterExplorer реализует основную логику регистрации роутеров. Он создает http методы, добавляет контроллеры в качестве их callbacks, привязывает эти контроллеры к пространству модуля, в котором он находится, и реализует функциональность ответов и их обработки для запросов.

./core/router/routes-explorer.ts

import { NestContainer } from '../injector/container';
import { RouterProxyCallback } from './router-proxy';
import { addLeadingSlash } from '../../common/utils/shared.utils';
import { Type } from '../../common/interfaces/type.interface';
import { Controller } from '../../common/interfaces/controller.interface';
import { PATH_METADATA, METHOD_METADATA, ROUTE_ARGS_METADATA, PARAMTYPES_METADATA } from '../../common/constants';
import { RequestMethod } from '../../common/enums/request-method.enum';
import { HttpServer } from '../../common/interfaces/http-server.interface';
import { InstanceWrapper } from '../injector/instance-wrapper';
import { RouterMethodFactory } from '../helpers/router-method-factory';
import {
  isConstructor,
  isFunction,
  isString,
} from '../../common/utils/shared.utils';
import { RouteParamtypes } from '../../common/enums/route-paramtypes.enum';
 
 
export interface RoutePathProperties {
    path: string[];
    requestMethod: RequestMethod;
    targetCallback: RouterProxyCallback;
    methodName: string;
  }
 
export class RouterExplorer {
    private readonly routerMethodFactory = new RouterMethodFactory();
 
    constructor (
        private readonly container: NestContainer,
    ) {
    }    
 
    public explore<T extends HttpServer = any>(
        instanceWrapper: InstanceWrapper,
        module: string,
        router: T,
        basePath: string,
      ) {
        const { instance } = instanceWrapper;
        const routePaths: RoutePathProperties[] = this.scanForPaths(instance);
       
        // Для каждого метода контроллера запускает регистрацию роутеров
        (routePaths || []).forEach((pathProperties: any) => {
            this.applyCallbackToRouter(
              router,
              pathProperties,
              instanceWrapper,
              module,
              basePath,
            );
        })
    }
 
    /**
     * Метод, который сканирует контроллер, и находит у него методы
     * запроса с определенными путями, например метод, на который
     * навешен декоратор @post('add_to_database').
     * В таком случае эта функция возвращает массив методов контроллера
     * с путями, телами этих методов, методом request и именами, которые
     * получаются в методе exploreMethodMetadata
     */
    public scanForPaths(
        instance: Controller,
      ): RoutePathProperties[] {
        const instancePrototype = Object.getPrototypeOf(instance);
        let methodNames = Object.getOwnPropertyNames(instancePrototype);
 
        const isMethod = (prop: string) => {
          const descriptor = Object.getOwnPropertyDescriptor(instancePrototype, prop);
          if (descriptor?.set || descriptor?.get) {
            return false;
          }
          return !isConstructor(prop) && isFunction(instancePrototype[prop]);
        };
   
        return methodNames.filter(isMethod).map(method => this.exploreMethodMetadata(instance, instancePrototype, method))
    }
 
    /**
     * Для определенного метода контроллера возвращает его свойства,
     * для метода scanForPaths
     */
    public exploreMethodMetadata(
      instance: Controller,
      prototype: object,
      methodName: string,
    ): RoutePathProperties {
      const instanceCallback = (instance as any)[methodName];
      const prototypeCallback = (prototype as any)[methodName];
      const routePath = Reflect.getMetadata(PATH_METADATA, prototypeCallback);
 
      const requestMethod: RequestMethod = Reflect.getMetadata(
        METHOD_METADATA,
        prototypeCallback,
      );
      const path = isString(routePath)
        ? [addLeadingSlash(routePath)]
        : routePath.map((p: string) => addLeadingSlash(p));
      return {
        path,
        requestMethod,
        targetCallback: instanceCallback,
        methodName,
      };
    }
 
    private applyCallbackToRouter<T extends HttpServer>(
        router: T,
        pathProperties: RoutePathProperties,
        instanceWrapper: InstanceWrapper,
        moduleKey: string,
        basePath: string,
      ) {
        const {
          path: paths,
          requestMethod,
          targetCallback,
          methodName,
        } = pathProperties;
        const { instance } = instanceWrapper;
        // Получает определенный http метод
        const routerMethod = this.routerMethodFactory
          .get(router, requestMethod)
          .bind(router);
   
        // Создает callback для определенного метода
        const handler = this.createCallbackProxy(
          instance,
          targetCallback,
          methodName,
        );
   
        // Если декоратор используется как @Post('add_to_database'),
        // то будет вызвано один раз для этого пути.
        paths.forEach(path => {
          const fullPath = this.stripEndSlash(basePath) + path;
          // Региструет http метод. Сопоставляет путь метода, и его callback,
          // полученный из контроллера. Ответ же производится reply методом,
          // реализованным в классе ExpressAdapter
          routerMethod(this.stripEndSlash(fullPath) || '/', handler);
        });
    }
 
    public stripEndSlash(str: string) {
      return str[str.length - 1] === '/' ? str.slice(0, str.length - 1) : str;
    }
 
    public createCallbackProxy(
      instance: Controller,
      callback: (...args: any[]) => unknown,
      methodName: string,
    ) {
      // Достает ключи данных запроса указанных ранее в декораторах @Body() и @Param()
      const metadata = Reflect.getMetadata(ROUTE_ARGS_METADATA, instance.constructor, methodName) || {};
      const keys = Object.keys(metadata);
      const argsLength = Math.max(...keys.map(key => metadata[key].index)) + 1
     
      // Извлеченные данные из request, такие как тело и параметры запроса.
      const paramsOptions = this.exchangeKeysForValues(keys, metadata);
 
      const fnApplyParams = this.resolveParamsOptions(paramsOptions)
      const handler = <TRequest, TResponse>(
        args: any[],
        req: TRequest,
        res: TResponse,
        next: Function,
      ) => async () => {
        // так как args это объект, а не примитивная переменная,
        // то он передается по ссылке, а не по значению,
        // поэтому он изменяется, и после вызова fnApplyParams,
        // в args хранятся аргументы, полученные из request
        fnApplyParams && (await fnApplyParams(args, req, res, next));
        // Здесь мы привязываем один из методов контроллера,
        // например, добавление данных в базу данных, и аргументы из request,
        // и теперь он может ими управлять, как и задумано
        return callback.apply(instance, args);
      };
      const targetCallback = async <TRequest, TResponse>(
          req: TRequest,
          res: TResponse,
          next: Function,
        ) => {
          // Заполняется undefined для дальнейшего изменения реальными данными
          // из request
          const args = Array.apply(null, { argsLength } as any).fill(undefined);
          // result это экземпляр контроллера с пространством данных аргументов
          // из request
          const result = await handler(args, req, res, next)()
          const applicationRef = this.container.getHttpAdapterRef()
          if(!applicationRef) {
            throw new Error(`Http server not created`)
          }
          return await applicationRef.reply(res, result);
        }
      return async <TRequest, TResponse>(
        req: TRequest,
        res: TResponse,
        next: () => void,
      ) => {
        try {
          await targetCallback(req, res, next);
        } catch (e) {
          throw e
        }
      };
    }
 
    /**
     * extractValue здесь это метод exchangeKeyForValue.
     * И ему передается request, для извлечения данных запроса
     */
    public resolveParamsOptions(paramsOptions: any) {
      const resolveFn = async (args: any, req: any, res: any, next: any) => {
        const resolveParamValue = async (param: any) => {
          const { index, extractValue } = param;
          const value = extractValue(req, res, next);
          args[index] = value
        }
        await Promise.all(paramsOptions.map(resolveParamValue));
      }
      return paramsOptions && paramsOptions.length ? resolveFn : null;
    }
 
    /**
     * Перебирает ключи данных запроса для вызова для каждого
     * метода exchangeKeyForValue, который достанет соответствующие данные,
     * которые были определены ранее в декораторах @Body() и @Param(),
     * из request.
     */
    public exchangeKeysForValues(
      keys: string[],
      metadata: Record<number, any>,
    ): any[] {
      return keys.map((key: any) => {
        const { index, data } = metadata[key];
        const numericType = Number(key.split(':')[0]);
        const extractValue = <TRequest, TResponse>(
          req: TRequest,
          res: TResponse,
          next: Function,
        ) =>
          this.exchangeKeyForValue(numericType, data, {
            req,
            res,
            next,
        });
        return { index, extractValue, type: numericType, data }
      })
    }
 
    /**
     * Проверяет чему соответствует ключ данных, телу или параметрам запроса.
     * Это определяется в соответствующих декораторах @Body() и @Param().
     * И теперь, когда запрос на соответствующий api выполнен, мы пытаемся
     * достать их из request, если они были переданы.
     */
    public exchangeKeyForValue<
      TRequest extends Record<string, any> = any,
      TResponse = any,
      TResult = any
    >(
      key: RouteParamtypes | string,
      data: string | object | any,
      { req, res, next }: { req: TRequest; res: TResponse; next: Function },
    ): TResult | null {
      switch (key) {
        case RouteParamtypes.BODY:
          return data && req.body ? req.body[data] : req.body;
        case RouteParamtypes.PARAM:
          return data ? req.params[data] : req.params;
        default:
          return null;
      }
    }
 
    public extractRouterPath(metatype: Type<Controller>, prefix = ''): string[] {
        let path = Reflect.getMetadata(PATH_METADATA, metatype);
   
        if (Array.isArray(path)) {
          path = path.map(p => prefix + addLeadingSlash(p));
        } else {
          path = [prefix + addLeadingSlash(path)];
        }
   
        return path.map((p: string) => addLeadingSlash(p));
    }
}

Что к этому стоит добавить, так это то, что в методе applyCallbackToRouter для получения http метода используется класс RouterMethodFactory, который, на самом деле, имеет всего один метод

./core/helpers/router-method-factory.ts

import { HttpServer } from '../../common/interfaces/http-server.interface';
import { RequestMethod } from '../../common/enums/request-method.enum';
 
export class RouterMethodFactory {
  public get(target: HttpServer, requestMethod: RequestMethod): Function {
    switch (requestMethod) {
      case RequestMethod.POST:
        return target.post;
      default: {
        return target.get;
      }
    }
  }
}

Что ж. Если вы еще здесь, поздравляю! Мы написали все ядро нашего мини Nest фреймворка. Теперь, все, что осталось, это написать декораторы, на которых мы и пишем Nest приложение в качестве пользователей.

Начнем с декоратора @Module(), и сперва посмотрим на пример его использования из документации

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
 
@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

Мы видим, что метаданные указываются как параметры декоратора, теперь реализуем его.

./common/decorators/module.decorator.ts

import { MODULE_METADATA as metadataConstants } from '../constants';
 
const metadataKeys = [
  metadataConstants.IMPORTS,
  metadataConstants.EXPORTS,
  metadataConstants.CONTROLLERS,
  metadataConstants.PROVIDERS,
];
 
/**
 * Проверяет, чтобы были указаны только правильные массивы,
 * соответствующие metadataKeys
 */
export function validateModuleKeys(keys: string[]) {
  const validateKey = (key: string) => {
    if (metadataKeys.includes(key)) {
      return;
    }
    throw new Error(`NOT INVALID KEY: ${key}`);
  };
  keys.forEach(validateKey);
}
 
/**
 * Сохраняет зависимости в объект Reflect.
 * Где property название одной из зависимости,
 * например controllers. Именно благодаря этому,
 * у нас есть возможность извлекать данные после.
 */
export function Module(metadata: any): ClassDecorator {
  const propsKeys = Object.keys(metadata);
  validateModuleKeys(propsKeys);
 
  return (target: Function) => {
    for (const property in metadata) {
      if (metadata.hasOwnProperty(property)) {
        Reflect.defineMetadata(property, (metadata as any)[property], target);
      }
    }
  };
}

Довольно не сложно, не так ли? Действительно, декораторы одна из довольно простых частей Nest.

Теперь рассмотрим декоратор @Controller(), который, все, что делает, это сохраняет базовый путь контроллера, ведь сам контроллер уже сохранен в Reflect по модулю, в котором он используется.

./common/decorators/controller.decorator.ts

import { PATH_METADATA } from "../constants";
import { isUndefined } from "../utils/shared.utils";
 
export function Controller(
  prefix?: string,
): ClassDecorator {
  const defaultPath = '/';
 
  const path = isUndefined(prefix) ? defaultPath : prefix
 
  return (target: object) => {
    Reflect.defineMetadata(PATH_METADATA, path, target);
  };
}

Помните про декоратор @Injectable(), который якобы помечает класс как провайдер? Как уже написано выше, он лишь устанавливает время жизни провайдера. Класс помечается как провайдер, только если он передается в массив providers соответствующего модуля. И хоть мы не реализовали возможность изменения времени жизни для провайдера, но для полноты, все равно рассмотрим этот декоратор.

./common/decorators/injectable.decorator.ts

import { SCOPE_OPTIONS_METADATA } from '../constants';
 
export enum Scope {
    DEFAULT,
    TRANSIENT,
    REQUEST,
}
 
export interface ScopeOptions {
    scope?: Scope;
}
 
export type InjectableOptions = ScopeOptions;
 
export function Injectable(options?: InjectableOptions): ClassDecorator {
    return (target: object) => {
      Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, options, target);
    };
}

Теперь у нас осталось всего четыре декоратора для реализации, для данных запроса, а именно @Body() и @Param(), и для http методов, @Post() и @Get().

Сперва рассмотрим первые два.

./common/decorators/route-params.decorator.ts

import { ROUTE_ARGS_METADATA } from "../constants";
import { RouteParamtypes } from "../enums/route-paramtypes.enum";
import { isNil, isString } from "../utils/shared.utils";
 
/**
 * Здесь используется неизменяемость данных, для того, чтобы
 * использовать один метод для нескольких типов запроса.
 */
const createPipesRouteParamDecorator = (paramtype: RouteParamtypes) => (
    data?: any,
  ): ParameterDecorator => (target, key, index) => {
    const hasParamData = isNil(data) || isString(data);
    const paramData = hasParamData ? data : undefined;
    const args =
      Reflect.getMetadata(ROUTE_ARGS_METADATA, target.constructor, key) || {};
 
    // Где paramtype это body или param, а index его
    // положение в параметрах функции, где находится декоратор,
    // для правильного присвоения после получения из request
    Reflect.defineMetadata(
      ROUTE_ARGS_METADATA,
      {
        ...args,
        [`${paramtype}:${index}`]: {
          index,
          data: paramData,
        },
      },
      target.constructor,
      key,
    );
};
 
export function Body(
    property?: string,
  ): ParameterDecorator {
    return createPipesRouteParamDecorator(RouteParamtypes.BODY)(
      property,
    );
}
 
export function Param(
    property?: string,
  ): ParameterDecorator {
    return createPipesRouteParamDecorator(RouteParamtypes.PARAM)(
      property,
    );
}

И последнее, декораторы post и get, которые сохраняют в объект Reflect для определенных методов контроллеров их пути и методы запроса.

./common/decorators/request-mapping.decorator.ts

import { METHOD_METADATA, PATH_METADATA } from '../constants';
import { RequestMethod } from '../enums/request-method.enum';
 
export interface RequestMappingMetadata {
  path?: string | string[];
  method?: RequestMethod;
}
 
const defaultMetadata = {
  [PATH_METADATA]: '/',
  [METHOD_METADATA]: RequestMethod.GET,
};
 
export const RequestMapping = (
  metadata: RequestMappingMetadata = defaultMetadata,
): MethodDecorator => {
  const pathMetadata = metadata[PATH_METADATA];
  const path = pathMetadata && pathMetadata.length ? pathMetadata : '/';
  const requestMethod = metadata[METHOD_METADATA] || RequestMethod.GET;
 
  return (
    target: object,
    key: string | symbol,
    descriptor: TypedPropertyDescriptor<any>,
  ) => {
    Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
    Reflect.defineMetadata(METHOD_METADATA, requestMethod, descriptor.value);
    return descriptor;
  };
};
 
const createMappingDecorator = (method: RequestMethod) => (
  path?: string | string[],
): MethodDecorator => {
  return RequestMapping({
    [PATH_METADATA]: path,
    [METHOD_METADATA]: method,
  });
};
 
/**
 * Обработчик маршрута (метод) Decorator. Направляет запросы HTTP POST по указанному пути.
 *
 * @publicApi
 */
export const Post = createMappingDecorator(RequestMethod.POST);
 
/**
 * Обработчик маршрута (метод) Decorator. Направляет запросы HTTP GET по указанному пути.
 *
 * @publicApi
 */
export const Get = createMappingDecorator(RequestMethod.GET);

Хорошая работа, наш мини Nest готов! 

Теперь мы можем создать директорию project-view на уровне других директорий nest, и написать простое приложение

./project-view/main.ts

import { NestFactory } from '../core';
import { AppModule } from './app.module';
 
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

./project-view/app.controller.ts

import { Controller, Get, Post, Body, Param } from '../common';
import { AppService } from './app.service';
 
@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}
 
  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
 
  @Post('body/:id')
  recieveBody(@Body() data: any, @Param('id') id: string) {
    return 'body: ' + data.data + ' has been received and id: ${id}';
  }
}

./project-view/app.service.ts

import { Injectable } from '../common';
 
@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

./project-view/app.module.ts

import { Module } from '../common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
 
@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

После чего инициализировать typescript проект, создав tsconfig.json файл с помощью команды

tsc --init

и настроить его как-то вот так

{
  "compilerOptions": {
    "target": "es2017",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "module": "commonjs",
    "outDir": "./",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
  },
  "include": ["packages/**/*", "integration/**/*", "./core/", "./common/", "./project-view/", "./platform-express/"],
  "exclude": ["node_modules", "**/*.spec.ts"]
}

Теперь мы можем скомпилировать typescript в js с помощью следующей команды

tsc --build

перейти в директорию нашего пользовательского приложения

cd project-view

и запустить скомпилированный входной файл

node main.js

Вы можете проверить результат, например, через postman, и поиграться с body и params для post запроса.

Теперь вы знаете Nest чуть лучше :)

Tags:
Hubs:
Total votes 7: ↑6 and ↓1+8
Comments0

Articles