likes
comments
collection
share

Nestjs 实战系列(一)—— Nest框架

作者站长头像
站长
· 阅读数 69

简介

Nest (NestJS) 是一个用于构建高效、可扩展的 Node.js 服务器端应用程序的开发框架。它利用 JavaScript 的渐进增强的能力,使用并完全支持 TypeScript (仍然允许开发者使用纯 JavaScript 进行开发),并结合了 OOP (面向对象编程)、FP (函数式编程)和 FRP (函数响应式编程)。

Nest 在这些常见的 Node.js 框架 (Express/Fastify) 之上提高了一个抽象级别,但仍然向开发者直接暴露了底层框架的 API。这使得开发者可以自由地使用适用于底层平台的无数的第三方模块。

一、框架基础

1、安装nest命令,快速构建nestjs工程

npm i -g @nestjs/cli

安装完成后输入nest --version确认是否安装成功

2、初始化nest工程

(1)nest new

(2)输入工程名

(3)选怎包管理器(默认npm)

Nestjs 实战系列(一)—— Nest框架

(4)进入工程目录,执行npm run start 启动工程,默认3000端口,查看127.0.0.1:3000

Nestjs 实战系列(一)—— Nest框架

3、初始工程组织结构

Nestjs 实战系列(一)—— Nest框架

(1)程序入口: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();

(2)程序根模块:app.module.ts,所有需要初始化的模块

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

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

\

(3)控制器(controller):app.controller.ts,程序处理特定请求的模块

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

(4)服务提供者(serviceProvider):app.service.ts,为控制器提供服务,处理业务逻辑

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

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

4、script 命令生成新的控制器

nest g co

输入控制器名称,如下文的coffees

Nestjs 实战系列(一)—— Nest框架

程序自动生成相应模块并在入库添加该模块

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CoffeesController } from './coffees/coffees.controller';

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

基础方法实例

import { Controller, Get } from '@nestjs/common';
@Controller('coffees')
export class CoffeesController {
  @Get()
  findAll() {
    return "all coffees";
  }
}

Nestjs 实战系列(一)—— Nest框架

5、获取请求体中的参数

/***** get请求 *****/
@Get()
findAll(@Query() query) {
  const { limit, offset } = query
  response.status(200).send(`limit:${limit}, offset:${offset}`);
}

// 动态获取参数
@Get(':id')
findOne(@Param('id') id: string) {
  return `return #${id} coffee`;
}


/***** post请求 *****/
// 获取全部参数
@Post()
create(@Body() body) {
  return body;
}
// 获取参数中某一属性(获取参数中name属性)
@Post()
create(@Body('name') body) {
  return body;
}

6、请求返回状态码

两种方式设置状态码

一种是express框架的原生方式

@Get('flavors')
findAll(@Res() response) {
    response.status(200).send('all coffees');
}

另外一种使用nestjs装饰器@HttpCode

@Post()
@HttpCode(HttpStatus.ACCEPTED)
create(@Body('name') body) {
    return body;
}

!!!注意: 推荐使用第二种

第一种会影响nest功能的一些兼容性,比如两种不能混用,且第一种会影响nest框架拦截器的使用;

一旦使用了@Res()注解,则必须使用response.send 才能返回。

7、服务(service)

创建服务命令

nest g s

Nestjs 实战系列(一)—— Nest框架

@Module({
  imports: [],
  controllers: [AppController, CoffeesController],
  providers: [AppService, CoffeesService],
})
export class AppModule {}
import { Injectable } from '@nestjs/common';

@Injectable()
export class CoffeesService {
   constructor(
     @InjectRepository(Coffee)
     private readonly coffeeRepository: Repository<Coffee>,
  ) {}
}

@Injectable()装饰器表示该类是可注入的

在控制器中通过构造函数注入服务

private readonly相当于java中的采用单例方式依赖注入服务

constructor(private readonly coffeeService:CoffeesService){}

8、异常处理(exception)

nest提供强大的异常处理功能,开发者可以定义在何种情况下抛出何种错误,甚至可以通throw关键字抛出自定义错误,比如

