NestJS工作基础概念

NestJS工作基础概念

Tags
Node.js
JavaScript
NestJS
Published
Jan 18, 2022
NestJS 是一个完全支持 TypeScriptNode.js服务端开发框架,其基本工作原理是在常见的框(Express/Fastify)上重新抽象了一个级别,通过装饰器串联起各工作模块。
整个工作流程主要由以下等模块组成:
  • Controller(控制器)
  • Provider(提供者)
  • Module(模块)
  • Middleware(中间件)
  • Filter(过滤器)
  • Pipe(管道)
  • Guard(守卫)
  • Decorator(装饰器)
模块间的关系(生命周期)如图所示:
notion image
下面以一个demo对各个模块进行简单的介绍,项目结构如下:
├── src
|  ├── app.module.ts
|  ├── main.ts
|  ├── filter
|  |  └── http.filter.ts
|  ├── guard
|  |  └── auth.guard.ts
|  ├── interceptor
|  |  └── app.interceptor.ts
|  ├── middleware
|  |  └── proxy.middleware.ts
|  ├── modules
|  |  ├── demo
|  |  |  ├── demo.controller.ts
|  |  |  ├── demo.dto.ts
|  |  |  ├── demo.module.ts
|  |  |  └── demo.service.ts
|  ├── pipe
|  |  └── validation.pipe.ts

Controller

负责处理 http 接口请求和响应,可以理解为对应各路由 API 路径处理。
/* demo.controller.ts */
import { Controller, Get, Query, Post, Body } from '@nestjs/common';

@Controller('/demo')
export class DemoController {
  @Get('/say')
  async sayGet(@Query() query) {
    return 'hello world';
  }

	@Post('/say')
  async sayPost(@Body() body) {
    return 'hello world';
  }
}
其中对应的 @Query@Body分别获取 Get 和 Post 请求的参数。

Provider

一个用 @Injectable() 装饰器包装的类,可以通过 constructor 注入依赖关系,其实就是传统的Service服务,用来处理数据相关。
/* demo.service.ts */
import { Injectable } from '@nestjs/common'

@Injectable()
export class DemoService {
  constructor() {}

  async getName(): Promise<string> {
    return 'onfuns'
  }
}
然后在 demo.controller.ts 注入service类。
import { Inject, Controller, Get, Query, Post, Body } from '@nestjs/common'
import { DemoService } from './demo.service'
@Controller('/demo')
export class DemoController {
  constructor(@Inject(DemoService) private readonly service: DemoService) {}

  @Get('/say')
  async sayGet(@Query() query) {
    console.log(query)
    const name = await this.service.getName()
    return `hello sayGet ${name}`
  }

  @Post('/say')
  async sayPost(@Body() body) {
    console.log(body)
    return 'hello sayPost'
  }
}
此时 provider 已经准备就绪,只要在module中完成关联就可以使用了。

Module

聚合控制器和提供者的类,由 @Module包裹,一个项目有多个模块,每个模块都有紧密相关的功能。当其他模块需要引入时,可以通过 exports 暴露出Service供其他功能使用。
/* demo.module.ts */
import { Module } from '@nestjs/common'
import { DemoController } from './demo.controller'
import { DemoService } from './demo.service'
@Module({
  imports: [],
  controllers: [DemoController],
  providers: [DemoService],
	exports:[]
})
export class DemoModule {}

Middleware

中间件是在路由处理前调用,等用于 express 中间件功能,下面使用中间件对一些特定的路由代理。
/* proxy.middleware.ts */
import { createProxyMiddleware } from 'http-proxy-middleware'
import { NestMiddleware } from '@nestjs/common'
import { Request, Response } from 'express'
export class ProxyMiddleware implements NestMiddleware {
  private proxy = createProxyMiddleware({
    target: 'https://api.com',
    changeOrigin: true,
    pathRewrite: {
      '/api': '/api/v2',
    },
    secure: false,
    onProxyReq: (proxyReq, req: Request, res: Response) => {
      console.log(
        `[NestMiddleware]: Proxying ${req.url} request originally made to '${req.originalUrl}'...`,
      )
    },
    onProxyRes: (proxyReq, req: Request, res: Response) => {},
  })
  use(req: Request, res: Response, next: () => void) {
    this.proxy(req, res, next)
  }
}
在根 module 中注册使用
/* app.module.ts */
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common'
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(ProxyMiddleware).forRoutes({ path: 'api/*', method: RequestMethod.ALL })
  }
}

