Рубрики
Без рубрики

Бэкэнд -сервер узла – производитель youtube GIF с использованием Next.js, Node и Rabbitmq

Привет всем, эта статья является второй частью сериала YouTube GIF Maker с использованием next.js, узел А … Tagged с WebDev, Node, React, JavaScript.

Привет всем, эта статья является второй частью сериала YouTube GIF Maker с использованием Next.js, Node и Rabbitmq.

В этой статье мы погрузимся в построение бэкэнд -сервера нашего YouTube для конвертера GIF. Эта статья будет содержать некоторые фрагменты кода, но весь проект можно получить на GitHub который содержит полный исходный код, а также дополнительные интеграционные тесты и API Swagger Docs. Вы также можете просмотреть приложение демо Анкет Здесь будут рассмотрены следующие темы

  • Функциональные возможности
  • Архитектура проекта
  • Реализация
    • Схема базы данных
    • Обработка маршрутов
    • Контроллер
    • Услуги
      • Служба работы
      • Rabbitmq обслуживание

Функциональные возможности

Как видно на приведенной выше диаграмме последовательностей, сервер бэкэнд имеет 3 основных функциональных возможностей, которые являются:

  • Обработка запросов на преобразование GIF, создав новую запись работы в базе данных
  • Отправка событий в RabbitMQ, указывая на то, что была создана новая работа по конверсии (очередь задач)
  • Обработка запросов на получение заданий путем запроса задания по его идентификатору из базы данных и возврата соответствующего ответа.

Архитектура проекта

Наша архитектура приложения Express содержит три основных компонента

  • Обработчик маршрута
  • Контроллер
  • обслуживание

У каждого из них есть определенные функциональные возможности, которые мы будем разговоры в некоторых деталях, а также объясняют, почему его структурировано таким образом

  • Обработчик маршрута
    • Отвечает за маршрутизацию пути к их обработчикам маршрутов. Как правило, эти обработчики маршрута состоят из массива обработчиков, которые мы называем «цепочкой промежуточной программы», окончательным обработчиком в этой цепи является контроллер маршрута
    • Цепочка промежуточного программного обеспечения обычно отвечает за «проверки» по входящему запросу, а также в некоторых случаях изменение объекта запроса. В нашем случае мы будем выполнять валидацию, используя индивидуальное промежуточное программное обеспечение.
  • Контроллер
    • Извлечение данных из запроса, а также, если это необходимо, а также дезинфицировать эти данные
    • Делегирование управления соответствующей службе
    • Обрабатывать ответы
    • Делегирование ошибок в пользовательскую обработку ошибок промежуточного программного обеспечения
  • обслуживание
    • Имеет всю бизнес -логику
    • Доступ к данным с помощью уровня доступа данных (ORM/ODM)

Контроллеры должен быть тупой Это означает, что они не должны иметь никаких подробностей о бизнес -логике, все, что они знают, это «какая служба может обрабатывать этот запрос», «какие данные нуждаются в этой службе», «как должен выглядеть ответ». Это избегает иметь Жирные контроллеры

Реализация

Схема базы данных

В этом проекте мы используем Typeorm который представляет собой готовый ОРМ, который поддерживает многие базы данных (мы будем использовать MongoDB, как упомянуто в первой части серии).

Мы собираемся представлять каждое преобразование GIF как работу, которая будет нашей единственной коллекцией. Коллекция вакансий в Typemorm выглядит так

import { BaseEntity, Entity, ObjectID, Column, CreateDateColumn, UpdateDateColumn, ObjectIdColumn } from 'typeorm';

@Entity('jobs')
export class Job extends BaseEntity {
  @ObjectIdColumn()
  id: ObjectID;

  @Column({
    nullable: false,
  })
  youtubeUrl: string;

  @Column({
    nullable: false,
  })
  youtubeId: string;

  @Column({
    nullable: true,
  })
  gifUrl: string;

  @Column({
    nullable: false,
  })
  startTime: number;

  @Column({
    nullable: false,
  })
  endTime: number;

  @Column({
    type: 'enum',
    enum: ['pending', 'processing', 'done', 'error'],
  })
  status: 'pending' | 'processing' | 'done' | 'error';