throw 'a random exception'

保证所有的错误都冒泡

import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { Coffee } from './entities/coffee.entity';

@Injectable()
export class CoffeesService {
  private coffees: Coffee[] = [
    {
      name: 'aaa',
      brand: 'bbbbb',
      recommendations: 6,
      flavors: ['cccc', 'ddddd']
    }
  ]

  findOne(name: String): Coffee {
    const coffee = this.coffees.find(item => item.name === name);
    if (!coffee) {
      throw new HttpException(`coffee ${name} not found`, HttpStatus.NOT_FOUND)
    }
    return coffee;
  }
}

9、模块(module)

模块可以封装该模块相关的控制器,服务提供者,方便将服务分组、模块化

通过命令

nest g module coffees

即可生成

Nestjs 实战系列(一)—— Nest框架

app.module.ts import中自动写入CoffeesModule

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

分组之后

coffee.module.ts

import { Module } from '@nestjs/common';
import { CoffeesController } from './coffees.controller';
import { CoffeesService } from './coffees.service';

@Module({
  controllers: [CoffeesController],
  providers: [CoffeesService]
})
export class CoffeesModule { }

// app.module.ts变为
@Module({
  imports: [CoffeesModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

生命周期事件

生命周期事件在应用初始化与终止时发生。Nest在 modules ,injectables 和 controllers 的以下每个生命周期事件(首先要使能shutdown钩子,如下描述)中调用注册钩子方法。和上图所示的一样,Nest也调用合适的底层方法来监听连接,以及终止监听连接。

在下述表格中,onModuleDestroy, beforeApplicationShutdown 和 onApplicationShutdown 仅仅在显式调用 app.close() 或者应用收到特定系统信号(例如 SIGTERM)并且在初始化时(参见下表的应用shutdown部分)正确调用了enableShutdownHooks方法后被触发。

生命周期钩子方法生命周期时间触发钩子方法调用
OnModuleInit()初始化主模块依赖处理后调用一次
OnApplicationBootstrap()在应用程序完全启动并监听连接后调用一次
OnModuleDestroy()收到终止信号(例如SIGTERM)后调用
beforeApplicationShutdown()在onModuleDestroy()完成(Promise被resolved或者rejected);一旦完成,将关闭所有连接(调用app.close() 方法).
OnApplicationShutdown()连接关闭处理时调用(app.close())

使用案例

app.service.ts

注意class需要: ****implements OnModuleInit

import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { addUserInfo } from './user/user.service';
import { addRootDepartment } from './department/department.service';

@Injectable()
export class AppService implements OnModuleInit {
  public constructor(private readonly configService: ConfigService) {}
  // 下面使用Nestjs的生命周期函数
  // 初始化主模块依赖处理后调用一次
  onModuleInit() {
    console.log(`onModuleInit...`);
  }
  // 在应用程序完全启动并监听连接后调用一次
  onApplicationBootstrap() {
    console.log(`onApplicationBootstrap...`);
    //读取配置文件
    Init(this.configService);
  }
  // 	连接关闭处理时调用(app.close())
  OnApplicationShutdown() {
    console.log(`OnApplicationShutdown...`);
  }

  getHello() {
    return 'hello';
  }
}

// 项目初始化操作: 初始化数据库
async function Init(configService) {
  try {
    const depart = await addRootDepartment(configService.get('INITDATA.DEPART'));
    await addUserInfo({
      ...configService.get('INITDATA.USER'),
      department: { id: depart.id },
    });
    console.log('已初始化系统根组织机构与超管账户!');
  } catch (error) {
    console.error('!!!系统初始化数据失败!', error.message);
  }
}

Nest.js的核心技术: MVC、IOC、AOP

MVC,IOC,AOP是Nest.js编程上最常用的核心技术,而这些技术类似于Java的Spring, 在社区上,有一句对它的描述: "JS版的Spring"。

(1)MVC

MVC是Model View Controller的简写。也就是模型层,视图层,控制层。 它跟MVVM架构不一样,MVC架构下,请求会先发送到控制层(C) ,再由它调度模型层(M)的Service来完成业务逻辑, 最后返回对应的视图层(V)。 下面是它的逻辑图。 复制代码

Nestjs 实战系列(一)—— Nest框架

(2)IOC

  • IOC是Inverse Of Control的简写(控制反转)。简单来说就是通过@Controller、@Injectable等装饰器声明,类(class) 就会被Nest.js扫描,创建对应的对象并添加到一个容器里,即依赖“自动注入” ,也就是DI(dependency inject),依赖注入的思想。
  • IOC架构的好处就是不需要手动创建对象和根据依赖关系传入不同对象的构造器中,一切都是自动扫描并创建、注入的。

Nestjs 实战系列(一)—— Nest框架

Nestjs 实战系列(一)—— Nest框架

(3)AOP

AOP架构又是什么呢?AOP是Aspect Oriented Program的简写。也就是面向切面编程。

在Nest中,一个请求过来,会经过Controller到Service再到Repository。也就是MVC的一个流程。 在进入这个Controller之前呢,我们会进行一个JWT或者异常处理,日志处理等等。

如果直接在改造Controller控制层的代码,这显然很不优雅,可能还会使控制器变得无法控制。

所以在进入控制层(C)之前,就进行了一个切面,在进入Controller之前做一些处理。 如下图:

Nestjs 实战系列(一)—— Nest框架

在Nest.js实现AOP的方式有5种:Gurad(守卫)、Pipe(管道)、Interceptor(拦截器)、ExceptionFilter(异常过滤器)、还有Middleware(中间件)。

而中间件来实现AOP比较特别,因为Express的中间件洋葱模型,它可以透明的在外面包一层,加入一些逻辑,内层感知不到。Nest.js的Middleware中间件是直接继承于Express框架的。

下面是AOP的一个例子代码: 复制代码

Nestjs 实战系列(一)—— Nest框架

上面的代码对单独的路由进行了Gurad和Interceptor加上了两层透明的逻辑(权限控制以及拦截器),这就是AOP的好处。

AOP的五种方式执行顺序

Nest.js实现AOP的方式有5种: Gurad(守卫)、Pipe(管道)、Interceptor(拦截器)、ExceptionFilter(异常过滤器)、还有Middleware(中间件)。其AOP在Nest.js的调用顺序可以由下面流程图表示:

Nestjs 实战系列(一)—— Nest框架

下面我们就依次介绍这五种AOP的使用:

中间件

在src 统计新建一个common文件夹,接着在里面再新增一个middleware文件夹(这个里面放所有有关中间件文件),下面简单做个请求日志的中间件.

新增

在middleware文件夹中新增一个logger.middleware文件

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

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: () => void) {
    const { method, path, query, body } = req;
    let params = query;
    if (method == 'POST') {
      params = body;
    }
    console.log(`${method} ${path} params values:${JSON.stringify(params)}`);
    next();
  }
}

使用

在 app.module文件中使用

import { Module,MiddlewareConsumer,RequestMethod } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CoffeesModule } from './coffees/coffees.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SvncatalogueModule } from './svncatalogue/svncatalogue.module';
import { UploadfileModule } from './uploadfile/uploadfile.module';
import {LoggerMiddleware} from './common/middleware/logger.middleware'

@Module({
  imports: [
    CoffeesModule,
    TypeOrmModule.forRoot(),
    SvncatalogueModule,
    UploadfileModule
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {
    configure(consumer: MiddlewareConsumer) {
    // 为路由添加中间件
    // consumer.apply(LoggerMiddleware).forRoutes(''); // 空串表示所有接口都会走该中间件,也可指定某路由path

    //指定projectManage controller的路由走该中间件
    consumer
      .apply(LoggerMiddleware)
      // 排除某接口的post请求,也支持配置多个,但是此处/halfMonthApi不可少
      // .exclude({ path: '/halfMonthApi/projectManage/closeProject', method: RequestMethod.POST })

      // 所有接口都会走该中间件
      // .forRoutes('');

      // 定projectManage controller的路由走该中间件
      // .forRoutes(ProjectManageController);

      // 应用于哪些路由,path需要准确,但是不需要/halfMonthApi
      .forRoutes({ path: '/user/list', method: RequestMethod.GET });
  }
}

注意: 目前测试的结果是 exclude 时候path要加上app.setGlobalPrefix('/halfMonthApi') 的前缀"/halfMonthApi",

但forRoutes时却不需要加前缀

结果

Nestjs 实战系列(一)—— Nest框架

支持多个中间件的配置

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

路由守卫Guard

熟悉Vue的伙伴应该比较熟悉这个概念,通俗的说就是在访问指定的路由之前回调一个处理函数,如果该函数返回true或者调用了next()就会放行当前访问,否则阻断当前访问。

NestJs中路由守卫也是如此,通过继承CanActive接口即可定义一个路由守卫。

定义路由守卫

// src/guard/user.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Request } from 'express';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext,): boolean | Promise<boolean> | Observable<boolean> {
  // 在这里可以写一些你要的代码 TODO
  const request = context.switchToHttp().getRequest<Request>();
    // 读取token 或者一些其它请求头的信息
    const authorization = request.header('auth');
    if (!authorization) {
      return false;
    }
    return true;
  }
}
  • 在上述代码中,如果返回true,则可以访问程序,如果false则无法访问

路由守卫级别

1.控制器级别

该级别会对被装饰控制器的所有路由方法生效。

import { Controller, Get, Post, Body, UseGuards,Headers } from '@nestjs/common';
import { AuthGuard } from 'src/guard/user.guard'
@UseGuards(AuthGuard)
export class UserController {
  constructor(
    private readonly userService: UserService,
    private readonly projectManageService: ProjectManageService,
  ) {
    this.projectManageService = projectManageService;
  }
  @Get('list')
  async findAll(@Headers() headers) {  //有传递auth
  try {
    const list = await this.userService.findAll();
    return ResultData.success(list);
   } catch ({ message }) {
    return ResultData.fail(AppHttpCode.COMMON_ERR, message);
   }
  }
  // 查看当前用户信息
  @Get('info')
  info() {
    return {username: '模拟查询到的信息'};
  }
}
2.方法级别
import { Controller, Get, Post, Body, UseGuards,Headers } from '@nestjs/common';
import { AuthGuard } from 'src/guard/user.guard'
@Get('list')
@UseGuards(AuthGuard)
async findAll(@Headers() headers) {  //有传递auth
  try {
    const list = await this.userService.findAll();
    return ResultData.success(list);
  } catch ({ message }) {
    return ResultData.fail(AppHttpCode.COMMON_ERR, message);
  }
}
3.全局级别
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // 由于main.ts启动时并未初始化依赖注入容器,所以依赖必须手动传入,一般情况下不建议使用全局守卫,因为依赖注入得自己解决。
  app.useGlobalGuards(new UserGuard(new UserService()));
  await app.listen(3000);
}

