Nest.js 学习小记

300 阅读9分钟

来自小满zs的教程 附链接

在底层,Nest使用强大的 HTTP Server 框架,如 ExpressFastify。Nest 在这些框架之上提供了一定程度的抽象,同时也将其 API 直接暴露给开发人员。这样可以轻松使用每个平台的无数第三方模块。

依赖注入

TypeScript装饰器

装饰器是一种特殊类型的声明,它能够被附加到类声明,属性, 访问符,方法或方法参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入

类装饰器

自动把class的构造函数传入到装饰器的第一个参数target,然后通过prototype可以自定义添加属性和方法

image.png

属性装饰器(PropertyDecorator)

同样使用@符号给属性添加装饰器,他会返回两个参数

  1. 原型对象
  2. 属性的名称

参数装饰器(ParameterDecorator)

同样使用@符号给属性添加装饰器,他会返回三个参数

  1. 原型对象
  2. 方法的名称
  3. 参数的位置(从0开始)

方法装饰器(MethodDecorator)

同样使用@符号给属性添加装饰器,他会返回三个参数

  1. 原形对象
  2. 方法的名称
  3. 属性描述符 可写对应writable,可枚举对应enumerable,可配置对应configurable

image.png

装饰器工厂 (nestjs Get装饰器的实现)

image.png

Nest.js 学习

安装

$ npm i -g @nestjs/cli
$ nest new project-name

基本文件介绍

  1. app.module.ts: 根模块用于处理其他类的引用与共享
  2. app.controller.ts: 常用功能是用来处理http请求以及调用service层的处理方法
  3. app.service.ts: 封装通用的业务逻辑、与数据层的交互、其他额外的一些第三方请求

nest 命令

  1. nest --help: 获取nest命令的详细介绍
  2. nest g mo user: 在user文件夹下生成 user.module.ts 文件
  3. nest g co user: 在user文件夹下生成 user.controller.ts 文件
  4. nest g s user: 在user文件夹下生成 user.service.ts 文件
  5. nest g resource user: 生成如下目录结构
  6. nest g mi logger: 生成中间件 logger.middleware.ts 文件

image.png

RESTful版本控制

