NestJS 定位是 NodeJS 的后端企业级框架,由于其完美支持 TypeScript 加上优秀的架构设计备受好评,它在常见的 HTTP 基础库上(Express/Fastify)上重新抽象了一个级别,同时提供了基础库没有的 MVC、依赖注入、AOP(面向切面设计)等特性,使代码模块结构统一,更利于扩展与维护。
设计思想
MVC(Model–view–controller)
将项目结构分为传统的模型(Model)、视图(View)和控制器(Controller)。Model层负责管理数据库相关实体及服务层的数据读写实现,控制层对请求解析及调用服务层数据组装,而视图层负责对控制层返回的数据展示:
NestJS 中封装了 Service、Controller 等装饰器,被这些装饰器声明的类会自动归类为服务层、控制层等,然后通过 Injectable 装饰成 Provider 被其他类引入使用。
//article.service.ts import { Injectable } from '@nestjs/common'; @Injectable() export class ArticleService { find(): string { return "hello world"; } }
在控制层中使用
//article.controllter.ts import { Inject, Controller, Get, Query } from '@nestjs/common' import { ArticleService } from './article.service' @Controller('/article') export class ArticleController { constructor(@Inject(ArticleService) private readonly service: ArticleService) {} @Get('/find') async find(@Query() query) { return await this.service.find() } }
MVC模式主要实现 Controller 和 Service 解耦,Controller 负责路由请求、入参处理及返回数据的处理等,而 Service 负责数据存储读写等处理,两者之间不用相互关心内部如何实现,职责清晰明了。
依赖注入
依赖注入是IOC(控制反转)中的一种设计思想,上文中提到的 @Controller、@Injectable 装饰器声明的类会被 NestJS 自动扫描,然后通过容器 Provider 注入到全文作用域中,这样应用启动时所有的对象会根据构造器里声明的依赖自动注入。
constructor(@Inject(ArticleService) private readonly service: ArticleService) {}
AOP(面向切面编程)
官方的定义是:旨在将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度,减少侵入性。说的有些一头雾水,其实就是将本该应用于每个函数上的通用方法抽离处理,然后入口处统一处理。
打个比方,比如需要将后台接口全部鉴权,非登录则调用失败,常规的做法是在这些接口控制层里面一个个调用鉴权代码,而如果使用AOP思想,只需在路由入口处统一拦截处理,将和主业务不想干的代码放在最外层模块,随时启用或关闭达到无入侵性。
NestJS 主要有 Middleware(中间件)、Guard(守卫)、Pipe(管道)、Inteceptor(拦截器)、Filter(过滤器)等用到 AOP 思想,上面所举例的鉴权就可以通过 Guard 来实现。
NestJS 工作流程图:
功能模块
下面主要对常用的模块做个简单介绍,通过这些模块的认识,可以学习 NestJS 中 AOP 思想的精妙之处。
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) => {}, onProxyRes: (proxyReq, req: Request, res: Response) => {}, }) use(req: Request, res: Response, next: () => void) { this.proxy(req, res, next) } }
支持全局或者路由级别设置生效,适用于日志捕获、cookie设置等
//mian.ts 全局使用 const app = await NestFactory.create(AppModule) app.use(ProxyMiddleware) //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 }) } }
Guard(守卫)
可以使用守卫给某些路由加上特定权限,返回 true 为通过,false 则是失败然后中断后续生命周期的执行,这就凸显出 AOP 设计思想的优势了,不会入侵 Controller ,在全局注册使用,随时可以开启不影响主流程改动。
/* auth.guard.ts */ import { Injectable, CanActivate, ExecutionContext, HttpException, HttpStatus, } from '@nestjs/common' import { Reflector } from '@nestjs/core' import { Request } from 'express' import { UserService } from '@/modules/user/user.service' @Injectable() export class UserGuard implements CanActivate { private readonly reflector: Reflector = new Reflector() private readonly userService = new UserService() 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'] if (!token || this.userService.invalid(token)) return false } }
这里使用了自定义装饰器,可以在 Controller 中给一些特点路由加上免鉴权鉴权标志:
@Controller('/article') export class ArticleController { constructor(@Inject(ArticleService) private readonly service: ArticleService) {} @Get('/find') @SetMetadata('roles', ['all']) async find(@Query() query) { return await this.service.find() } @Post('/create') async create(@Body() body) { return 'create' } }
这样接口
/article/find
免鉴权,其余接口未加 roles 属性则统一走鉴权模式。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
辅助验证数据的有效性。/* article.dto.ts */ import { IsNotEmpty } from 'class-validator' export class CreateDto { @IsNotEmpty({ message: '名称不能为空' }) name: string @IsNotEmpty({ message: '分类不能为空' }) category_id: string }
在 Controller 中将 Dto 与对应的入参绑定,当入参不符合 Dto 中的规则时会抛出异常。
/* article.controller.ts */ import { Inject, Controller, Get, Query, Post, Body } from '@nestjs/common' import { ArticleService } from './article.service' import { CreateDto } from './article.dto' @Controller('/article') export class ArticleController { constructor(@Inject(ArticleService) private readonly service: ArticleService) {} @Get('/find') @SetMetadata('roles', ['all']) async find(@Query() query) { return await this.service.find() } @Post('/create') async create(@Body() body:CreateDto) { return 'create' } }
Interceptor(拦截器)
拦截器主要是在控制层中路由处理的过程中加入一些额外操作,比如统一封装结果返回格式等等。
Interceptor 使用 RxJS 的 Observable 流来管理异步操作,当调用 next.handle() 后才会触发控制层中对应的目标函数。
下面案例为返回统一格式的响应数据:
/* 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:[] }
Filter(过滤器)
在整个应用链路调用的过程都可能主动或被动的抛出一些异常,NestJS 提供了异常捕获器,负责捕获整个应用程序中的所有抛出的异常。
NestJS 中不仅内置了多种捕获器 Exception filters,同时还支持自定义对捕获器的扩展。
比如当捕获到未处理的异常时,可以转换为友好的方式让客户端使用,以下为返回一个统一结构的错误响应数据:
/* 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) } }
总结
NestJS 是一款优秀的企业级框架,很好的应用了 MVC、IOC、AOP 等架构思想,通过各模块的功能划分,在大型应用架构设计上毫不逊色。
如果你有在使用 NodeJS 开发后端,在模型、控制器、服务层等之间关系上模棱两可,或者想找一个功能齐全、生态丰富的 NodeJS 框架构建大型应用,那么建议你了解一下 NestJS,其中的思想和架构可以很好的锻炼前端开发的思维模式,加深对前后端整个技术知识体系的认知。