nest

781 阅读8分钟

1, 介绍

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

周任务和开发设计项目背景

作为前端leader,组内和很多系统,每周会举行前端周会,会议分三个模块

  • 周任务回顾
  • 代码评审
  • 知识分享
    公司内部有confluence,svn等系统存放文档,但是觉得其格式不太符合我的需求,需要有汇总分析功能。于是自己写了个系统,主要是给前端团队管理 周任务和开发设计

2,技术栈

前端采用vue3框架,主要分为

  • 首页 - 我的代办任务
  • 系统版本 - 列表 (增删改查)
  • 任务列表 --关联 系统版本 列表 (增删改查)
    任务下采用md记载基本描述,以及开发设计
  • 周任务列表 -- 关联 任务 列表 (增删改查)
  • 用户管理 -- 管理员新增用户,普通用户只能修改自己密码
  • 登陆

后台采用 nest 框架,搭配一台mysql 对数据持久化存储

  • multer 对文件管理,md支持粘贴图片,功能参考 掘金 md编辑器
  • swagger 对api进行管理
  • jwt 对登陆认证
  • nest 守卫对增删改接口 控制
  • minio 文件存储

3,nest概念

image.png

3.1、Exception filters异常过滤器

  Exception filters异常过滤器可以捕获在后端接受处理任何阶段所跑出的异常,捕获到异常后,然后返回处理过的异常结果给客户端(比如返回错误码,错误提示信息等等)。

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

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

  我们可以看到host是实现了ArgumentsHost接口的,在host中可以获取运行环境中的信息,如果在http请求中那么可以获取request和response,如果在socket中也可以获取client和data信息。

  同样的,对于异常过滤器,我们可以指定在某一个模块中使用,或者指定其在全局使用等。

3.2、Pipes管道

  Pipes一般用户验证请求中参数是否符合要求,起到一个校验参数的功能。

  比如我们对于一个请求中的某些参数,需要校验或者转化参数的类型:

@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;
  }
}

  上述的ParseIntPipe就可以把参数转化成十进制的整型数字。我们可以这样使用:

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

  对于get请求中的参数id,调用new ParseIntPipe方法来将id参数转化成十进制的整数。

3.3、middleware中间件

  在nestjs中的middle完全跟express的中间件一摸一样。不仅如此,我们还可以直接使用express中的中间件,比如在我的应用中需要处理core跨域:

import * as cors from 'cors';
async function bootstrap() {
  onst app = await NestFactory.create(/* 创建app的业务逻辑*/)
  app.use(cors({
    origin:'http://localhost:8080',
    credentials:true
  }));
  await app.listen(3000)
}
bootstrap();

在上述的代码中我们可以直接通过app.use来使用core这个express中的中间件。从而使得server端支持core跨域等。

初此之外,跟nestjs的中间件也完全保留了express中的中间件的特点:

  • 在中间件中接受response和request作为参数,并且可以修改请求对象request和结果返回对象response
  • 可以结束对于请求的处理,直接将请求的结果返回,也就是说可以在中间件中直接res.send等。
  • 在该中间件处理完毕后,如果没有将请求结果返回,那么可以通过next方法,将中间件传递给下一个中间件处理。

在nestjs中,中间件跟express中完全一样,除了可以复用express中间件外,在nestjs中针对某一个特定的路由来使用中间件也十分的方便:

class ApplicationModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('cats');
  }
}

上面就是对于特定的路由url为/cats的时候,使用LoggerMiddleware中间件。

3.4 、Guards守卫

  Guards守卫,其作用就是决定一个请求是否应该被处理函数接受并处理,当然我们也可以在middleware中间件中来做请求的接受与否的处理,与middleware相比,Guards可以获得更加详细的关于请求的执行上下文信息。

通常Guards守卫层,位于middleware之后,请求正式被处理函数处理之前。

下面是一个Guards的例子:

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

这里的context实现了一个ExecutionContext接口,该接口中具有丰富的执行上下文信息。

export interface ArgumentsHost {
  getArgs<T extends Array<any> = any[]>(): T;
  getArgByIndex<T = any>(index: number): T;
  switchToRpc(): RpcArgumentsHost;
  switchToHttp(): HttpArgumentsHost;
  switchToWs(): WsArgumentsHost;
}

export interface ExecutionContext extends ArgumentsHost {
  getClass<T = any>(): Type<T>;
  getHandler(): Function;
}

除了ArgumentsHost中的信息外,ExecutionContext还包含了getClass用户获取对于某一个路由处理的,控制器。而getClass用于获取返回对于指定路由后台处理时的处理函数。

对于Guards处理函数,如果返回true,那么请求会被正常的处理,如果返回false那么请求会抛出异常。

3.5、interceptors拦截器

   拦截器可以给每一个需要执行的函数绑定,拦截器将在该函数执行前或者执行后运行。可以转换函数执行后返回的结果等。

概括来说:

interceptors拦截器在函数执行前或者执行后可以运行,如果在执行后运行,可以拦截函数执行的返回结果,修改参数等。

再来举一个超时处理的例子:

@Injectable()
export class TimeoutInterceptor implements NestInterceptor{
  intercept(
    context:ExecutionContext,
    call$:Observable<any>
  ):Observable<any>{
    return call$.pipe(timeout(5000));
  }
}

该拦截器可以定义在控制器上,可以处理超时请求。

4,原理部分

4.1 DI依赖注入与IoC控制反转

使用后端开发的都不陌生。

  • DI IoC容器动态的将某个依赖关系注入到组件之中
  • 控制反转IoC(Inversion of Control)是说创建对象的控制权进行转移,以前创建对象的主动权和创建时机是由自己把控的,而现在这种权力转移到第三方,比如转移交给了IoC容器,它就是一个专门用来创建对象的工厂

