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

Kate

Administrator
Команда форума
Цитата из документации:

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



 
Сверху