JS后端框架 Nest.js入门篇

文章说明:此文仅记录博主初次学习nestjs之后的认识和理解,经验尚浅,文章内容仅供参考,如若有误,敬请指正。
阅读建议:搭配文章的侧边栏目录进行食用,体验会更佳哦!

一:多模块应用结构

0.多模块目录结构示例

src
├──cats
│    ├──dto
│    │   └──create-cat.dto.ts
│    ├──interfaces
│    │     └──cat.interface.ts
│    ├─cats.service.ts
│    ├─cats.controller.ts
│    └──cats.module.ts
├──dogs
│    ├──dto
│    │   └──create-dog.dto.ts
│    ├──interfaces
│    │     └──dog.interface.ts
│    ├─dogs.service.ts
│    ├─dogs.controller.ts
│    └──dogs.module.ts
├──app.module.ts
└──main.ts

1.模块划分和定义

模块划分
  • 划分原则:后端通常对一个业务对象的增删改查等操作的完整业务流程(三层)进行统一管理,在nestjs中,统一管理不仅仅是将相关的所有资源放入同一个文件夹,而且引入了模块作为管理单位。
  • nestjs模块主要内容:模块管理文件、三层架构实现文件(controller、service、dao)、相关pojo类文件(dto、vo、bo、do)
普通 / 全局模块案例
  • 模块管理文件
import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

// @Global()// 全局模块,应该只注册一次,最好由根或核心模块注册。
@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService],
})
export class CatsModule {}

2.模块整合

  • 根模块(app.module.ts)
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';
import { DogsModule } from './dogts/dogs.module';

@Module({
  imports: [CatsModule , DogsModule],
})
export class ApplicationModule {}

3.模块通信

模块数据的私有与暴露
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService]// 模块暴露:引入CatsModule模块的其它模块可以访问CatsService。模块私有:模块未暴露的即为私有
})

export class CatsModule {}
模块之间的引入与导出
// 在核心模块下引入并导出公共模块
@Module({
  imports: [CommonModule],
  exports: [CommonModule],
})
export class CoreModule {}

二:控制器(controller)

控制器的职责:

  • 接收请求并获取请求数据
  • 传递请求数据进行业务调用
  • 根据业务调用结果构造返回响应

控制器的配置:根模块(app.module.ts)中配置控制器 controllers: [AppController],

0.单线程处理

  • Node.js 不遵循请求/响应多线程无状态模型,每个请求都由主线程处理。
  • 当请求完整周期内涉及到或可能涉及到异步操作时,可使用 async / await进行异步处理

1.接收请求并获取请求数据

匹配请求
  • 匹配处理:装饰器建立请求与controller的映射。
  • 匹配规则:请求前缀prefix(class前缀、method前缀,支持通配符*) 、请求方法 GET/POST/…
  • 动态url匹配
  • 子域路由:根据请求的host来判断接口的调用权限
访问request
请求信息装饰器方式访问普通对象访问
请求@Request()req
请求session@Session()req.session
请求参数@Param(key?: string)req.params/req.params[key]
请求参数@Body(key?: string)req.body/req.body[key]
请求参数@Query(key?: string)req.query/req.query[key]
请求头@Headers(name?: string)req.headers/req.headers[name]
请求IP@Ip()req.ip
  • 案例
import { Controller, Get ,Req, Ip, Post,Query,Param,Body} from '@nestjs/common';
import { AppService } from './app.service';
import { Request, query } from 'express';
// import {CreateCatDto} from './DTO/catDto'
class CreateCatDto {
  readonly name: string;
  readonly age: number;
  readonly breed: string;
}
@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('test')
  getHello(@Req() request: Request, @Query('name') name:String,@Ip() ip:String): string {
    console.log(`请求参数name为(普通对象): ${request.query['name']}`)
    console.log(`请求参数name为(装饰器): ${name}`)
    console.log(`ip地址为(普通对象): ${request.ip}`)
    console.log(`ip地址为(装饰器): ${ip}`)
    return this.appService.getHello();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return `This action returns a #${id} cat`;
  }

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {// POST请求参数获取,@Body()配合DTO类
    return 'This action adds a new cat';
  }
}

2.传递请求数据进行业务调用

  • 引入业务类:import { AppService } from ‘./app.service’;
  • 调用业务类的方法: return this.appService.getHello();

3.根据业务调用结果构造返回响应

访问response
响应信息装饰器方式访问普通对象访问
响应@Response() @Res()*res
构造状态行
  • 状态码:@HttpCode(204)
构造响应头
  • 响应头:@Header(‘Cache-Control’, ‘none’)
构造响应体
  • 返回页面/基本类型
  • 返回对象/数组:自动序列化为JSON
