NestJS基础概念

NestJS基础概念

Tags
NodeJS
NestJS
Published
Published January 18, 2022
Property
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()