Filter

内置的异常捕获器,负责处理整个应用程序中的所有抛出的异常。当捕获到未处理的异常时,可以转换为友好的方式让客户端使用,以下为返回一个统一结构的错误响应数据。
/* http.filter.ts */
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common'
import { Request, Response } from 'express'
@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 instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR
    const success = status >= 200 && status < 300
    if (!success) console.log(exception)
    const errorResponse = {
      success,
      data: null,
      message: exception.message,
    }
    response.status(status)
    response.header('Content-Type', 'application/json; charset=utf-8')
    response.send(errorResponse)
  }
}

Pipe

管道对传入数据转换或校验,一般结合Dto使用,可以校验用户传入数据的准确性:
  • 转换:管道将输入数据转换为所需的数据输出;
  • 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常。
/* validation.pipe.ts */
import { ArgumentMetadata, PipeTransform, HttpException, HttpStatus } from '@nestjs/common'
import { validate } from 'class-validator'
import { plainToClass } from 'class-transformer'

export class ValidationPipe implements PipeTransform {
  async transform(value: any, { metatype }: ArgumentMetadata) {
    // 如果没有传入验证规则,则不验证,直接返回数据
    if (!metatype || !this.toValidate(metatype)) {
      return value
    }
    const object = plainToClass(metatype, value)
    const errors = await validate(object)
    if (errors.length > 0) {
      const msg = Object.values(errors[0].constraints)[0]
      throw new HttpException(`failed: ${msg}`, HttpStatus.BAD_REQUEST)
    }
    return value
  }
  private toValidate(metatype: any): boolean {
    const types: any[] = [String, Boolean, Number, Array, Object]
    return !types.includes(metatype)
  }
}
创建需要的dto文件,通过 class-validator辅助验证数据的有效性。
/* demo.dto.ts */
import { IsNotEmpty } from 'class-validator'

export class CreateDto {
  @IsNotEmpty({ message: '名称不能为空' })
  name: string

  @IsNotEmpty({ message: '分类不能为空' })
  category_id: string
}

export class UpdateDto {
  @IsNotEmpty({ message: 'id 不能为空' })
  id: number
}
在controller中将dto 与对应的入参绑定,当入参不符合dto中的规则时会抛出异常。
/* demo.controller.ts */
import { Inject, Controller, Get, Query, Post, Body } from '@nestjs/common'
import { DemoService } from './demo.service'
import { CreateDto, UpdateDto } from './demo.dto'

@Controller('/demo')
export class DemoController {
  constructor(@Inject(DemoService) private readonly service: DemoService) {}

  @Get('/say')
  async sayGet(@Query() query: UpdateDto) {
    console.log(query)
    const name = await this.service.getName()
    return `hello sayGet ${name}`
  }

  @Post('/say')
  async sayPost(@Body() body: CreateDto) {
    console.log(body)
    return 'hello sayPost'
  }
}

Guard

守卫根据权限,角色,访问控制列表等来给某些路由加上特定权限,一下为一个全局守卫,会判断roles 元数据,当 roles 为 all 时则不鉴权直接通过,否则拦截抛出 TOKEN_INVALID错误。
/* auth.guard.ts */
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  HttpException,
  HttpStatus,
} from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { Request } from 'express'
import { CommonService } from '@/modules/common/common.service'
@Injectable()
export class UserGuard implements CanActivate {
  private readonly reflector: Reflector = new Reflector()
  private readonly commonService = new CommonService()

