Используйте NestJS для создания серверных приложений

Архитектура внешний интерфейс NestJS
Используйте NestJS для создания серверных приложений

предисловие

В последнее время возникла необходимость во внедрении набора API-интерфейсов среднего уровня на основе внешнего стека технологий для обработки набора API-интерфейсов, поддерживаемых внешним интерфейсом.JSONконфигурационный файл.

После недолгих поисков я наконец выбралnest.jsЭта структура, благодаря поддержке АОП-программирования, совместима сSpringBootзаписывается аналогичным образом и может быть записан какSpringBootПрименяется тот набор архитектурных идей, что очень дружелюбно ко мне как штатному инженеру (немного знающему Java) 😁

После 3-х дней изучения и метания, я наконец построил набор архитектуры, которой я вполне доволен.В этой статье я поделюсь с вами своим архитектурным планом.Заинтересованные разработчики могут прочитать эту статью.

написать впереди

Содержание этой статьи будет включатьTypeScript, если вы не понимаете этого достаточно, пожалуйста, сделайте первый шаг:Документация TypeScript на китайском языкеУчись, заходи в дверь 🤓.

  • Полный код проекта этой статьи перемещается:nest-project

  • Для зависимостей, установленных в этой статье, требуется, чтобы версия вашего узла была14.16.0и выше.

Вы можете использовать контроллер управления версией узлаnЧтобы управлять вашим узлом версии, вы можете использоватьnpm install -g nустановить его.

После установки вы можете просто использоватьn 版本号Вы можете установить и переключиться на соответствующую версию node. Возможно, потребуется использовать под macOSsudo n 版本号. Например:n 14.16.0.

СвязанныйnЧтобы узнать больше о способах использования, перейдите по ссылке:n-github

Строительство окружающей среды

На официальном сайте гнезда он предоставляетТри способа построения:

  • Установить с помощью интерфейса командной строки
  • Установить с помощью Git
  • Создать вручную

Эти три метода установки относительно просты, и заинтересованные разработчики могут обратиться к документации для изучения. Чтобы реализовать практические возможности каждого, в этой статье не используется описанный выше метод для сборки проекта.Мы будем использовать пряжу для инициализации пустого проекта с 0, а затем установим соответствующие зависимости гнезда.

Примечание. Если вы уже настроили среду, пропустите эту главу и перейдите к следующей главе:Архитектура проекта.

Инициализировать пустой проект

В этой статье для инициализации проекта используется пряжа. Если он у вас не установлен, вам нужно сначала установить его с помощью npm. Команда выглядит следующим образом:

  • npm install --global yarn

После установки вы можете использовать команду:yarn --versionчтобы убедиться, что установка прошла успешно, в случае успеха вы увидите вывод, подобный следующему:

image-20220111215750509

Далее мы создаем файл с именемnest-projectпустая папка, войдите в эту папку в терминале, используйте команду:yarn initЧтобы инициализировать проект, как показано ниже, вы можете заполнить его в соответствии с вашими потребностями.Часть со скобками можно оставить незаполненной и оставить по умолчанию, просто нажмите Enter.

image-20220111222505312

Затем открываем проект, в папке всего один файл package.json, содержимое следующее:

{
  "name": "nest-project",
  "version": "1.0.0",
  "main": "index.js", // 这个可以删除,不需要这个字段
  "author": "likai",
  "license": "MIT",
  "private": true
}

Приведенный выше контент — это то, что мы только что выбрали в терминале, поэтому вы также можете сами создать пустой файл, создать этот файл json и написать соответствующую конфигурацию для достижения того же результата.

Установить вложенные зависимости

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

{
  "dependencies": {
    "@nestjs/common": "^8.1.1",
    "@nestjs/core": "^8.1.1",
    "@nestjs/platform-express": "^8.1.1",
    "class-transformer": "^0.5.1",
    "class-validator": "^0.13.2",
    "reflect-metadata": "^0.1.13",
    "rimraf": "^3.0.2",
    "rxjs": "^7.4.0"
  },
  "devDependencies": {
    "@nestjs/cli": "^8.1.3",
    "@nestjs/schematics": "^8.0.4",
    "@types/express": "^4.17.13",
    "@types/node": "^16.11.1",
    "supertest": "^6.1.6",
    "ts-loader": "^9.2.6",
    "ts-node": "^10.3.0",
    "tsconfig-paths": "^3.11.0",
    "tslib": "^2.3.0",
    "typescript": "^4.4.4",
    "webpack": "5.0.0"
  }
}

