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

от автора

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

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 чуть лучше 🙂

Гитхаб репозиторий со всем кодом

Контакты для связи:

Почта — keith.la.00@gmail.com

Telegram — @NLavrenov00


ссылка на оригинал статьи https://habr.com/ru/post/651139/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *