Запись stestjs (c)

Node.js NestJS

существуетНачало работы с Nestjs (2), мы создали базовое приложение Nestjs. Ниже мы расширяем это.

Адрес источника:awesome-nest

Сериализация

В сущности, иногда некоторые поля не нужно возвращать на фронтенд, обычно нам нужно сделать отсев самостоятельно, а в Nestjs сотрудничать сclass-transformer, вы можете легко реализовать эту функцию.

Например, у нас есть базовый класс объектаcommon.entity.ts, при возврате данных мы не хотим ставитьcreate_atиupdate_atВозьми это тоже, ты можешь использовать это сейчас@Exclude()исключатьCommonEntityЭти два поля в:

import { CreateDateColumn, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'
import { Exclude } from 'class-transformer'

export class CommonEntity {
  @PrimaryGeneratedColumn('uuid')
  id: string

  @Exclude()
  @CreateDateColumn({
    comment: '创建时间',
  })
  create_at: number

  @Exclude()
  @UpdateDateColumn({
    comment: '更新时间',
  })
  update_at: number
}

Отметить и использовать в месте, соответствующем запросуClassSerializerInterceptor,В настоящее время,GET /api/v1/cats/1Данные, возвращаемые этим запросом, не будут содержатьcreate_atиupdate_atэти два поля.

@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {
  }

  @Get(':id')
  @UseInterceptors(ClassSerializerInterceptor)
  findOne(@Param('id') id: string): Promise<Array<Partial<CatEntity>>> {
    return this.catsService.getCat(id)
  }
}

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

@UseInterceptors(ClassSerializerInterceptor)
@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {
  }

  @Get(':id')
  findOne(@Param('id') id: string): Promise<Array<Partial<CatEntity>>> {
    return this.catsService.getCat(id)
  }
  
  @Post()
  create(@Body() createCatDto: CreateCatDto): Promise<void> {
    return this.catsService.createCat(createCatDto)
  }
}

даже вmain.tsВ нем как глобальный Interceptor, но это неудобно для мелкозернистого управления.

async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  app.setGlobalPrefix('api/v1')

  app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)))

  await app.listen(config.port, config.hostName, () => {
    Logger.log(
      `Awesome-nest API server has been started on http://${config.hostName}:${config.port}`,
    )
  })
}

bootstrap()

В некоторых сценариях нам нужно вернуться в поле в объекте и вернуть его, вы можете использовать@Transform():

@Entity('dog')
export class DogEntity extends CommonEntity {
  @Column({ length: 50 })
  @Transform(value => `dog: ${value}`)
  name: string

  @Column()
  age: number

  @Column({ length: 100, nullable: true })
  breed: string
}

В настоящее время,nameПоле проходит через@Transformупаковка станетdog: nameформат. Если нам нужно создать новое поле на основе существующего поля, мы можем использовать@Expose():

@Entity('dog')
export class DogEntity extends CommonEntity {
  @Column({ length: 50 })
  @Transform(value => `dog: ${value}`)
  name: string

  @Column()
  age: number

  @Column({ length: 100, nullable: true })
  breed: string

  @Expose()
  get isOld(): boolean {
    return this.age > 10
  }
}

Приведенный выше код будет основан на запросеageПолевой динамический расчетisOldЗначение , в настоящее время результат, возвращаемый запросом метода GET, выглядит следующим образом:

{
    "data": [
        {
            "id": "15149ec5-cddf-4981-89a0-62215b30ab81",
            "name": "dog: nana",
            "age": 12,
            "breed": "corgi",
            "isOld": true
        }
    ],
    "status": 0,
    "message": "请求成功"
}

дела

При использовании MySQL иногда нам нужно использовать транзакции, С помощью TypeORM транзакции можно использовать следующим образом:

@Delete(':name')
@Transaction()
delete(
  @Param('name') name: string,
  @TransactionManager() manager: EntityManager,
): Promise<void> {
    return this.catsService.deleteCat(name, manager)
}

@Transaction()Оберните все выполнение в контроллере или службе в транзакцию базы данных,@TransportManagerПредоставляется диспетчер сущностей транзакций, который необходимо использовать для выполнения запросов в рамках этой транзакции:

async deleteCat(name: string, manager: EntityManager): Promise<void> {
  await manager.delete(CatEntity, { name })
}

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

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

import { getConnection } from "typeorm";

// 获取连接并创建新的queryRunner
const connection = getConnection();
const queryRunner = connection.createQueryRunner();

// 使用我们的新queryRunner建立真正的数据库连
await queryRunner.connect();

// 现在我们可以在queryRunner上执行任何查询,例如:
await queryRunner.query("SELECT * FROM users");

// 我们还可以访问与queryRunner创建的连接一起使用的实体管理器:
const users = await queryRunner.manager.find(User);

// 开始事务:
await queryRunner.startTransaction();

try {
  // 对此事务执行一些操作:
  await queryRunner.manager.save(user1);
  await queryRunner.manager.save(user2);
  await queryRunner.manager.save(photos);

  // 提交事务:
  await queryRunner.commitTransaction();
} catch (err) {
  // 有错误做出回滚更改
  await queryRunner.rollbackTransaction();
}

QueryRunnerОбеспечивает одно соединение с базой данных. Используйте средство выполнения запросов для организации транзакций. Одна транзакция может быть установлена ​​только на одном обработчике запросов.

Сертификация

В этом приложении пользователь еще не аутентифицирован.Посредством аутентификации пользователя можно судить о легитимности и полномочиях роли доступа. Обычно аутентификация основана либо на сеансе, либо на токене. Здесь аутентификация пользователя выполняется с помощью JWT на основе токена (JSON Web Token).

Сначала установите соответствующие зависимости:

$ npm install --save @nestjs/passport passport @nestjs/jwt passport-jwt

затем создайтеjwt.strategy.tsИспользуется для проверки токена, когда TOKEN действителен, разрешается дальнейшая обработка, в противном случае возвращается401(Unanthorized):

import { ExtractJwt, Strategy } from 'passport-jwt'
import { PassportStrategy } from '@nestjs/passport'
import { Injectable, UnauthorizedException } from '@nestjs/common'
import config from '../../config'
import { UserEntity } from '../entities/user.entity'
import { AuthService } from './auth.service'

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: config.jwt.secret,
    })
  }

  async validate(payload: UserEntity) {
    const user = await this.authService.validateUser(payload)
    if (!user) {
      throw new UnauthorizedException('身份验证失败')
    }
    return user
  }
}

затем создайтеauth.service.ts, вышесказанноеjwt.strategy.tsЭтот сервис будет использоваться для проверки токенов и предоставления методов для создания токенов:

import { JwtService } from '@nestjs/jwt'
import { Injectable } from '@nestjs/common'
import { UserEntity } from '../entities/user.entity'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { Token } from './auth.interface'
import config from '../../config'

@Injectable()
export class AuthService {
  constructor(
    @InjectRepository(UserEntity)
    private readonly userRepository: Repository<UserEntity>,
    private readonly jwtService: JwtService,
  ) {
  }

  createToken(email: string): Token {
    const accessToken = this.jwtService.sign({ email })
    return {
      expires_in: config.jwt.signOptions.expiresIn,
      access_token: accessToken,
    }
  }

  async validateUser(payload: UserEntity): Promise<any> {
    return await this.userRepository.find({ email: payload.email })
  }
}

Эти два файла будут служить службами в соответствующемmoduleзарегистрирован и представленPassportModuleиJwtModule:

import { Module } from '@nestjs/common'
import { AuthService } from './auth/auth.service'
import { PassportModule } from '@nestjs/passport'
import { JwtModule } from '@nestjs/jwt'
import { JwtStrategy } from './auth/jwt.strategy'
import config from '../config'


@Module({
  imports: [
    PassportModule.register({ defaultStrategy: 'jwt' }),
    JwtModule.register(config.jwt),
  ],
  providers: [
    AuthService,
    JwtStrategy,
  ],
  exports: [],
})
export class FeaturesModule {
}

В это время вы можете использовать@UseGuards(AuthGuard())Чтобы проверить подлинность API, требующего аутентификации:

import {
  Body,
  ClassSerializerInterceptor,
  Controller,
  Get,
  Param,
  Post,
  UseGuards,
  UseInterceptors,
} from '@nestjs/common'

import { CatsService } from './cats.service'
import { CreateCatDto } from './cat.dto'
import { CatEntity } from '../entities/cat.entity'
import { AuthGuard } from '@nestjs/passport'

@Controller('cats')
@UseGuards(AuthGuard())
export class CatsController {
  constructor(private readonly catsService: CatsService) {
  }

  @Get(':id')
  @UseInterceptors(ClassSerializerInterceptor)
  findOne(@Param('id') id: string): Promise<Array<Partial<CatEntity>>> {
    return this.catsService.getCat(id)
  }

  @Post()
  create(@Body() createCatDto: CreateCatDto): Promise<void> {
    return this.catsService.createCat(createCatDto)
  }
}

При имитации запроса через Postman, если токен не принесён, будет возвращен следующий результат:

{
    "message": {
        "statusCode": 401,
        "error": "Unauthorized"
    },
    "status": 1
}

Безопасность

В веб-безопасности есть два распространенных метода атак: XSS (межсайтовый скриптинг) и CSRF (подделка межсайтовых запросов).

Методы аутентификации JWT, потому что нет файла cookie, поэтому не существует CSRF. Если вы не используете аутентификацию JWT, вы можете использоватьcsurfЭта библиотека для решения этой проблемы безопасности.

Для XSS вы можете использоватьhelmetвыполнять меры предосторожности. В шлеме есть 12 промежуточных программ, которые устанавливают некоторые HTTP-заголовки, связанные с безопасностью. НапримерxssFilterОн используется для некоторой защиты, связанной с XSS.

Для атак методом перебора с большим количеством запросов с одного IP можно использоватьexpress-rate-limitк ограничению скорости.

Для общих междоменных проблем NestJs предлагает два способа решения: пропускapp.enableCors()Способ включения междоменного доступа, другой, как показано ниже, включите его в объекте параметров Nest.

Наконец, все эти настройки включены как глобальное промежуточное ПО, и, наконец,main.ts, параметры безопасности следующие:

import * as helmet from 'helmet'
import * as rateLimit from 'express-rate-limit'

async function bootstrap() {
  const app = await NestFactory.create(AppModule, { cors: true })

  app.use(helmet())
  app.use(
    rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 100, // limit each IP to 100 requests per windowMs
    }),
  )

  await app.listen(config.port, config.hostName, () => {
    Logger.log(
      `Awesome-nest API server has been started on http://${config.hostName}:${config.port}`,
    )
  })
}

HTTP-запрос

Средняя пара NestjsAxiosинкапсулировать и использовать какHttpServiceВстроенныйHttpModuleсередина.HttpServiceТип возвращаемого значения и AngularHttpClient Moduleто же самое, обаobservables, так что вы можете использоватьrxjsОператоры в обрабатывают различные асинхронные операции.

Во-первых, нам нужно импортироватьHttpModule:

import { Global, HttpModule, Module } from '@nestjs/common'

import { LunarCalendarService } from './services/lunar-calendar/lunar-calendar.service'

@Global()
@Module({
  imports: [HttpModule],
  providers: [LunarCalendarService],
  exports: [HttpModule, LunarCalendarService],
})
export class SharedModule {}

Здесь мы ставимHttpModuleкак глобальный модуль, вsharedModuleимпортировать и экспортировать для использования другими модулями. Тогда мы можем использоватьHttpService, например, мы находимся вLunarCalendarServiceинъекцияHttpService, а затем вызовите егоgetспособ запроса информации лунного календаря на текущий день. В этот моментgetто, что возвращаетсяObservable.

для этогоObservableпоток, через который можно пройтиpipeВыполнить ряд операций, например, мы можем напрямую использовать rxjsmapОператор помогает нам фильтровать данные в одном слое, и ошибка тайм-аута будет сообщена более чем через 5 секунд.catchErrorпоможет нам поймать все ошибки и вернуть значение черезofОператор преобразуется вobservable:

import { HttpService, Injectable } from '@nestjs/common'
import { of, Observable } from 'rxjs'
import { catchError, map, timeout } from 'rxjs/operators'

@Injectable()
export class LunarCalendarService {
  constructor(private readonly httpService: HttpService) {
  }

  getLunarCalendar(): Observable<any> {
    return this.httpService
      .get('https://www.sojson.com/open/api/lunar/json.shtml')
      .pipe(
        map(res => res.data.data),
        timeout(5000),
        catchError(error => of(`Bad Promise: ${error}`))
      )
  }
}

Если вам нужно сделать аксиомынастроить, который можно установить непосредственно при регистрации модуля:

import { Global, HttpModule, Module } from '@nestjs/common'

import { LunarCalendarService } from './services/lunar-calendar/lunar-calendar.service'

@Global()
@Module({
  imports: [
    HttpModule.register({
      timeout: 5000,
      maxRedirects: 5,
    }),
  ],
  providers: [LunarCalendarService],
  exports: [HttpModule, LunarCalendarService],
})
export class SharedModule {}

Рендеринг шаблона

В Nestjs вы можете использовать hbs в качестве механизма рендеринга шаблонов:

$ npm install --save hbs

существуетmain.tsВ, мы говорим Экспресс,staticПапки используются для хранения статических файлов,viewsФайлы шаблонов включены:

import { NestFactory } from '@nestjs/core'
import { NestExpressApplication } from '@nestjs/platform-express'
import { join } from 'path'

import { AppModule } from './app.module'
import config from './config'
import { Logger } from './shared/utils/logger'

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule, {
    cors: true,
  })

  app.setGlobalPrefix('api/v1')

  app.useStaticAssets(join(__dirname, '..', 'static'))
  app.setBaseViewsDir(join(__dirname, '..', 'views'))
  app.setViewEngine('hbs')

  await app.listen(config.port, config.hostName, () => {
    Logger.log(
      `Awesome-nest API server has been started on http://${config.hostName}:${config.port}`,
    )
  })
}

существуетviewsсоздать новыйcatsPage.hbsФайл, скажем так, структура данных, которую нам нужно заполнить, такая:

{
  cats: [
    {
      id: 1,
      name: 'yyy',
      age: 12,
      breed: 'black cats'
    }
  ],
  title: 'Cats List',
}

На этом этапе вы можете написать шаблон следующим образом:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <meta http-equiv="X-UA-Compatible" content="ie=edge"/>
    <style>
        .table .default-td {
            width: 200px;
        }

        .table tbody>tr:nth-child(2n-1) {
            background-color: rgb(219, 212, 212);
        }

        .table tbody>tr:nth-child(2n) {
            background-color: rgb(172, 162, 162);
        }
    </style>