Затем мы открываем терминал, входим в каталог проекта и выполняемyarn installкоманда, интерфейс после успеха выглядит следующим образом:

image-20220111225541175

Установить зависимости спецификации кода

В этой статье для стандартизации кода используются eslint и prettier.Для разработчиков, которые этого не знают, перейдите к другой моей статье:Самостоятельно использовать ESLint+Prettier для форматирования кода.

Далее открываем ранее созданныйpackage.jsonфайл, вdevDependenciesДобавьте в объект следующий код:

{
  "devDependencies": {
    "eslint": "^7.0.0",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-prettier": "^3.3.1",
    "prettier": "^2.2.1",
    "@typescript-eslint/eslint-plugin": "^4.18.0",
    "@typescript-eslint/parser": "^4.18.0",
  }
}

После добавления выполнитьyarn installВнедрение пакета зависимостей завершено.

Добавить команду запуска

После установки всех зависимостей мы добавляем 6 запущенных скриптов в package.json для запуска проекта и создания пакета, как показано ниже:

  • prebuild удаляет каталог dist
  • собрать упакованный проект
  • начать запускать проект
  • start:dev запустить проект (поддерживается горячее обновление)
  • start:debug запускает проект в режиме отладчика (поддерживает отладку по точкам останова)
  • start:prod запустить упакованный проект
{
  "scripts": {
    "prebuild": "rimraf dist",
    "build": "nest build",
    "start": "nest start",
    "start:dev": "nest start --watch",
    "start:debug": "nest start --debug --watch",
    "start:prod": "node dist/main"
  }
}

Добавить файл конфигурации

Затем нам также нужно добавить файлы конфигурации, такие как гнездо, eslint и prettier в корневой каталог проекта, как показано ниже:

  • .editorconfigУнифицируйте файлы конфигурации для решения проблем, связанных с форматом кода, между различными операционными системами.
  • .eslintrc.jsконфигурационный файл eslint
  • .gitignoreФайлы, которые нужно игнорировать при фиксации git
  • .prettierrc.jsonболее красивый файл конфигурации
  • nest-cli.jsonконфигурационный файл гнезда
  • tsconfig.jsonфайл конфигурации машинописного текста
  • tsconfig.build.jsonСвязанные файлы конфигурации обработки для файлов ts при упаковке проекта

Для содержимого определенного файла щелкните синий шрифт выше, чтобы перейти непосредственно к соответствующему файлу в GitHub.

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

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

Полный код проекта, соответствующий этой главе, перемещен:nest-project

слой управления

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

Например

Далее создаем в каталоге srccontrollerпапку, создайте новую в ее каталогеAppController.tsдокумент.

Начнем с примера:

  1. иметь дело с/home/setTitleизpostзапрос, его параметры находятся в теле http
  2. иметь дело с/home/getTitleизgetзапрос, его параметры находятся в URL запроса

код реализации

Прочитав официальную документацию, мы можем написать следующий код:

import { Body, Controller, Get, Query, Post } from "@nestjs/common";

@Controller("home")
export class AppController {
  @Post("setTitle")
  setTitle(@Body() data: { id: number; title: string }): {
    code: number;
    data: null | string;
    msg: string;
  } {
    // 客户端传入的数据
    console.log(data);

    // 返回给客户端的数据
    return { code: 0, data: null, msg: "post方法调用成功" };
  }

  @Get("getTitle")
  getTitle(@Query("id") id: number): {
    code: number;
    data: string;
    msg: string;
  } {
    console.log("客户端传入的数据", id);
    return { code: 0, data: null, msg: "get方法调用成功" };
  }
}

Давайте посмотрим на роль каждого декоратора в приведенном выше коде:

  • @ControllerИспользуется для идентификации этого файла как контроллера, он принимает параметр, здесь я написал домой, который представляет все/homeзапросы будут идти сюда.
  • @PostОн используется для обработки запросов в формате post.Также принимает параметр.Здесь я написал setTitle, который представляет/home/setTitleПочтовый запрос будет здесь.
  • @BodyИспользуется для получения данных в теле http
  • @QueryИспользуется для получения данных в URL-адресе запроса

В документации Nest есть еще много декораторов, которые он предоставляет, которые могут справиться с различными сценариями разработки.Контроллер - запрос.

сервисный уровень

Сервисный уровень используется для обработки конкретной бизнес-логики, когда мы получаем запрос от клиента, мы извлекаем параметры и пишем конкретный бизнес-код.

Например

Далее создаем в каталоге srcserviceпапку, создайте новую в ее каталогеAppService.tsдокумент.