bootstrap();

异常处理

路由守卫返回false时框架会抛出ForbiddenException,客户端收到的默认响应如下:

{
  "statusCode": 403,
  "message": "Forbidden resource"
}

如果需要抛出其他异常,比如UnauthorizedException,可以直接在路由守卫的canActive()方法中抛出。

另外,在这里抛出的异常时可以被异常过滤器捕获并且处理的,所以我们可以自定义异常类型以及输出自定义响应数据。

拦截器

拦截成功的返回数据

首先使用命令创建一个拦截器:

nest g interceptor core/interceptor/transform

Nestjs 实战系列(一)—— Nest框架

拦截器代码实现:

import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { map, Observable } from 'rxjs';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map((data) => {
        return {
          data,
          code: 200,
          msg: '请求成功',
        };
      }),
    );
}
}

在main.ts中全局注册:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { TransformInterceptor } from './core/interceptor/transform.interceptor';
import {HttpExceptionFilter} from './core/filter/http-exception.filter'
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // 全局注册拦截器
  app.useGlobalInterceptors(new TransformInterceptor())
  // 全局请求前缀
  app.setGlobalPrefix('/nestApi');
  await app.listen(3000);
}
bootstrap();

请求失败时返回:

{
  "code": 400,
    "message": "error reason",
    "data": {}
}
拦截错误请求

