Nest+GrqphQL+Prisma+React全栈开发(三)-Nest篇

在Nest+GrqphQL+Prisma+React全栈开发这个系列中,我将会带领着大家从零到一,一步一步简单学会使用这些技术栈,并实际开发一个实战应用。

这篇是第三篇文章,Nest篇。

一、简介

Nestjs是一个高效的nodejs服务端应用开发框架,扩展性高,性能好,优点还包括原生支持TS,以及底层构建在强大的Http框架上,你可以自己选择是使用express或者fastify,以及支持模块化,微服务,装饰器语法,依赖注入(将自己的依赖的实例化交给外部容器控制)等等特性。

可以说非常强大好用。

二、结构

Nest主要有以下这几个部分组成:

  • 控制器
  • 提供者
  • 模块
  • 中间件
  • 异常过滤器
  • 管道
  • 守卫
  • 拦截器
  • 装饰器

下面我们一一来简单的介绍下这些模块:

(一)控制器

控制器负责处理传入的请求和向客户端返回响应,一个控制器包含多个路由。

/* cats.controller.ts */

import { Controller, Get, Post, Res, HttpStatus } from '@nestjs/common';
import { Response } from 'express';

@Controller('cats')
export class CatsController {
  @Post()
  create(@Res() res: Response) {
    res.status(HttpStatus.CREATED).send();
  }

  @Get()
  findAll(@Res() res: Response) {
    res.status(HttpStatus.OK).json([]);
  }
}
复制代码

(二)提供者

用 @Injectable() 装饰器注释的类,例如nest中的service, repositoryfactoryhelper等都可以被视为提供者。Nest中的依赖管理是通过依赖注入这个设计模式实现的,一般是通过控制器的构造函数注入的:

constructor(private readonly catsService: CatsService) {}
复制代码

也可以使用@Inject()基于属性注入:

import { Injectable, Inject } from '@nestjs/common';

@Injectable()
export class HttpService<T> {
  @Inject('HTTP_OPTIONS')
  private readonly httpClient: T;
}
复制代码

(三)模块

模块是具有 @Module() 装饰器的类。 @Module() 装饰器提供了元数据,Nest 用它来组织应用程序结构。每个 Nest 应用程序至少有一个模块,即根模块。根模块是 Nest 开始安排应用程序树的地方。

创建一个你自己的功能模块:

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}
复制代码

然后在app.module.ts中导入:

import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class ApplicationModule {}
复制代码

如果想要模块共享的话,只需在exports中导出:

exports: [CatsService]
复制代码

如果你想在任何地方导入相同的模块,可以设置为全局模块:

@Global()
@Module({
  ...
})
export class CatsModule {}
复制代码

Nest还支持动态模块,这些模块可以动态注册和配置提供程序,使用forRoot进行配置:

@Module({
 ...
})
export class DatabaseModule {
  static forRoot(entities = [], options?): DynamicModule {
    const providers = createDatabaseProviders(options, entities);
    return {
      module: DatabaseModule,
      providers: providers,
      exports: providers,
    };
  }
}
复制代码

(四)中间件

中间件是在路由处理程序 之前 调用的函数。Nest 中间件实际上等价于 express 中间件。

中间件有以下作用:

  • 对请求和响应对象进行更改。
  • 结束请求-响应周期。
  • 调用堆栈中的下一个中间件函数。
  • 如果当前的中间件函数没有结束请求-响应周期, 它必须调用 next() 将控制传递给下一个中间件函数。否则, 请求将被挂起。

我们可以在函数中或在具有 @Injectable() 装饰器的类中实现自定义 Nest中间件,只需要实现 NestMiddleware 接口即可。

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log('Request...');
    next();
  }
}
复制代码

不过中间件的使用需要注意以下几点:

  • 中间件不能在 @Module() 装饰器中列出
  • 必须使用模块类的 configure() 方法来设置它们
  • 包含中间件的模块必须实现 NestModule 接口
@Module({
  ...
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('cats');
  }
}
复制代码

如果是多个中间件,可以这样写:

consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);
复制代码

可以直接在app上直接注册全局中间件:

const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);
复制代码

(五)异常过滤器