重定向
  • 基本语法:@Redirect(‘https://nestjs.com’)
  • 动态url:controller方法 return { url: ‘https://docs.nestjs.com/v5/’ };

二:控制反转IOC

  • 基本理解:可以看作为工厂模式的升华,能够解开对象引用与对象创建之间的耦合。
  • 实现方式:通过在全局维护一个IOC容器,来统一管理对象的配置和创建(依赖注入)。

1.IOC创建对象案例

1):定义一个provider类(cats.service.ts)
普通provider,注入时等同于 new CatsService(httpClient)
import { Injectable } from '@nestjs/common';
@Injectable()
export class CatsService {
 constructor(
   private readonly httpClient: HttpClientClass
  ) {}
  test(): String {
    return 'test provider1';
  }
}
泛型Provider,注入时等同于 new DogsService< HttpClientClass>(httpClient)
  • 基于构造函数的注入
import { Injectable } from '@nestjs/common';
// 2.多级provider = new DogsService(HttpClientClass1)
@Injectable()
export class DogsService<T>{
 constructor(
   //@Optional() 可选注入
   @Optional() @Inject('HTTP_OPTIONS') private readonly httpClient: T
  ) {}
  test(): String {
    return 'test provider2';
  }
}
  • 基于属性的注入
import { Injectable } from '@nestjs/common';
@Injectable()
export class DogsService<T>{
  @Inject('HTTP_OPTIONS') 
  private readonly httpClient: T;
  test(): String {
    return 'test provider2';
  }
}
2):将该provider类注册至IOC容器中(app.module.ts)
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class AppModule {}
3):从IOC容器中获取该provider类对象(cats.controller.ts)
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  // provider可通过`constructor` **注入**依赖关系
  constructor(private catsService: CatsService) {}
  
  @Post()
  async test() {
    this.catsService.test();
  }

}

四:切面Middleware(常用于请求预处理、响应后处理)

1.作为切面

1):切面类型:NestMiddleware接口实现类
2):切面输入
  • Request对象
  • Response对象
  • next函数
3):切面输出:next进入下一个切面
4):切面使用场景
  • 执行任何代码。
  • 对请求和响应对象进行更改。
  • 结束请求-响应周期。
  • 调用堆栈中的下一个中间件函数。
  • 如果当前的中间件函数没有结束请求-响应周期, 它必须调用 next() 将控制传递给下一个中间件函数。否则, 请求将被挂起。

2.简单案例

1):中间件定义
  • 方式1:类中间件
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  // 中间件定义:必须是NestMiddleware接口的实现类并且实现use方法,参数为请求、响应、转发到下一个中间件的方法
  use(req: Request, res: Response, next: Function) {
    console.log('Request...');
    next();
  }
}
  • 方式2:函数式中间件
export function logger(req, res, next) {
  	console.log(`Request...`);
  	next();
}
2):中间件注册
  • 模块注册(app.module.ts)
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';
import { CatsController } from './cats/cats.controller.ts';

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  // 中间件注册:不在@module中配置,而是在NestModule接口的configure方法中注册一个中间件消费者(MiddlewareConsumer)实例,在该实例中配置该中间件消费者的消费特征(应用哪个中间件、处理哪些请求、排除哪些请求)。
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)// 可配置多个中间件
      .exclude(
    { path: 'cats', method: RequestMethod.GET },
    { path: 'cats', method: RequestMethod.POST },
    'cats/(.*)',
  )
      // forRoutes配置经过中间件的请求有哪些,可接受一个字符串、多个字符串、对象、一个控制器类甚至多个控制器类,传对象时path属性支持通配符*。
      .forRoutes(CatsController);
  }
}
  • 全局注册(main.ts)
const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);

五:切面Guard(常用于权限管理)

1.作为切面

此文暂不对细节展开探讨

1):切面类型:CanActivate接口实现类
2):切面输入
ExecutionContext对象:执行上下文对象,继承至参数主机对象ArgumentsHost

此文关注点不在ExecutionContext对象,在此不做细节探讨。

export interface ExecutionContext extends ArgumentsHost {
  getClass<T = any>(): Type<T>;
  getHandler(): Function;
}
3):切面输出:继续请求 or 返回响应
  • return boolean | Promise | Observable
4):切面使用场景
  • 权限管理

2.权限管理案例

权限管理可通过守卫技术实现,守卫之名顾名思义,可负责鉴权,符合权限的请求通过,不符合的直接响应。

1):给控制器绑定权限(给门贴上通过规则)
  • 自定义元数据装饰器
// 自定义权限装饰器,@SetMetadata()装饰器拥有将定制元数据附加到路由处理程序的能力,可用于控制器绑定权限以便读取
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
  • 使用元数据装饰器
@Post()
// @SetMetadata('roles', ['admin']) // 方式1:使用@SetMetadata装饰器
@Roles('admin') // 方式2:使用自定义装饰器封装@SetMetadata装饰器
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}
2):给控制器绑定守卫者(给门分配站岗门卫)
  • 定义守卫者
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@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);
  }
}
  • 绑定守卫者
// 方式1:方法守卫(controller上)
@UseGuards(new RolesGuard())
// 方式2:全局守卫(app.module.ts)
@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
  ],
})
3):守卫者鉴权失败(被门卫叉刀拦下)
  • 默认响应
{
  "statusCode": 403,
  "message": "Forbidden resource"
}
  • 自定义响应
// 守卫者抛出异常
throw new UnauthorizedException(); 

六:切面Interceptor

1.作为切面