首先使用命令创建一个过滤器:

nest g filter core/filter/http-exception

Nestjs 实战系列(一)—— Nest框架

过滤器代码实现:

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

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp(); // 获取请求上下文
    const response = ctx.getResponse(); // 获取请求上下文中的 response对象
    const status = exception.getStatus(); // 获取异常状态码

    // 设置错误信息
    const message = exception.message
      ? exception.message
      : `${status >= 500 ? 'Service Error' : 'Client Error'}`;
    const errorResponse = {
      data: {},
      msg: message,
      code: status,
    };

    // 设置返回的状态码, 请求头,发送错误信息
    response.status(status);
    response.header('Content-Type', 'application/json; charset=utf-8');
    response.send(errorResponse);
  }
}

在main.ts中全局注册:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { TransformInterceptor } from './core/interceptor/transform.interceptor';
import {HttpExceptionFilter} from './core/filter/http-exception.filter'
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // 全局注册拦截器
  app.useGlobalInterceptors(new TransformInterceptor())
  // 全局请求前缀
  app.setGlobalPrefix('/nestApi');
  //过滤处理 HTTP 异常
  app.useGlobalFilters(new HttpExceptionFilter())
  await app.listen(3000);
}
bootstrap();

这样对请求错误就可以统一的返回了,返回请求错误只需要抛出异常即可

throw new HttpException('id不存在', 401);

请求数据验证DTO(管道Pipe)

你是否曾经为了验证参数,写了一大堆 if - else ?然后还要判断各种参数类型?相似的结构在不同的方法里判断,却又要复制一遍代码?

使用 DTO 可以清晰的了解对象的结构,使用 Pipes(管道)配合 class-validator 还可以对参数类型进行判断,还可以在验证失败的时候抛出错误信息。

接收一个接口请求,如:'/xxx',然后我获取到了这个请求的参数req,但是我却不知道这个请求的参数req里面的内容是否有效(字段是否合格),Nest使用 Class 来做 DTO,即数据传输对象(Data Transfer Object),是一种设计模式之间传输据的软件应用系统。

我们使用管道来帮我验证参数。通俗来讲就是,对请求接口的入参进行验证和转换操作,验证合格才会将内容给到路由对应的方法中去,失败了就进入异常过滤器中。

管道是具有 @Injectable() 装饰器的类。管道应实现 PipeTransform 接口。

管道有两个类型:

  • 转换:管道将输入数据转换为所需的数据输出
  • 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常;

安装

npm install --save class-validator class-transformer

Nest.js自带了三个开箱即用的管道:ValidationPipe、ParseIntPipe和ParseUUIDPipe, 其中ValidationPipe 配合class-validator就可以完美的实现我们想要的效果(对参数类型进行验证,验证失败抛出异常)。

ValidationPipe

管道验证操作通常用在dto这种传输层的文件中用作验证操作,如:

创建dto
import { IsNotEmpty, IsNumber } from 'class-validator';
export class CreateUserDto {
  @IsNotEmpty({ message: '项目名称不允许为空' })
  projectName: string;

  @IsNotEmpty({ message: '项目令号不允许为空' })
  projectNum: string;
}
readonly
import { IsString } from 'class-validator';
// 添加readonly之后可以防止参数修改,?表示参数可选
export class CreateCoffeeDto {
  readonly name?: String
  @IsString({ each: true }) // 每一项数组内容必须为string
  readonly flavors: string[];
}

@Post()
create(@Body() createCoffee: CreateCoffeeDto) {
    createCoffee.name = "卡布奇诺" // 这里ts会报错,因为不可以修改只读属性
    return createCoffeeDto;
}
dto 类型继承
使用dto
import { CreateUserDto } from '../dto/project.dto';
//新增&&编辑项目
  @Post('addProject')
  async addUser(@Body() createParam: CreateUserDto) {
    try {
      const data = await this.projectManageService.addProject(createParam);
      return ResultData.success(data);
    } catch (err) {
      return ResultData.fail(AppHttpCode.COMMON_ERR, err?.message);
    }
  }