nest Injectable demo

type Constructor<T = any> = new (...args: any[]) => T;

const Injectable = (): ClassDecorator => target => {};

class OtherService {
  a = 1;
}

@Injectable()
class TestService {
  constructor(public readonly otherService: OtherService) {}

  testMethod() {
    console.log(this.otherService.a);
  }
}

const Factory = <T>(target: Constructor<T>): T => {
  // 获取所有注入的服务
  const providers = Reflect.getMetadata('design:paramtypes', target); // [OtherService]
  const args = providers.map((provider: Constructor) => new provider());
  return new target(...args);
};

Factory(TestService).testMethod(); 

在nestjs中也参考了angular中的依赖注入的思想,也是用module、controller和service。

@Module({
  imports:[otherModule],
  providers:[SaveService],
  controllers:[SaveController,SaveExtroController]
})
export class SaveModule {}

  上面就是nestjs中如何定一个module,在imports属性中可以注入其他模块,在providers 注入相应的在控制器中需要用到的service,在控制器中注入需要的controller。

4.2,元数据、反射与装饰器

基本概念

  • MetaData:也称元数据,元数据是用来描述数据的数据。可以借助仓库reflect-metadata
  • Reflect:es6规范中,Reflect已存在,简单来说,这个API的作用就是可以实现对变量操作的函数化,也就是反射,具体可看阮一峰es6关于reflect的教程
  • Decorator:装饰器,主要用来扩展类和类的方法,使其功能更强大。具体可看阮一峰es6关于decorator的教程

装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,装饰器能在编译阶段运行代码。也就是说,装饰器本质就是编译时执行的函数

nest controller和getter的实现

const METHOD_METADATA = 'method';
const PATH_METADATA = 'path';

const Controller = (path: string): ClassDecorator => {
    return target => {
        Reflect.defineMetadata(PATH_METADATA, path, target);
    }
}
const createMappingDecorator = (method: string) => (path: string): MethodDecorator => {
      return (target, key, descriptor) => {
        Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
        Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value);
      }
}


const Get = createMappingDecorator('GET');
const Post = createMappingDecorator('POST');


@Controller('/test')
class SomeClass {
      @Get('/a')
      someGetMethod() {
          return 'hello world';
      }

      @Post('/b')
      somePostMethod() {
          return 'zhangjing';
      }

}
function isFunction(arg: any): boolean {
    return typeof arg === 'function';
}

function isConstructor(arg: string) {
    return arg === 'constructor';
}


function mapRoute(instance) {
      const prototype = Object.getPrototypeOf(instance);

      // 筛选出类的 methodName
      const methodsNames = Object.getOwnPropertyNames(prototype)
                                  .filter(item => !isConstructor(item) && isFunction(prototype[item]))
      return methodsNames.map(methodName => {
        const fn = prototype[methodName];

        // 取出定义的 metadata
        const route = Reflect.getMetadata(PATH_METADATA, fn);
        const method = Reflect.getMetadata(METHOD_METADATA, fn);
        return {
          route,
          method,
          fn,
          methodName
        }
      })
 }
    


console.log(Reflect.getMetadata(PATH_METADATA, SomeClass)); // '/test'
console.log(mapRoute(new SomeClass()));

5,部署

均采用的docker部署,包括:

  • mysql
  • node :后端nest
  • monio
  • nginx :前端代码vue3

部署配置见另一文章 docker

6,常见的数据传输方式:

  • url param
import { Controller, Get, Param } from '@nestjs/common';

@Controller('params-parse')
export class ParamsParseController {
  @Get(':id')
  urlParma(@Param('id') id: string) {
    return `传递的id:${id}`;
  }
}
  • query
import { Controller, Get, Param, Query } from '@nestjs/common';

@Controller('params-parse')  
export class ParamsParseController {  
    @Get('query')  
    query(@Query('code') code: string) {  
        return `传递的code:${code}`;  
    }
}
  • form-data
import {Controller,Post,Body,UploadedFiles,UseInterceptors} from '@nestjs/common';
import { CreateParamsParseDto } from './dto/create-params-parse.dto';
import { AnyFilesInterceptor } from '@nestjs/platform-express';
import { Express } from 'express';

@Controller('params-parse')
export class ParamsParseController {

  @Post('file')
  @UseInterceptors(AnyFilesInterceptor({ dest: 'uploads/' }))
  file(
    @Body() createParamsParseDto: CreateParamsParseDto,
    @UploadedFiles() files: Array<Express.Multer.File>,
  ) {
    return `传递的对象:${JSON.stringify(createParamsParseDto)}`;
  }
}
  • form-urlencoded
export class CreateParamsParseDto {
  code: string;
}

import { Controller, Post, Body } from '@nestjs/common';
import { CreateParamsParseDto } from './dto/create-params-parse.dto';

@Controller('params-parse')
export class ParamsParseController {
  @Post('urlencoded')
  body(@Body() createParamsParseDto: CreateParamsParseDto) {
    return `传递的对象:${JSON.stringify(createParamsParseDto)}`;
  }
}
  • json
import { Controller, Post, Body } from '@nestjs/common';
import { CreateParamsParseDto } from './dto/create-params-parse.dto';

@Controller('params-parse')
export class ParamsParseController {
  @Post('json')
  json(@Body() createParamsParseDto: CreateParamsParseDto) {
    return `传递的对象:${JSON.stringify(createParamsParseDto)}`;
  }
}

-- 推荐文章

es7之Reflect Metadata

Nestjs的设计思想和使用方法

nest入门

欢迎关注我的前端自检清单,我和你一起成长