  @Column()
  @CreateDateColumn()
  createdAt: Date;

  @Column()
  @UpdateDateColumn()
  updatedAt: Date;
}

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

Обработка маршрутов

Как упоминалось ранее, у нас будет всего два маршрута.

  • Маршрут для создания новой работы по конверсии GIF
  • Маршрут для получения данных о задании преобразования из своего идентификатора, который будет использоваться для опроса позже клиентской стороной

Так выглядит наш обработчик маршрута

//routes.interface
import { Router } from 'express';

interface Route {
  path?: string;
  router: Router;
}

export default Route;
//jobs.route.ts
import { Router } from 'express';
import { CreateJobDto } from '../../common/dtos/createJob.dto';
import Route from '../../common/interfaces/routes.interface';
import JobsController from '../../controllers/jobs.controller';
import validationMiddleware from '../middlewares/validation.middleware';

class JobsRoute implements Route {
  public path = '/jobs';
  public router = Router();

  constructor(private jobsController = new JobsController()) {
    this.initializeRoutes();
  }

  private initializeRoutes() {
    this.router.get(`${this.path}/:id`, this.jobsController.getJobById);
    this.router.post(`${this.path}`, validationMiddleware(CreateJobDto, 'body'), this.jobsController.createJob);
  }
}

export default JobsRoute;

Для проверки мы используем индивидуальное промежуточное программное обеспечение, которое проверяет DTO с использованием класс-владелец и класс-трансформатор

//createJob.dto
import { Expose } from 'class-transformer';
import { IsNotEmpty, IsNumber, IsString, Matches } from 'class-validator';
import { IsGreaterThan } from './validators/isGreaterThan';
import { MaximumDifference } from './validators/maximumDifference';

export class CreateJobDto {
  @IsNotEmpty()
  @IsString()
  @Matches(/^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/, {
    message: 'Invalid youtube url',
  })
  @Expose()
  public youtubeUrl: string;

  @IsNotEmpty()
  @IsNumber()
  @Expose()
  public startTime: number;

  @IsNotEmpty()
  @IsNumber()
  @IsGreaterThan('startTime', {
    message: 'end time must be greater than start time',
  })
  @MaximumDifference('startTime', {
    message: 'maximum gif duration is 30 seconds',
  })
  @Expose()
  public endTime: number;
}

Обратите внимание, что isgreaterthan и максимальная дифференциация являются пользовательскими декораторами валидации классов. По сути, они выглядят так (дополнительную информацию об этом можно найти в документах class-validator )

//isGreaterThan.ts
import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator';

export function IsGreaterThan(property: string, validationOptions?: ValidationOptions) {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      name: 'isGreaterThan',
      target: object.constructor,
      propertyName: propertyName,
      constraints: [property],
      options: validationOptions,
      validator: {
        validate(value: any, args: ValidationArguments) {
          const [relatedPropertyName] = args.constraints;
          const relatedValue = (args.object as any)[relatedPropertyName];
          return typeof value === 'number' && typeof relatedValue === 'number' && value > relatedValue; 
        },
      },
    });
  };
}

Максимальная диффференция выглядит похоже на это, но его возврат выглядит так, вместо этого

return typeof value === 'number' && typeof relatedValue === 'number' && value - relatedValue <= difference; 

И теперь наше промежуточное программное обеспечение выглядит так

validation.middleware.ts
import { plainToClass } from 'class-transformer';
import { validate, ValidationError } from 'class-validator';
import { RequestHandler } from 'express';

const validationMiddleware = (type: any, value: string | 'body' | 'query' | 'params' = 'body', skipMissingProperties = false): RequestHandler => {
  return (req, res, next) => {
    validate(plainToClass(type, req[value]), { skipMissingProperties }).then((errors: ValidationError[]) => {
      if (errors.length > 0) {
        const message = errors.map((error: ValidationError) => Object.values(error.constraints)).join(', ');
        res.status(400).send(message);
      } else {
        next();
      }
    });
  };
};

export default validationMiddleware;

Контроллер

Наш контроллер выглядит довольно стандартно, единственными выводами извлекают объект CreateJobdto из тела, используя Plaintoclass из класса Transformer с ExcudeExtrogyValues: True, который разрушает только обнаженные поля (с декоратором @Expose () в классе CreateJobdto) об этом в этом в этом в этом в этом Класс-трансформатор документы