1):切面类型:NestInterceptor接口实现类
2):切面输入
ExecutionContext对象:执行上下文对象,继承至参数主机对象ArgumentsHost
next函数
3):切面输出:next进入下一个切面(可携带后拦截器逻辑)
4):切面使用场景
  • 在函数执行之前/之后绑定额外的逻辑
  • 转换从函数返回的结果
  • 转换从函数抛出的异常
  • 扩展基本函数行为
  • 根据所选条件完全重写函数 (例如, 缓存目的)

2.拦截器简单案例

1):拦截器定义
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    // return之前:前拦截器逻辑
      console.log('Before...');

    const now = Date.now();
    return next
      .handle()// handle之后:后拦截器逻辑
      .pipe(
        // catchError(err => throwError(new BadGatewayException())),// 覆盖异常,在异常过滤器切面之前
        tap(() => console.log(`After... ${Date.now() - now}ms`)),
      );
  }
}
2):拦截器绑定
  • 绑定到方法
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
  • 绑定到全局(app.module.ts)
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: LoggingInterceptor,
    },
  ],
})
export class AppModule {}

七:切面Pipe(常用于数据校验、数据转换)

1.作为切面

1):切面类型:PipeTransform接口实现类
2):切面输入
value:流入管道的值
ArgumentMetadata:该value的元数据信息
export interface ArgumentMetadata {
  type: 'body' | 'query' | 'param' | 'custom';
  metatype?: Type<unknown>;
  data?: string;
}
3):切面输出:管道流出的值 or 抛出异常返回响应
4):切面使用场景
  • 数据校验
  • 数据转换

2.数据校验案例(类验证器方式)

输入数据通过验证管道验证,成功返回原数据,失败则请求结束返回数据校验异常响应。

1):自动校验工具(声明式校验)
  • 安装工具
npm i --save class-validator class-transformer
  • 验证管道(通常内置管道ValidationPipe即可满足需求,当然也可使用自定义管道)
2):校验规则声明

class-validator 文档查看所有类型约束

import { IsString, IsInt } from 'class-validator';
// CreateCatDto 实例的校验规则
export class CreateCatDto {
  @IsString()
  name: string;

  @IsInt()
  age: number;

  @IsString()
  breed: string;
}
3):数据绑定校验管道
  • 参数级别绑定
@Post()
async create(@Body(new ValidationPipe()) createCatDto: CreateCatDto类中) {// 使用内置验证管道ValidationPipe验证数据createCatDto,规则定义在createCatDto对象的类定义中
  this.catsService.create(createCatDto);
}
  • 方法级别绑定
@Post()
@UsePipes(ValidationPipe)// provider依赖注入
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}
  • 全局级别绑定(main.ts)
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

3.数据转换案例

1):管道函数
  • 使用内置转换管道
  • 自定义转换管道
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
// 此管道ParseIntPipe已经内置
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
  transform(value: string, metadata: ArgumentMetadata): number {
    const val = parseInt(value, 10);
    if (isNaN(val)) {
      throw new BadRequestException('Validation failed');
    }
    return val;
  }
}
2):数据绑定转换管道
@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
  return await this.catsService.findOne(id);
}

八:切面ExceptionFilter(常用于全局异常处理)

1.作为切面

1):切面类型:ExceptionFilter接口实现类
2):切面输入
HttpException异常对象
ArgumentsHost参数主机对象
3):切面输出:构造异常响应
4):切面使用场景
  • 异常处理

2.异常处理

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

1):不处理
  • 全局捕获后返回的响应结构
{
    "statusCode": 500,
    "message": "Internal server error"
}
2):一般需求处理
  • 手动抛出
// 方式1:直接抛出HttpException,异常信息为forbidden
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
// 方式2:直接抛出HttpException,异常信息为object
throw new HttpException({
    status: HttpStatus.FORBIDDEN,
    error: 'This is a custom message',
  }, HttpStatus.FORBIDDEN);
// 方式3:抛出HttpException子类(内置异常/自定义异常)
throw new UnAuthenticationException();
// 自定义异常类UnAuthenticationException
// export class UnAuthenticationException extends HttpException {
//   constructor() {
//     super('not authentication', 30000);
//   }
// }
  • 全局捕获后返回的响应结构
// 方式1
{
    "statusCode": 403,
    "message": "Forbidden"
}
// 方式2
{
  "status": 403,
  "error": "This is a custom message"
}
// 方式3
{
    "statusCode": 30000,
    "message": "not authentication"
}
3):复杂需求处理

在异常捕获到具体响应之前,如果有需要添加aop切面处理的需求(如日志记录等),可以通过添加异常过滤器的形式实现。

手动抛出(此切面不关注)
异常过滤器捕获
  • 异常过滤器定义
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)// @Catch()会捕获不经手动抛出的异常(即第一种不作处理的异常)
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(HttpExceptionFilter) // 通过@UseFilters这个装饰器,使被装饰的方法绑定HttpExceptionFilter这个异常过滤器
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}
// 另一种:也可绑定为全局/模块异常过滤器
全局捕获后返回的响应结构
{
  statusCode: 'status:xxx',
  timestamp: 'date:xxx',
  path: 'url:xxx',
}
  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值