附链接:RESTful API如何进行版本控制

  1. 第一步,在main.ts中打开配置 app.enableVersioning(VersioningType.URI), 控制版本的类型可以选择,包括
    • URI
    • 请求参数
    • 自定义Header
    • 媒体类型
  2. 第二步,在 user.controller.ts 中配置
    使用以下设置,http://localhost:3000/v1/user 可以匹配,但 http://localhost:3000/user 不能 配
    @Controller({
    path:"user",
    version:'1'
    }
    
  3. 单个请求配置
    @Get()
    @Version('1')
    

控制器

image.png

注意:@Post请求的请求体内容默认为json,如果需要其他格式请设置请求头

路由参数

使用:id占位,使用进行读取@Params 读取

  1. 方法一:
@Get(':id')
findOne(@Param() params): string {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}
  1. 方法二:
@Get(':id')
findOne(@Param('id') id): string {
  return `This action returns a #${id} cat`;
}

状态码装饰器

状态码默认返回200
通过@HttpCode可以修改返回的状态码 (status

@Post()
@HttpCode(204)
create() {
  return 'This action adds a new cat';
}

Session 插件

  1. 安装
npm i express-session --save
npm i @types/express-session -D
  1. 引入
import * as session from 'express-session' 
app.use(session())
  1. 参数
image.png

注意

  1. 如果后端返回的Cookie前端要接收到,那么请求头需要携带Cookie
  2. axios携带Cookie的方式axios.defaults.withCredentials=true
  3. 直接访问地址默认携带Cookie

提供者

基本使用,注册位置标注

import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserService2 } from './user.service2';
import { UserService3 } from './user.service3';
import { UserController } from './user.controller';
 
@Module({
  controllers: [UserController],
  // 提供者注册
  providers: [
  // 语法糖
   UserService2,
  ]
})
export class UserModule { }

类提供者

useClass

{
    provide: "Xiaoman",
    useClass: UserService
  }, 

值提供者

useValue

{
    provide: "JD",
    useValue: ['TB', 'PDD', 'JD']
}

工厂提供者

useFactory

{
    provide: "Test",
    // inject属性接受一个提供者数组,在实例化过程中,Nest 将解析该数组并将其作为参数传递给工厂函数
    inject: [UserService2],
    useFactory(UserService2: UserService2) {
      return new UserService3(UserService2)
    }
}

支持异步

{
    provide: "sync",
    async useFactory() {
      return await  new Promise((r) => {
        setTimeout(() => {
          r('sync')
        }, 3000)
      })
    }
}

别名提供者

useExisting 语法允许您为现有的提供程序创建别名。这将创建两种访问同一提供者的方法

 {
    provide: 'AliasedLoggerService',
    useExisting: LoggerService,
  },

导出自定义提供者

与任何提供程序一样,自定义提供程序的作用域仅限于其声明模块。要使它对其他模块可见,必须导出它。要导出自定义提供程序,我们可以使用其令牌或完整的提供程序对象

const connectionFactory = {
// 令牌
  provide: 'CONNECTION',
  useFactory: (optionsProvider: OptionsProvider) => {
    const options = optionsProvider.get();
    return new DatabaseConnection(options);
  },
  inject: [OptionsProvider],
};
@Module({
  providers: [connectionFactory],
  // 从这里导出
  exports: ['CONNECTION'],
})
export class AppModule {}

模块

共享模块

image.png
  1. 第一步,编写共享组件,使用exports
@Module({
  controllers: [ListController],
  providers: [ListService],
  exports: [ListService]
})
export class ListModule {}
  1. 第二步,注册共享组件,在需要共享的组件中使用imports
@Module({
  imports: [ListModule],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule { }

注意

在UserModule中引入了ListModule后,不能再用同样的方式在ListModule中引入UserModule

全局模块

使用@Global() 修饰,exports导出,可以不用局部注册

  1. 第一步,使用@Global声明为全局模块
  2. 第二步,使用exports导出模块
  3. 第三步,在根模块AppModule中声明
import { Global, Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';

@Global()
@Module({
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService]
})
export class UserModule { }

动态模块

动态模块主要就是为了给模块传递参数 可以给该模块添加一个静态方法 用来接受参数

  1. 第一步,定义动态模块
interface Options {
  path: string
}
// 动态模块
@Module({
  controllers: [ConfigController],
  providers: [ConfigService],
})
export class ConfigModule {
  static forRoot(option: Options): DynamicModule {
    return {
      // 如果需要注册全局,设置global
      global: true,
      module: ConfigModule,
      providers: [{
        provide: 'config',
        useValue: option?.path
      }],
      exports: [{
        provide: 'config',
        useValue: option?.path
      }]
    }
  }
}
  1. 第二步,注册使用动态模块
imports: [UserModule, ConfigModule.forRoot({path: '/dt'}), ListModule]
  1. 第三步,使用动态模块的
// 使用值提供器,声明方式
@Inject("config") private readonly config: any

中间件

Nest中间件实际上等价于express中间件
nest g mi logger 生成中间件文件

  1. 第一步定义中间件,使用@Injectable()装饰器

logger.middleware.ts

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log('Request...');
    next();
  }
}
  1. 第二步应用中间件,在module中 实现 NestModule ,再在configure(consumer: MiddlewareConsumer)方法中注册中间件
import { Module, NestModule, RequestMethod, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .exclude(
        { path: 'cats', method: RequestMethod.GET },
        { path: 'cats', method: RequestMethod.POST },
        'cats/(.*)',
      )
      .forRoutes({ path: 'cats', method: RequestMethod.GET });
  }
}
  1. forRoutes({path: 'ab*cd',method:RequestMethod.All}),可以使用路由通配符,*被称为通配符
  2. forRoutes()可接受一个字符串多个字符串对象、一个控制器类甚至多个控制器类
  3. 可以使用exclude()排除某些路由。此方法可以采用一个字符串,多个字符串或一个 RouteInfo 对象来标识要排除的路由
  4. 我们可以把中间件定义成一个简单的函数,叫做函数式中间件

多个中间件

为了绑定顺序执行的多个中间件 使用apply()方法进行绑定

consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);

全局中间件

一次性将中间件绑定到每个注册路由 使用use()方法绑定全局

const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);

响应拦截器

拦截器是使用 @Injectable() 装饰器注解的类。拦截器应该实现 NestInterceptor 接口

  1. 第一步,定义一个拦截器,用来格式化返回的数据
import { Injectable, NestInterceptor, CallHandler } from '@nestjs/common'
import { map } from 'rxjs/operators'
import { Observable } from 'rxjs'

interface data<T> {
      data: T
}