//jobs.controllers.ts
import { plainToClass } from 'class-transformer';
import { NextFunction, Request, Response } from 'express';
import { CreateJobDto } from '../common/dtos/createJob.dto';
import { Job } from '../entities/jobs.entity';
import JobsService from '../services/jobs.service';

class JobsController {
  constructor(private jobService = new JobsService()) {}

  public createJob = async (req: Request, res: Response, next: NextFunction): Promise => {
    try {
      const jobDto: CreateJobDto = plainToClass(CreateJobDto, req.body, { excludeExtraneousValues: true });
      const createdJob: Job = await this.jobService.createJob(jobDto);

      res.status(201).json(createdJob);
    } catch (error) {
      next(error);
    }
  };

  public getJobById = async (req: Request, res: Response, next: NextFunction): Promise => {
    try {
      const jobId = req.params.id;
      const job: Job = await this.jobService.findJobById(jobId);

      const responseStatus = job.status === 'done' ? 200 : 202;
      res.status(responseStatus).json(job);
    } catch (error) {
      next(error);
    }
  };
}

export default JobsController;

Также стоит отметить, что код состояния ответа [get]/job/{id} составляет 202, когда задание преобразования все еще находится в обработке. См. Асинхронную шаблон запроса-ответа для получения дополнительной информации об этом

В случае ошибки ошибка передается в промежуточную программу ошибки, которая является последним промежуточным программным обеспечением в нашей цепочке промежуточного программного обеспечения Express, и она выглядит так:

//error.middleware.ts
import { NextFunction, Request, Response } from 'express';

import { isBoom, Boom } from '@hapi/boom';
import { logger } from '../../common/utils/logger';

function errorMiddleware(error: Boom | Error, req: Request, res: Response, next: NextFunction) {
  const statusCode: number = isBoom(error) ? error.output.statusCode : 500;
  const errorMessage: string = isBoom(error) ? error.message : 'Something went wrong';
  logger.error(`StatusCode : ${statusCode}, Message : ${error}`);

  return res.status(statusCode).send(errorMessage);
}
export default errorMiddleware;

Вы можете заметить, что мы импортировали пакет под названием Boom, мы поговорим об этом позже в разделе услуг

Услуги

Служба работы

Jobservice имеет всю бизнес -логику и доступ к уровню доступа к данным, а также общение с услугой RabbitMQ для отправки событий в очередь

//jobs.service.ts
import * as Boom from '@hapi/boom';
import Container from 'typedi';
import { CreateJobDto } from '../common/dtos/createJob.dto';
import EventEmitter from '../common/utils/eventEmitter';
import { Job } from '../entities/jobs.entity';
import RabbitMQService from './rabbitmq.service';

class JobsService {
  private events = {
    JobCreated: 'JobCreated',
  };

  constructor() {
    this.intiializeEvents();
  }

  private intiializeEvents() {
    EventEmitter.on(this.events.JobCreated, (job: Job) => {
      const rabbitMQInstance = Container.get(RabbitMQService);
      rabbitMQInstance.sendToQueue(JSON.stringify(job));
    });
  }

  public async findJobById(jobId: string): Promise {
    const job: Job = await Job.findOne(jobId);
    if (!job) throw Boom.notFound();

    return job;
  }

  public async createJob(jobDto: CreateJobDto): Promise {
    const createdJob: Job = await Job.save({ ...jobDto, youtubeId: jobDto.youtubeUrl.split('v=')[1]?.slice(0, 11), status: 'pending' } as Job);
    EventEmitter.emit(this.events.JobCreated, createdJob);
    return createdJob;
  }
}

export default JobsService;

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

  • Бум
    • Используется для создания HTTP -объектов с мощным, простым и дружелюбным интерфейсом. Вы можете увидеть, насколько легко он бросил 404, не найденной объект ошибки
  • Типеди
    • Typedi – это мощный пакет впрыска зависимостей, который имеет много функций. Одной из этих функций является наличие синглтонских услуг, которые мы используем в нашем случае.

Теперь давайте подробнее рассмотрим некоторые функции в классе

Intiializeevents ()