Например:

  • Напишите метод, чтобы сделать что-то в соответствии с идентификатором и вернуть результат операции после ее выполнения.

код реализации

Изучив документацию, мы знаем, что нам нужно использовать@Injectable()Чтобы украсить этот класс, код выглядит так:

import { Injectable } from "@nestjs/common";

@Injectable()
export class AppService {
  public setTitle(id: string): {
    code: number;
    data: null | string;
    msg: string;
  } {
    // 根据id做一些事情,此处省略
    console.log(id);
    // 返回操作结果
    return { code: 0, data: null, msg: "设置成功" };
  }
}

После выполнения вышеуказанных операций нам также необходимо изменитьAppController,существуетconstructorПредставьте сервис, который мы только что создали, часть кода выглядит следующим образом:

export class TextAttributeController {
  constructor(private readonly appService: AppService) {}
  @Post("setTitle")
  setTitle(){
    // 此处省略了较多代码,这里的重点是演示如何调用我们刚才写好的方法
    return this.appService.setTitle();
  }
}

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

интерфейсный слой

Этот уровень используется для объявления того, какие методы находятся в каждом классе службы, что может значительно улучшить читабельность кода. Без этого слоя, когда методов в сервисе становится все больше и больше, код будет очень длинным, и быстро найти метод станет очень трудоемко.

Например

Затем мы создаем его в каталоге srcinterfaceпапку, создайте новую в ее каталогеAppInterface.tsдокумент.

Например, нам нужно объявить 5 методов следующим образом:

  1. getTitle
  2. getName
  3. getAge
  4. setName
  5. setTitle

код реализации

Использование в TypeScriptinterfaceключевое слово для объявления интерфейса, то приведенный выше пример преобразуется в код следующим образом:

export interface AppInterface {
  getTitle(): string;
  getName(): string;
  getAge(): string;
  setName(): string;
  setTitle(): string;
}

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

@Injectable()
export class AppService implements AppInterface {
  getAge(): string {
    return "";
  }

  getName(): string {
    return "";
  }
  
  // 	其他方法省略
}

В TypeScript мы используемimplementsключевое слово для реализации интерфейса.

Модульный слой

Этот слой используется@Module()Класс для декораторов, предоставляющий метаданные, которые Nest использует для организации структуры приложения. Когда у нас появились уровень управления и сервисный уровень, они перестали работать, потому что им не хватало организации.

код реализации

Далее создаем в каталоге srcmoduleпапка, созданная в ее каталогеAppModule.tsфайл, код такой:

  • controllers — это тип данных массива, здесь мы можем ввести контроллеры слоя контроллера один за другим.
  • Поставщики также является массивом типа данных, мы можем представить услуги сервисного слоя один за другим здесь.
import { Module } from "@nestjs/common";
import { AppController } from "../controller/AppController";
import { AppService } from "../service/AppService";

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService]
})
export class AppModule {}

Для подробного ознакомления с контроллерами и провайдерами перейдите по ссылке:Nest-@module

Файл записи конфигурации

Далее в каталоге src создаемmain.tsфайл, его код выглядит так:

  • Импортируйте AppModule и используйте NestFactory для создания экземпляров.
  • Установите порт 3000 в качестве прослушивателя для этого проекта.
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./module/AppModule";

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

Наконец, мы запускаем package.jsonstart:devкоманда, доступная в браузереhttp://127.0.0.1:3000для доступа к элементу.

image-20220114230042606

Проверить контроллер, созданный уровнем управления

Далее, давайте проверим, что предыдущийAppController.tsМогут ли нормально работать два метода, написанные в.

Проверка метода Get

Давайте сначала проверим доступ к запросу на получение и получим доступ к нему в браузере.http://127.0.0.1:3000/home/getTitle?id=12, интерфейс клиента выглядит следующим образом:

image-20220114230439191

Сервер также выводит идентификатор, переданный клиентом, в адресной строке, как показано ниже:

image-20220114230550220

Подтвердить метод публикации

Нам нужно использовать postman, чтобы проверить, можно ли нормально получить доступ к методу post.Предполагая, что вы установили postman, мы создаем новый запрос и пишем по адресуhttp://127.0.0.1:3000/home/setTitle, результат доступа следующий:

image-20220114230935445

Точно так же сервер также получит данные json, которые мы передали в теле http, как показано ниже:

image-20220114231123801

Слой DTO (обработка параметров клиента)