开启管道【推荐】
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  //设置全局管道
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true, // 删除请求参数中不在 DTO 中的所有属性
    }),
  );
  ...  ...
  • whitelist: true // 删除请求参数中不在 DTO 中的所有属性

如上面的“CreateUserDto”约定了projectName、projectNum两个属性,如在接口调用时传递参数为{projectName: "学生教育管理系统", projectNum:'Stu', address:"南京"},则在controller中接收到的参数就去除address

ValidationPipe的配置除了whiteList外还有一些其他属性,大姐也可自行研究。

自定义管道

如果有特殊需求可以采用自定义管道的实现方式,对传入参数和类型做定制化特殊处理,案例如下:

// src/pipe/validation.pipe.ts
import { ArgumentMetadata, Injectable, PipeTransform, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';

@Injectable()
export class ValidationPipe implements PipeTransform {
  async transform(value: any, { metatype }: ArgumentMetadata) {
    console.log(`value:`, value, 'metatype: ', metatype);
    if (!metatype || !this.toValidate(metatype)) {
      // 如果没有传入验证规则,则不验证,直接返回数据
      return value;
    }
    // 将对象转换为 Class 来验证
    const object = plainToClass(metatype, value);
    const errors = await validate(object);
    if (errors.length > 0) {
      const msg = Object.values(errors[0].constraints)[0]; // 只需要取第一个错误信息并返回即可
      throw new BadRequestException(`Validation failed: ${msg}`);
    }
    return value;
  }

  private toValidate(metatype: any): boolean {
    const types: any[] = [String, Boolean, Number, Array, Object];
    return !types.includes(metatype);
  }
}
使用自定义管道
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';
import { ValidationPipe} from './pipe/validation.pipe.ts'
async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  //全局管道
  app.useGlobalPipes(new ValidationPipe());
  //开启服务
  await app.listen(3000);
}
bootstrap();

ParseIntPipe

转换传入的参数为数字

  //根据条件查询
  @Get(':id')
  findOne(@Param('id') id: number) {
    console.log(typeof id)   //string
    return this.coffeeService.findOne(id);
  }
  //根据条件查询
  @Get(':id')
  findOne(@Param('id',new ParseIntPipe()) id: number) {
    console.log(typeof id) //'number'
    return this.coffeeService.findOne(id);
  }

如:传递过来的是/test?id=‘123’"这里会将字符串‘123’转换成数字123

ParseUUIDPipe

验证字符串是否是 UUID(通用唯一识别码)

  //根据条件查询
  @Get(':id')
  findOne(@Param('id',new ParseUUIDPipe()) id: number) {
    return this.coffeeService.findOne(id);
  }

如:传递过来的是/test?id=‘xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx’"这里会验证格式是否正确,不正确则抛出错误,否则调用findOne方法

Nestjs 实战系列(一)—— Nest框架

接口格式统一处理

一般开发中是不会根据HTTP状态码来判断接口成功与失败的, 而是会根据请求返回的数据,里面加上code字段

定义返回的json格式:

{
  "code": 200,
    "msg": "OK",
    "data": {
  }
}

返回Http状态码

统一约定返回的状态码code值

common/code.enum.ts

export enum AppHttpCode {
  SUCCESS = 200,
  AUTH_FAIL = 401, // "未授权,认证失败"
  LOGIN_FAIL = 417, // "账号密码错误",
  COMMON_ERR = 400, // 其他异常
  // 402: "登录请求——获取应用信息异常",
  // 403: "登录请求——获取角色信息失败",
  // 404: "登录请求——账户userId不存在",
  // 405: "登录请求——token生成失败",
  // 406: "该账号已休眠",
  // 407: "不在可访问时间段",
  // 408: "您的客户端禁止访问",
  // 409: "登录用户数已达最大限制",
  // 410: "该账号已注销",
  // 411: "您的临时账号已过期"
  // 430: "该账号已锁定"
}