</head>
<body>
<p>{{ title }}</p>
<table class="table">
    <thead>
    <tr>
        <td class="id default-td">id</td>
        <td class="name default-td">name</td>
        <td class="age default-td">age</td>
        <td class="breed default-td">breed</td>
    </tr>
    </thead>
    <tbody>
    {{#each cats}}
        <tr>
            <td>{{id}}</td>
            <td>{{name}}</td>
            <td>{{age}}</td>
            <td>{{breed}}</td>
        </tr>
    {{/each}}
    </tbody>
</table>
</body>
</html>

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

В контроллере пройти@Renderуказывает имя шаблона, а вreturnВозвращаем данные, которые необходимо заполнить:

@Get('page')
@Render('catsPage')
getCatsPage() {
  return {
    cats: [
      {
        id: 1,
        name: 'yyy',
        age: 12,
        breed: 'black cats'
      }
    ],
    title: 'Cats List',
  }
}

Nestjs также поддерживает интеграцию с другими платформами SSR, такими как Next, Angular Universal, Nuxt. Специальное использование демо позволяет просматривать эти проекты отдельноnestify,nest-angular,simple-todos.

Документация Swagger

Nestjs также предоставляет поддержку документации swagger, которая нам удобна для отслеживания и тестирования API:

$ npm install --save @nestjs/swagger swagger-ui-express

существуетmain.tsДокументация по промежуточному ПО:

const options = new DocumentBuilder()
    .setTitle('Awesome-nest')
    .setDescription('The Awesome-nest API Documents')
    .setBasePath('api/v1')
    .addBearerAuth()
    .setVersion('0.0.1')
    .build()

const document = SwaggerModule.createDocument(app, options)
SwaggerModule.setup('docs', app, document)

В этот момент посетитеhttp://localhost:3300/docsВы можете увидеть страницу документации swagger.

Для разных API можно использовать в контроллере@ApiUseTags()Для классификации для API, требующих аутентификации, вы можете добавить@ApiBearerAuth(), чтобы после заполнения токена в swagger можно было напрямую протестировать API:

@ApiUseTags('cats')
@ApiBearerAuth()
@Controller('cats')
@UseGuards(AuthGuard())
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Get('page')
  @Render('catsPage')
  getCatsPage(): Promise<any> {
    return this.catsService.getCats()
  }
}

Для нашего запланированного DTO, чтобы сделатьSwaggerModuleЧтобы получить доступ к свойствам класса, мы должны использовать@ApiModelProperty()Декоратор помечает все эти свойства:

import { ApiModelProperty } from '@nestjs/swagger'
import { IsEmail, IsString } from 'class-validator'

export class AccountDto {
  @ApiModelProperty()
  @IsString()
  @IsEmail()
  readonly email: string

  @ApiModelProperty()
  @IsString()
  readonly password: string
}

Для получения дополнительной информации об использовании документации swagger вы можете посетить официальный сайт.OpenAPI (Swagger)Содержание.

горячая перезагрузка

Во время разработки запуститеnpm run start:devВ это время выполняется полная компиляция. Если проект относительно большой, полная компиляция займет много времени. В это время мы можем использовать веб-пакет, чтобы помочь нам выполнить инкрементную компиляцию, что значительно повысит эффективность разработки.

Сначала установите зависимости, связанные с веб-пакетом:

$ npm i --save-dev webpack webpack-cli webpack-node-externals ts-loader

Создайте один в корневом каталогеwebpack.config.js:

const webpack = require('webpack');
const path = require('path');
const nodeExternals = require('webpack-node-externals');

module.exports = {
  entry: ['webpack/hot/poll?100', './src/main.ts'],
  watch: true,
  target: 'node',
  externals: [
    nodeExternals({
      whitelist: ['webpack/hot/poll?100'],
    }),
  ],
  module: {
    rules: [
      {
        test: /.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  mode: 'development',
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },
  plugins: [new webpack.HotModuleReplacementPlugin()],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'server.js',
  },
};

существуетmain.tsВключить HMR в:

declare const module: any;

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

  if (module.hot) {
    module.hot.accept();
    module.hot.dispose(() => app.close());
  }
}
bootstrap();

существуетpackage.jsonДобавьте следующие две команды в:

{
  "scripts": {
    "start": "node dist/server",
		"webpack": "webpack --config webpack.config.js"
  }
}

бегатьnpm run webpackПосле этого webpack начинает смотреть файл, а затем запускается в другом окне командной строки.npm start.