  async canActivate(context: ExecutionContext): Promise<any> {
    const request = context.switchToHttp().getRequest<Request>()
    const roles = this.reflector.get<string[]>('roles', context.getHandler())
    //有 all标志的说明接口不做鉴权
    if (roles && roles.includes('all')) return true
    const token = request.headers['x-auth-id-token']
    console.log('guard x-auth-id-token received:', token)
    if (!token) return this.fail()
    const data = await this.commonService.verifyIdToken(token)
    if (data === false) this.fail()
    return !!data
  }
  fail() {
    throw new HttpException('TOKEN_INVALID', HttpStatus.FORBIDDEN)
  }
}
在controller中 给对应路由加上不鉴权标志
/* demo.controller.ts */
import { Inject, Controller, Get, Query, Post, Body, SetMetadata } from '@nestjs/common'
import { DemoService } from './demo.service'
import { CreateDto, UpdateDto } from './demo.dto'

@Controller('/demo')
export class DemoController {
  constructor(@Inject(DemoService) private readonly service: DemoService) {}

  @Get('/say')
  @SetMetadata('roles', ['all'])
  async sayGet(@Query() query: UpdateDto) {
    console.log(query)
    const name = await this.service.getName()
    return `hello sayGet ${name}`
  }

  @Post('/say')
  async sayPost(@Body() body: CreateDto) {
    console.log(body)
    return 'hello sayPost'
  }
}
这样接口 /demo/say get请求时是不鉴权的,其余接口未加roles则统一走鉴权模式。

Interceptor

拦截器主要为在函数执行之前/之后拦截额外处理、转换从函数返回的结果、转换从函数抛出的异常等功能,一般用来处理响应数据,下面案例为返回统一格式的响应数据。
/* app.interceptor.ts  */
import {
  Injectable,
  ExecutionContext,
  NestInterceptor,
  CallHandler,
  HttpStatus,
} from '@nestjs/common'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
@Injectable()
export class AppInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const ctx = context.switchToHttp()
    const res = ctx.getResponse()
    const req = ctx.getRequest()
    // 统一调整返回状态码为200
    if (['POST', 'PUT', 'DELETE', 'OPTIONS'].includes(req.method)) {
      res.status(HttpStatus.OK)
    }
    const resMapData = map((data: any) => {
      let result = { success: true, message: '请求成功', data: null }
      if (data && typeof data === 'object' && 'success' in data) {
        result = { ...result, ...data }
      } else {
        result.data = data
      }
      return result
    })
    return next.handle().pipe(resMapData)
  }
}
// { success:true,message:'请求成功',data:[] }

入口

对应的根 module 和启动文件,以上模块案例中都是在 main.ts 中使用全局注册的方法,也可以局部注册对应某个模块生效,具体用法可参考官方文档。
/* app.module.ts  */
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common'
import { ProxyMiddleware } from '@/middleware/proxy.middleware'
import { DemoModule } from '@/modules/demo/demo.module'
@Module({
  imports: [
    DemoModule,
  ],
  controllers: [],
  providers: [],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(ProxyMiddleware).forRoutes({ path: 'java/*', method: RequestMethod.ALL })
  }
}
/* main.ts  */
import { NestFactory } from '@nestjs/core'
import { NestExpressApplication } from '@nestjs/platform-express'
import { AppModule } from './app.module'
import { AppInterceptor } from '@/interceptor/app.Interceptor'
import { HttpExceptionFilter } from '@/filter/http.filter'
import { UserGuard } from '@/guard/auth.guard'
import { ValidationPipe } from '@/pipe/validation.pipe'
import * as cookieParse from 'cookie-parser'

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule, {
    bodyParser: true,
  })
  //app.setGlobalPrefix('', { exclude: [] })
  app.useGlobalFilters(new HttpExceptionFilter())
  app.useGlobalInterceptors(new AppInterceptor())
  app.useGlobalGuards(new UserGuard())
  app.useGlobalPipes(new ValidationPipe())
  app.enableCors()
  app.use(cookieParse())
  const port = process.env.PORT || 4001
  await app.listen(port)
  console.log(`listen on http://localhost:${port}`)
}
bootstrap()