结构封装与案例

封装返回结构类,直接供controller调用返回

common/resultData.ts

import { AppHttpCode } from '../common/code.enum';
export class ResultData {
  constructor(code = AppHttpCode.SUCCESS, msg: string, data?: any) {
    this.code = code;
    this.msg = msg;
    this.data = data;
  }
  code: number;
  msg: string;
  data?: any;

  static success(data?: any, msg?: string): ResultData {
    return new ResultData(AppHttpCode.SUCCESS, msg || '请求执行成功!', data);
  }
  static fail(code: number, msg: string, data?: any): ResultData {
    return new ResultData(code || AppHttpCode.COMMON_ERR, msg || '请求异常请稍后重试!', data);
  }
}

使用案例

@Get('queryList')
  async queryList(@Query() query: QueryProjectDto, @Headers() headers) {
    try {
      const userInfo = headerUserInfo(headers);
      const descendants = await this.departmentService.findDescendants(userInfo.departId);
      const idList = [];
      for (const i of descendants) {
        idList.push(i.id);
      }
      const data = await this.projectManageService.queryList(idList, query);
      return ResultData.success(data);  // 正常返回
    } catch (err) {
      return ResultData.fail(AppHttpCode.COMMON_ERR, err?.message); // 异常返回
    }
  }
// 重置密码
@Post('resetPwd')
async resetPwd(@Body() userInfo: resetPasswordDto) {
  try {
    await this.userService.resetPwd(userInfo);
    return ResultData.success();
  } catch ({ message }) {
    return ResultData.fail(AppHttpCode.COMMON_ERR, message);
  }
}

cookie的使用

1.安装

npm install cookie-parser --save

在main.ts中 引入与配置

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { join } from 'path';
import * as cookieParser from 'cookie-parser';

async function bootstrap() {
  // ...
  // 配置cookie中间件,()里面写上东西
  app.use(cookieParser('this signed cookies'));
  await app.listen(3000);
}
bootstrap();

2.设置

在控制器里面可以设置cookie,需要使用Response装饰器 装饰器需要引入

注意: 一旦引入@Response 就不可以直接return data,必须得使用res.json 或者res.send 才能将数据返回请求方

import { Controller, Get, Response } from '@nestjs/common';
@Controller('article')
export class ArticleController {
    @Get()
    index(@Response() res){
        //  配置cookie的键值,过期时间,是否只要后端可读 ,第三个参数为加密
        res.cookie('username', 'aabbcc我是cookie', {
          maxAge: 1000*60*10,
          httpOnly: true,
          signed: true
        });
        // 响应数据
        // 注:res和return不能同时使用,否则卡死
        // return '这是文章页面'; 
        res.send('这是文章页面');
    }
}

3.获取

设置完cookie后,在其他控制器中就可以使用cookie(使用Request装饰器)

import { Body, Controller, Get, Post, Render, Response, Request } from '@nestjs/common';
@Controller('user')
export class UserController {
    // ...
    // 获取cookie
    @Get('cookie')
    getCookie(@Request() req){
        // 1. 获取cookie
        console.log(req.cookies.username);
        return res.cookies.username;
    }
}

配置静态资源访问目录

前端页面和后端的一些文件需要开放给前端访问时,需要配置类似于文件服务功能

import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';
// import { TransformInterceptor } from './core/interceptor/transform.interceptor';
import { HttpExceptionFilter } from './core/filter/http-exception.filter';
async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  // 全局注册拦截器
  // app.useGlobalInterceptors(new TransformInterceptor());
  //过滤处理 HTTP 异常
  app.useGlobalFilters(new HttpExceptionFilter());
  // 全局URL前缀
  app.setGlobalPrefix('/halfMonthApi');
  //配置静态资源目录
  app.useStaticAssets('docs');
  await app.listen(3000);
}
bootstrap();

设置完成后,可以直接访问静态资源,例如:docs文件夹下有个 word文件,此时,就可以直接在浏览器中直接访问=====》 http://localhost:3000/test.docx

转载自:https://juejin.cn/post/7145378254780629023
评论
请登录