了解了Nest CLI的使用之后,本节我们来小试牛刀,创建一个服务端应用,并使用React构建一个客户端应用。我们将实现客户端发送请求后,服务端接收请求、进行数据处理,并返回新的数据给客户端。如果你喜欢使用Vue,也可以选择通过Vue来创建项目;但请注意,本案例不涉及复杂的前端交互。
首先,运行nest n web-app -p pnpm命令生成服务端项目,如图2-23所示。
图2-23 创建服务端项目
然后,启动该项目并保持热重载,如图2-24所示。
图2-24 运行服务端项目
使用React提供的脚手架create-react-app生成项目,先全局安装一下:
npm install -g create-react-app
查看是否安装成功:
create-react-app --version
结果如图2-25所示。
图2-25 检查是否安装成功
执行create-react-app web-app-front命令,创建名为web-app-front的项目。创建成功后的效果如图2-26所示。
图2-26 创建客户端项目
同样,启动该项目,执行npm start命令,结果如图2-27所示。
图2-27 扏行客户端项目
在发送请求之前,我们需要进行一些准备工作。首先,在前端设置请求代理,以便能够访问8088端口下的服务。接着,通过axios发送一个GET请求,成功后将数据显示在页面上。
为了设置请求代理,在目录web-app-front/src下新建setupProxy.js文件,如图2-28所示。
图2-28 新建setupProxy文件
设置代理逻辑,这里将端口自定义为8088,代码如下:
执行npm install axios命令来安装axios库。然后在App.js文件中引入axios,并定义一个名为getHello的API接口,用于请求/123路径的资源。以下是示例代码:
import axios from 'axios'; function getHello() { return axios.get('/api/123'); }
通过单击按钮触发AJAX请求,并回显接收到的数据。以下是部分示例代码:
切换到web-app服务端,在main.ts文件中将端口修改为8088,以匹配前端代理端口,代码如下:
async function bootstrap() { const app = await NestFactory.create(AppModule); // 修改端口为8088 await app.listen(8088); } bootstrap();
接下来,在app.controller.ts文件的getHello方法中接收参数id,同时调用Service服务的getHello()方法并传递参数id,代码如下:
最终getHello()方法会返回新的JSON数据:
前后端交互逻辑完成之后,在客户端单击按钮发送请求,可以从网络面板看到正常返回的数据并在页面上显示,如图2-29所示。
图2-29 前后端交互结果图
至此,一个简易的前后端交互流程就完成了。
本小节将把请求方式改为POST,并优化项目结构。通过模块化来管理请求API。以user模块为例,执行“nest g resource user”命令来生成user模块。接下来,在user.controller.ts中修改请求路径,示例代码如下:
同时,在user.service.ts中打印并返回提示信息:
接下来,同步修改web-app-front前端项目的App.js,新增createUser请求接口和handleCreateUser用户事件,代码如下:
单击页面上的“创建用户”按钮,后端会正常接收到请求参数,并向客户端发送响应,如图2-30所示。
图2-30 创建用户返回结果
值得注意的是,在请求接口中,/user/*后面的路由对应于User控制器下的各类接口方法。这样的设计具有以下优点:它提供了清晰的路由层次结构,并且可以方便地实现对控制器层面的权限管理和拦截器操作。这些内容将在后续章节中详细讲解。
通过本节的学习,一个完整的前后端分离的Nest应用已经构建完成。现在,你已经具备了创建自己的Nest应用的能力。赶快去实践,将所学知识应用到实际项目中吧!
Nest采用了多种优秀的设计模式和软件设计思想,其中之一就是面向切面编程(AOP)。AOP的引入解决了传统面向对象编程中的一些问题,比如代码重复和业务逻辑的混杂。作为Nest最核心的设计理念之一,本节将深入学习AOP的工作机制及其应用。
Nest属于MVC(Model-View-Controller,模型-视图-控制器)架构体系,实际上,大多数后端框架都是基于这一架构设计的。在MVC架构中:
● Model层负责业务逻辑处理,包括数据的获取、存储、验证以及数据库操作。
● Controller层通常用于处理用户的输入,调度Service服务,以及进行API的路由管理。
● View层在传统的服务器端渲染中,可能使用如ejs、hbs等模板引擎。在前后端分离的体系中,通常指的是客户端框架(如Vue或React)负责的部分。
当一个HTTP请求到达服务器时,它首先会被Controller层接收。Controller层会根据请求调用Model层中的相应模块来处理业务逻辑,并将处理结果返回给View层以进行展示。整个基础流程的示意图如图2-31所示。
图2-31 HTTP请求基本流程
在MVC架构的基础上,Nest还引入了AOP的思想,从而具备了面向切面编程的能力。我们经常听到后端开发者提到AOP切面,那么究竟什么是面向切面编程呢?接下来将给出答案。
以一个HTTP请求为例,客户端发送请求时首先会经过Controller(控制器)、Service(服务)、DB(数据访问或操作)等模块。如果想要在这些模块中加入一些操作,例如数据验证、权限校验或日志统计,应该怎么办呢?
首先,我们可能会想到在Controller中加入参数校验逻辑或者权限校验,如果不通过验证,就直接返回错误。这样看起来似乎没什么问题。但是,如果有多个功能模块都需要进行校验,并且权限校验的逻辑相同,那么是不是意味着需要在多个Controller中重复加入这段逻辑?显然,这样做会导致公共逻辑与业务逻辑耦合。有没有办法可以做到统一管理呢?
答案是有的,办法如图2-32所示。
图2-32 切面工作原理图
由图2-32可见,在Controller的前后都可以“切一刀”,用来统一处理公共逻辑,这样就不会侵入Controller、Service等业务代码。
事实上,在Nest中,请求流程可以换一种角度来看,如图2-33所示。
图2-33 Nest请求流程
中间的灰色区域属于AOP切面部分,包含Middleware(中间件)、Guard(守卫)、Interceptor(拦截器)、Pipe(管道)和Filter(过滤器)。它们都是AOP思想的具体实现。
前面我们了解了AOP思想的优势,接下来看看AOP在Nest中的应用。这里将以store-web项目作为演示,代码已经放到GitHub对应章节的目录下。
Nest的中间件默认是基于Express的,它在请求流程中的位置如图2-34所示。
图2-34 中间件的位置
中间件可以在路由处理程序之前或之后插入执行任务,它们分为全局中间件和局部中间件两种类型。
全局中间件通过use方法调用,与Express中的使用方式类似。所有进入应用的请求都会经过全局中间件,通常用于执行日志统计、监控、安全性处理等任务。例如,在main.ts文件中使用全局中间件的方式如下,其中LoggerMiddleware是一个用于日志统计的中间件。示例代码如下:
async function bootstrap() { const app = await NestFactory.create(AppModule); // 中间件 app.use(new LoggerMiddleware().use) // 启动服务 await app.listen(8088); } bootstrap();
局部中间件通常应用于特定的控制器或单个路由上,以实现更细粒度的逻辑控制。例如,可以将LoggerMiddleware绑定到/person/create-person路由,并指定其适用的HTTP请求方法。以下是如何在控制器中应用局部中间件的示例代码:
对于中间件的更深入的知识,我们将在后续章节中进行详细介绍。
守卫的职责很明确,通常用于权限、角色等授权操作。守卫所在的位置与中间件类似,可以对请求进行拦截和过滤,如图2-35所示。
图2-35 守卫的位置
守卫在调用路由程序之前返回true或者false来判断是否通行,分为全局守卫和局部守卫。
守卫必须实现CanActivate接口中的canActivate()方法,代码如下:
全局守卫在main.ts中通过useGlobalGuards来调用,每个路由程序都会经过它进行权限验证才能够通行。
async function bootstrap() { const app = await NestFactory.create(AppModule); // 守卫 app.useGlobalGuards(new PersonGuard()) // 启动服务 await app.listen(8088); }
同样,与中间件类似,作为局部守卫,可以缩小控制范围,从而实现更加精细的权限控制。控制器中的示例代码如下:
@Controller('person') // 声明守卫 @UseGuards(new PersonGuard()) // 控制器 export class PersonController {}
拦截器不同于中间件和守卫,它在路由请求之前和之后都可以进行逻辑处理,能够充分操作request和response对象,如图2-36所示。拦截器通常用于记录请求日志、转换或格式化响应数据等。
图2-36 拦截器的位置
为了更好地说明拦截器的作用,下面的代码定义了一个用于统计接口超时的拦截器:
在上述代码中,拦截器必须实现NestInterceptor接口中的intercept方法。该方法包含与守卫相同的ExecutionContext上下文对象作为第一个参数。第二个参数是CallHandler,它代表每个路由处理程序的方法。只有当拦截器调用了handle方法后,控制权才会交给路由处理程序。在Nest中,拦截器通常与RxJS异步处理库一起使用,以执行一些异步逻辑,例如上例中的超时(timeout)统计。
类似于守卫,拦截器可以设置为控制器作用域、方法作用域或全局作用域。
(1)控制器作用域允许拦截器只作用于某个控制器。当程序执行到控制器时触发拦截器逻辑,通过@UseInterceptors装饰器将TimeoutInterceptor绑定到控制器类,代码如下:
@Controller('person') // 为控制器绑定超时拦截器 @UseInterceptors(new TimeoutInterceptor()) export class PersonController {}
(2)方法作用域把拦截器的作用范围限制在某个方法上,比全局或控制器级别的范围更加精确。当程序执行到该方法时,触发拦截器的逻辑。绑定方式如下:
@Get() // 为单独的方法绑定超时拦截器 @UseInterceptors(new TimeoutInterceptor()) findAll() { return this.personService.findAll(); }
(3)全局作用域允许拦截器应用到整个应用中。在main.ts文件中,可以通过app.useGlobalInterceptors()方法进行绑定,代码如下:
async function bootstrap() { const app = await NestFactory.create(AppModule); // 全局超时拦截器 app.useGlobalInterceptors(new TimeoutInterceptor()); // 启动服务 await app.listen(8088); } bootstrap();
管道用于处理通用逻辑,其中两个典型的用例是处理请求参数的验证(validation)和转换(transformation)。在执行路由方法之前,会首先执行管道逻辑,并将经过管道转换后的参数传递给路由方法。它的位置如图2-37所示。
图2-37 管道的位置
尽管Nest框架中已经内置了一些管道(Pipes),例如ParseIntPipe,但有时我们可能需要实现特定的转换功能,比如将数字转换为八进制。在这种情况下,我们需要创建自定义管道。以下是一个自定义管道的示例代码:
自定义管道需要实现PipeTransform接口的transform()方法。其中,value表示需要处理的方法参数,而metadata则是描述该参数的元数据,用于标识其属于哪种类型,如'body'、'query'、'param'和'custom'。
除了自定义管道和内置管道外,通常我们还会结合第三方验证库,例如class-validator。在后面的章节中将会详细介绍这部分内容。
如果管道验证器验证失败,则需要抛出异常并回应给客户端。这时就需要使用异常过滤器来处理。
Nest中最为常见的是HTTP异常过滤器,通常用于在后端服务发生异常时向客户端报告异常的类型。目前内置的HTTP异常包含:
● BadRequestException
● UnauthorizedException
● NotFoundException
● ForbiddenException
● NotAcceptableException
● RequestTimeoutException
● ConflictException
● GoneException
● HttpVersionNotSupportedException
● PayloadTooLargeException
● UnsupportedMediaTypeException
● ...
它们都继承自HttpException类。当然,我们也可以自定义异常过滤器,并向前端返回统一的数据格式:
自定义异常过滤器需要实现ExceptionFilter接口的catch()方法来拦截异常。ArgumentsHost能够获取不同平台的传输协议上下文,用于访问request和response对象。
另外,@Catch()装饰器用于声明要拦截的异常类型,这里使用的是HttpException。异常过滤器可应用于控制器作用域、方法作用域和全局作用域,我们都会尝试一遍。
(1)将过滤器绑定到控制器,代码如下:
@Controller('person') // 绑定到控制器 @UseFilters(new HttpExceptionFilter()) export class PersonController {}
(2)将过滤器绑定到某个路由方法中,代码如下:
@Get() // 绑定到路由方法 @UseFilters(new HttpExceptionFilter()) findAll() { return this.personService.findAll(); }
(3)将过滤器绑定到全局中,代码如下:
async function bootstrap() { const app = await NestFactory.create(AppModule); // 全局异常过滤器 app.useGlobalFilters(new HttpExceptionFilter()); // 启动服务 await app.listen(8088); } bootstrap();
以上就是Nest实现AOP架构思想的几种方式。不同的切面解决不同场景下的通用逻辑抽离问题,从而实现了更加灵活和可维护的应用程序。