В предыдущем примере параметры, которые мы получаем от клиента, напрямую прописываются в параметрах каждого метода в контроллере, проблемы, вызванные этим:

  1. Это снизит читабельность кода, а писать большое количество параметров в методе очень неэлегантно.
  2. Когда многие методы должны передавать одни и те же параметры, необходимо написать много повторяющегося кода, что значительно снижает удобство сопровождения.
  3. Валидацию параметров нужно прописать в методе в контроллере, что будет генерировать избыточный код.

Роль слоя DTO заключается в решении вышеуказанных проблем, мы используемclassдля обработки параметров, переданных клиентом.

код реализации

Мы создаем папку DTO в каталоге src и создаем его в своем каталогеAppDto.tsфайл, код такой:

export class AppDto {
  public id: string;
  public title: string;
  public name: string;
}

export class GetNameDto extends AppDto {
  public type: string;
}

Впоследствии мыAppController.tsЕго можно использовать в методе в , код такой:

import { AppDto, GetNameDto } from "../dto/AppDto";

@Controller("home")
export class AppController {
  @Post("setTitle")
  setTitle(@Body() data: AppDto): {
    code: number;
    data: null | string;
    msg: string;
  } {
    // 其他代码省略
  }
  
  @Get("getName")
  getName(@Body() data: GetNameDto): {
    code: number;
    data: null | string;
    msg: string;
  } {
    // 其他代码省略
  }
}

После выполнения вышеуказанных операций мы успешно решили 1, 2 задачи. Поскольку получение параметров реализовано с помощью классов, мы можем использовать наследование, чтобы избежать избыточного кода.

Проверка правильности параметров с помощью конвейера

Далее используем пайпы для решения третьей задачи, на официальном сайте гнезда это предусмотрено8 встроенных труб из коробки, здесь нам нужно использовать егоValidationPipeконвейер для проверки параметров.

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

  • Для привязки к контроллеру или его методу мы используем@UsePipes()декоратор и создайте экземпляр конвейера и передайте его на проверку Joi.
  • Сделайте его глобальным конвейером на входе для каждого процессора маршрутов в приложении.

Здесь мы используем конвейер глобальной области видимости и модифицируем файл main.ts, код выглядит следующим образом:

import { NestFactory } from "@nestjs/core";
import { AppModule } from "./module/AppModule";
import { ValidationPipe } from "@nestjs/common";

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

Чтобы узнать о конкретном принципе конвейера, перейдите по ссылке:вложенный трубопровод

Затем мы можем использовать связанный с ним декоратор на уровне dto для проверки параметров,AppDto.tsЧасть кода выглядит так:

import { IsString, MinLength } from "class-validator";

export class AppDto {
  @MinLength(5)
  @IsString()
  public id!: string;
  @IsString()
  public title!: string;
  @IsString()
  public name!: string;
}

export class GetNameDto extends AppDto {
  @IsString()
  public type!: string;
}

Наконец, мы используем postman, чтобы проверить, работает ли он, следующим образом:

  • Идентификатор типа передается в
  • Не передан параметр имени

Сервер вернул ошибку 400 и сообщил причину ошибки.

image-20220116221632391

Поскольку мы передаем ненулевую проверку параметра декоратору, в классе dto нам нужно использовать!:оператор, чтобы утверждать, что параметр должен иметь значение.

мы начинаем сclass-validator'В пакете представлен декоратор проверки строкового типа, который также может проверять другие типы. Заинтересованные разработчики, пожалуйста, перейдите по адресу:class-validator#usage

Слой VO (представление, возвращаемое клиенту)

Обычно поля, которые мы возвращаем клиенту, являются фиксированными.На уровне контроллера ранее в этой статье мы возвращали оба методаcode,data,msgЭти три поля, только данные разные. Затем мы должны инкапсулировать его и передавать данные в качестве параметров, что значительно повышает удобство сопровождения кода, что мы и называем слоем VO.

пакетный инструментальный класс

Мы создаем его в каталоге srcVOпапка, созданная в ее каталогеResultVO.tsфайл, код такой:

  • Просто создайте класс и добавьте три поля
  • Напишите методы get и set для каждого поля
export class ResultVO<T> {
  private code!: number;
  private msg!: string;
  private data!: T | null;

  public getCode(): number {
    return this.code;
  }

  public setCode(value: number): void {
    this.code = value;
  }

  public getMsg(): string {
    return this.msg;
  }

  public setMsg(value: string): void {
    this.msg = value;
  }

  public getData(): T | null {
    return this.data;
  }

  public setData(value: T | null): void {
    this.data = value;
  }
}