@Injectable()
export class Response<T = any> implements NestInterceptor {
      intercept(context, next: CallHandler): Observable<data<T>> {
      // 由于 handle() 返回一个RxJS Observable,我们有很多种操作符可以用来操作流
            return next
                  .handle()
                  .pipe(map(data => {
                        return {
                              data,
                              code: 200,
                              message: "牛逼"
                        }
                  }))
      }
}
  1. 第二步,注册,可以局部注册也可以全局注册

局部注册 请注意,我们传递的是 LoggingInterceptor 类型而不是实例,让框架承担实例化责任并启用依赖注入。另一种可用的方法是传递立即创建的实例:

@UseInterceptors(LoggingInterceptor)
export class CatsController {}

立即创建实例

@UseInterceptors(new LoggingInterceptor())
export class CatsController {}

全局注册

app.useGlobalInterceptors(new Response())

异常过滤器

内置的异常层负责处理整个应用程序中的所有抛出的异常。当捕获到未处理的异常时,最终用户将收到友好的响应。

  1. 第一步,定义一个过滤器
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common'

import { Request, Response } from 'express'
// @Catch() 可以传递多个参数,所以你可以通过逗号分隔来为多个类型的异常设置过滤器。
@Catch(HttpException)
export class HttpFilter implements ExceptionFilter {
      catch(exception: HttpException, host: ArgumentsHost) {
            const ctx = host.switchToHttp()
            const request = ctx.getRequest<Request>()
            const response = ctx.getResponse<Response>()

            const status = exception.getStatus()
            console.log(exception);

            response
                  .status(status)
                  .json({
                        data: exception,
                        time: new Date().getTime(),
                        success: false,
                        path: request.url,
                        code: status
                  })
      }
}
  1. 第二步,注册,可以局部绑定也可以全局绑定

局部绑定

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

全局绑定

app.useGlobalFilters(new HttpFilter())

管道 | 管道验证

内置管道

Nest 自带九个开箱即用的管道,即

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

他们从 @nestjs/common 包中导出,使用举例

@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
  return this.catsService.findOne(id);
}

自定义管道

对象结构验证
依赖包

npm install --save joi
npm install --save-dev @types/joi

类结构验证
依赖包

$ npm i --save class-validator class-transformer
  1. 第一步,定义
import { ArgumentMetadata, HttpException, HttpStatus, Injectable, PipeTransform } from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';

@Injectable()
export class LoginPipe implements PipeTransform {
  async transform(value: any, metadata: ArgumentMetadata) {
    const dto = plainToInstance(metadata.metatype, value)
    // 验证value是否满足验证条件,满足返回空数组
    const error = await validate(dto)
    if (error.length) {
      throw new HttpException(error, HttpStatus.BAD_REQUEST)
    }
    return value;
  }
}
  1. 第二步,局部绑定
@Post()
create(@Body(LoginPipe) createLoginDto: CreateLoginDto) {
  return this.loginService.create(createLoginDto);
}

全局管道

全局绑定

app.useGlobalPipes(new ValidationPipe());

默认值设置

DefaultValuePipe

@Get()
async findAll(
  @Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe) activeOnly: boolean,
  @Query('page', new DefaultValuePipe(0), ParseIntPipe) page: number,
) {
  return this.catsService.findAll({ activeOnly, page });
}

守卫

守卫是一个使用 @Injectable() 装饰器的类。 守卫应该实现 CanActivate 接口。

权限验证

  1. 第一步,编写角色装饰器 nest g d roles
import { SetMetadata } from '@nestjs/common';
export const Roles = (...args: string[]) => SetMetadata('roles', args);
  1. 第二步,编写守卫 nest g gu roles
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Reflector } from '@nestjs/core';
import type { Request } from 'express';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private Reflector: Reflector) { }
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    // 获取设置的属性
    const admin = this.Reflector.get<string[]>('roles', context.getHandler())    
    // 获取query数据
    const req = context.switchToHttp().getRequest<Request>()
    if (admin.includes(req.query.roles as string)){
      return true
    }else{
      return false
    }
  }
}
  1. 第三步,绑定角色,绑定守卫
@Post()
@Roles("admin")
@UseGuards(RolesGuard)
create(@Body(LoginPipe) createLoginDto: CreateLoginDto) {
  return this.loginService.create(createLoginDto);
}

自定义装饰器

参数装饰器

createParamDecorator

import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
  const request = ctx.switchToHttp().getRequest();
  return request.user;
});
  • data:可以接收传递的参数
  • ctx:上下文

聚合装饰器

applyDecorators

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