处理整个应用程序中的所有抛出的异常。Nest提供了一系列继承自核心异常 HttpException 的可用异常:

  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ForbiddenException
  • NotAcceptableException
  • RequestTimeoutException
  • ConflictException
  • GoneException
  • PayloadTooLargeException
  • UnsupportedMediaTypeException
  • UnprocessableException
  • InternalServerErrorException
  • NotImplementedException
  • BadGatewayException
  • ServiceUnavailableException
  • GatewayTimeoutException

不过我们也可以自定义异常过滤器,使用@Catch() 装饰器绑定所需的元数据到异常过滤器上,如果@Catch()参数列表为空,将会捕获每一个未处理的异常(不管异常类型如何)。

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
      });
  }
}
复制代码

绑定过滤器:

@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}
复制代码

你也可以使用全局范围的过滤器:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
bootstrap();
复制代码

(六)管道

具有 @Injectable() 装饰器的类,负责对数据转换或者验证。Nest 自带八个开箱即用的管道,即

  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • DefaultValuePipe
  • ParseEnumPipe
  • ParseFloatPipe

如果想要自定义管道,只应实现 PipeTransform 接口:

import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    return value;
  }
}
复制代码

使用@UsePipes()进行管道的绑定:

@Post()
@UsePipes(new JoiValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}
复制代码

(七)守卫

使用 @Injectable() 装饰器的类,根据运行时出现的某些条件(例如权限,角色,访问控制列表等)来确定给定的请求是否由路由处理程序处理。

守卫在每个中间件之后执行,但在任何拦截器或管道之前执行。

守卫必须实现一个canActivate()函数。此函数应该返回一个布尔值,指示是否允许当前请求。它可以同步或异步地返回响应:

  • 如果返回 true, 将处理用户调用。
  • 如果返回 false, 则 Nest 将忽略当前处理的请求。

例如实现一个授权守卫:

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);
  }
}
复制代码

使用也很简单:

@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}
复制代码

也可以设置全局守卫:

const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());
复制代码

我们还可以为守卫增加反射器,获取不同路由的不同元数据,以此做出决策。

先使用@SetMetadata()设置元数据:

@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}
复制代码

使用反射器获取设置的元数据:

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    return matchRoles(roles, user.roles);
  }
}
复制代码

(八)拦截器

使用 @Injectable() 装饰器注解的类,可以转换、扩展、重写函数,以及在函数之前之后绑定额外逻辑。

每个拦截器都有 intercept() 方法,它接收2个参数。 第一个是 ExecutionContext 实例(与守卫完全相同的对象)。第二个参数是 CallHandler``返回一个 Observable`。

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Before...');

    const now = Date.now();
    return next
      .handle()
      .pipe(
        tap(() => console.log(`After... ${Date.now() - now}ms`)),
      );
  }
}
复制代码

绑定拦截器:

@UseInterceptors(LoggingInterceptor)
export class CatsController {}
复制代码

绑定全局拦截器:

const app = await NestFactory.create(ApplicationModule);
app.useGlobalInterceptors(new LoggingInterceptor());
复制代码

(九)装饰器

Nest 是基于装饰器这种语言特性而创建的,你还可以自定义自己的装饰器。

例如我们可以自定义一个参数装饰器:

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
  const request = ctx.switchToHttp().getRequest();
  return request.user;
});
复制代码

然后使用它:

@Get()
async findOne(@User() user: UserEntity) {
  console.log(user);
}
复制代码

Nest 还可以聚合多个装饰器。例如,假设您要将与身份验证相关的所有装饰器聚合到一个装饰器中。这可以通过以下方法实现:

import { applyDecorators } from '@nestjs/common';

export function Auth(...roles: Role[]) {
  return applyDecorators(
    SetMetadata('roles', roles),
    UseGuards(AuthGuard, RolesGuard),
    ApiBearerAuth(),
    ApiUnauthorizedResponse({ description: 'Unauthorized"' })
  );
}
复制代码

使用聚合装饰器:

@Get('users')
@Auth('admin')
findAllUsers() {}
复制代码

三、使用

$ npm i -g @nestjs/cli 
$ nest new nest-demo
复制代码

使用nest的cli将会创建一个最简单的新项目目录,在src目录中文件组织如下:

src
 ├── app.controller.spec.ts
 ├── app.controller.ts
 ├── app.module.ts
 ├── app.service.ts
 └── main.ts //应用程序入口文件
复制代码

main.ts内容如下:

/* 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创建了一个应用实例,监听了3000端口。

Guess you like

Origin juejin.im/post/7068925905926062110