В этой функции используется глобальный EventEmitter, который мы используем по всему миру по всему проекту, чтобы добавить паб/суб -слой. это так же просто, как

//eventEmitter.ts
import { EventEmitter } from 'events';
export default new EventEmitter();

И теперь мы можем начать слушать события, в частности, событие, которое мы излучаем позже, создавая новую работу под названием «Заработанный»

  // Defines all the events in our service
  private events = {
    JobCreated: 'JobCreated',
  };

  private intiializeEvents() {
    // Start listening for the event 'JobCreated'
    EventEmitter.on(this.events.JobCreated, (job: Job) => {
    // Get a singleton instance of our RabbitMQService
      const rabbitMQInstance = Container.get(RabbitMQService);
    // Dispatch an event containing the data of the created job
      rabbitMQInstance.sendToQueue(JSON.stringify(job));
    });
  }

См. Дополнительную информацию о добавлении паба/суб -слоя в ваш Express Backend

createJob ()

Эта функция делает ровно две вещи.

  • Создание нового документа о работе в базе данных
  • Расследование события «Работаем», которое была создана новой работой, таким образом, слушатель событий справится с логикой отправки этого события в службу Rabbitmq

Rabbitmq обслуживание

Эта служба отвечает за подключение к серверу RabbitMQ, создание канала и инициализацию очереди, которая будет использоваться для производства задач (будет использован нашим работником службы).

amqplib используется в качестве клиента для нашего RabbitMQ Сервер

//rabbitmq.service.ts
import { Service } from 'typedi';
import amqp, { Channel, Connection } from 'amqplib';
import { logger } from '../common/utils/logger';

@Service()
export default class RabbitMQService {
  private connection: Connection;
  private channel: Channel;
  private queueName = 'ytgif-jobs';
  constructor() {
    this.initializeService();
  }

  private async initializeService() {
    try {
      await this.initializeConnection();
      await this.initializeChannel();
      await this.initializeQueues();
    } catch (err) {
      logger.error(err);
    }
  }
  private async initializeConnection() {
    try {
      this.connection = await amqp.connect(process.env.NODE_ENV === 'production' ? process.env.RABBITMQ_PROD : process.env.RABBITMQ_DEV);
      logger.info('Connected to RabbitMQ Server');
    } catch (err) {
      throw err;
    }
  }

  private async initializeChannel() {
    try {
      this.channel = await this.connection.createChannel();
      logger.info('Created RabbitMQ Channel');
    } catch (err) {
      throw err;
    }
  }

  private async initializeQueues() {
    try {
      await this.channel.assertQueue(this.queueName, {
        durable: true,
      });
      logger.info('Initialized RabbitMQ Queues');
    } catch (err) {
      throw err;
    }
  }

  public async sendToQueue(message: string) {
    this.channel.sendToQueue(this.queueName, Buffer.from(message), {
      persistent: true,
    });
    logger.info(`sent: ${message} to queue ${this.queueName}`);
  }
}

Код для начальной загрузки подключения/каналов/очередей довольно стандартный, и вы можете найти ссылки на эти функции на Rabbitmq Док или Anqplib Docs . Одна функция, которую нам нужно будет использовать извне этого класса, – это sendtoqueue () который используется для отправки сообщения в нашу очередь задач, как это видно в Jobservice, путем отправки строкового объекта задания.

 rabbitMQInstance.sendToQueue(JSON.stringify(job));

Теперь нам нужно только инициализировать службу RabbitMQ в начале нашего приложения, как это

import Container from 'typedi';

// Call initializeRabbitMQ() somewhere when starting the app
private initializeRabbitMQ() {
    Container.get(RabbitMqService);
  }

Теперь работа нашей бэкэнд -сервиса выполнена, и все, что осталось, – это для работника службы узлов, чтобы потреблять очередь задач и провести фактическое преобразование GIF.

Помните, что полный исходный код можно просмотреть на Репозиторий GitHub

В следующей части серии мы увидим, как мы можем реализовать работника службы узлов, который будет потреблять очередь задач и выполнить фактическое YouTube для преобразования GIF.

Оригинал: “https://dev.to/ragrag/node-backend-server-youtube-gif-maker-using-next-js-node-and-rabbitmq-47h7”