Затем мы создаем папку utils в каталоге src и создаем ее в своем каталоге.VOUtils.tsфайл, который инкапсулирует общие методы, которые удобно вызывать другим слоям напрямую Код выглядит следующим образом:

  • мы инкапсулировалиsuccessа такжеerrorметод
  • В случае успеха передать данные
  • В случае сбоя передайте код и сообщение, чтобы сообщить клиенту о причине ошибки.
// 返回给调用者的视图结构
import { ResultVO } from "../VO/ResultVO";

export class VOUtils {
  public static success<T>(data?: T): ResultVO<T> {
    const resultVo = new ResultVO<T>();
    resultVo.setCode(0);
    resultVo.setMsg("接口调用成功");
    resultVo.setData(data || null);
    return resultVo;
  }

  public static error(code: number, msg: string): ResultVO<null> {
    const resultVo = new ResultVO<null>();
    resultVo.setCode(code);
    resultVo.setMsg(msg);
    return resultVo;
  }
}

Уведомление:successМетод поддерживает входящие параметры любого типа.В реальных бизнес-требованиях уровень данных будет очень сложным.Когда вы фактически используете его, вы можете создать класс vo, соответствующий бизнесу в соответствии с вашими конкретными бизнес-требованиями, а затем создать его экземпляр. , присваивая значение каждому полю. Наконец, вы можете передать объект, который вы создали при вызове метода успеха.

использовать в бизнес-коде

Тогда мы можемserviceслой для использования созданного нами класса инструмента, пример кода выглядит следующим образом:

import { VOUtils } from "../utils/VOUtils";

@Injectable()
export class AppService implements AppInterface {
  // 其它代码省略
  setTitle(): VOUtils {
    return VOUtils.success("标题设置成功");
  }
}

Результат вызова интерфейса следующий:

image-20220116231739210

тип слоя

Когда мы пишем бизнес-код, мы сталкиваемся со многимиObjectТип данных, обычно мы определяем определенный тип для каждого поля. В настоящее время нам нужно объединить все типы для удобства обслуживания. Здесь мой подход заключается в создании папки типов в каталоге src и размещении всех определений типов. помещается в эту папку, а код выглядит так:

  • Создал папку типов
  • созданный в папке типаAppDataType.tsфайл для хранения всех типов
export type book = {
  title: string;
  author: string;
  time: string;
  updateTime: string;
};

export interface specialBook extends book {
  id: number;
  createTime: string;
}


Примечание. Все определения типов определяются с помощью ключевого слова type, которое можно напрямую импортировать при использовании.Когда мы хотим наследовать тип, мы должны использовать ключевое слово interface.

слой перечисления

Когда мы пишем бизнес-код, мы обязательно столкнемся с различными аномальными состояниями.При возникновении исключения на стороне сервера нам необходимо вернуть информацию об ошибке и код состояния на уровне VO.Если мы напрямую записываем данные в метод, когда нам нужно изменить его позже, это будет головная боль. Затем, когда мы определяем эти данные на уровне перечисления и напрямую используем наше определенное перечисление в бизнес-коде, эта проблема легко решается.

Мы создаем его в каталоге srcenumпапка, созданная в ее папкеAppEnum.tsфайл, код такой:

  • NOTFOUND указывает код ошибки
  • NOTFOUND_DESCRIPTION указывает описание кода ошибки
export enum AppEnum {
  NOTFOUND = -1,
  NOTFOUND_DESCRIPTION = "未找到相关人物"
}

Затем мы можем использовать его в бизнес-коде, как показано ниже:

@Injectable()
export class AppService implements AppInterface {
  // 其它代码省略
  getName(): VOUtils {
    return VOUtils.error(AppEnum.NOTFOUND, AppEnum.NOTFOUND_DESCRIPTION);
  }

}

Примечание: перечисление в машинописном тексте не может установить соответствующую информацию описания, когда оно определено как Java, поэтому здесь мы можем выбрать только способ сохранения страны по кривой.Если определение неверно, определите перечисление, заканчивающееся на __DESCRIPTION.

код проекта

Для получения полного кода, используемого в этой статье, перейдите в репозиторий проекта на GitHub:nest-project

напиши в конце

На данный момент статья опубликована.

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

Если вы заинтересованы во мне, пожалуйста, перейдите на мойперсональный сайт,Узнать больше о.

  • Если в статье есть ошибки, исправьте их в комментариях, если статья вам поможет, ставьте лайк и подписывайтесь 😊
  • Эта статья была впервые опубликована вудивительный программистПубличный аккаунт, перепечатка без разрешения запрещена 💌