普通视图

发现新文章,点击刷新页面。
昨天 — 2025年1月17日首页

NestJS中的Aop面向切面编程。不侵入业务逻辑,高复用通用逻辑。

作者 jinzunqinjiu
2025年1月17日 22:28

哈喽哈喽,我是你们的金樽清酒。寒假在家一直在学习nest。现在是已经学完了nest基础部分,开始往进阶学习了。我是通过看神光的文章学习的。基础知识呢又是最重要的。今天呢?我带大家了解一下什么是nestJs中的Aop编程,也叫面向切面编程。

声明:这是看了神光的课学习到的。大家想要更深入的了解可以看他的课。如有侵权行为必删。

什么叫面向切面编程呢?

nestjs采用的是MVC架构。相比于现代前端MVVM不同。

截屏2025-01-17 17.38.23.png 以前我们介绍过controller层是处理参数和路由的。Module层里面的provider是处理业务逻辑的。然后返回view视图。

在后端开发中,经常会有一些通用型的逻辑。比如打印日志呀,权限校验,异常处理。 那这些通用型逻辑写在什么地方呢?写在service里面,或者repository里面不够通过,难道每次都要调用这些方法嘛。那就写在controller里面,写在controller里面也会侵入业务逻辑,复用性不高。为此,我们得跳出业务逻辑,也就是在controller之前或之后进行统一的处理。nestjs提供了一种新的方式,叫AOP,面向切面编程。

什么叫面向切面编程呢?我们来看一下这个图。

截屏2025-01-17 17.47.02.png

在controller的前后,切一刀,来处理通用的逻辑,这就是AOP编程。这样复用性高,也不会侵入业务代码。

AOP编程的几种实现方式

AOP编程的实现方式呢有五种。

截屏2025-01-17 17.49.52.png 这是nestjs的官方的。我们来一一的了解一下吧。

Middware(中间件)

截屏2025-01-17 17.54.23.png 我们可以看到官方的图解,Middleware是发生在controller之前的。它可以获取请求数据和和响应数据。并且有一个next()函数。

我们可以尝试一下Middeware的用法。其实Middleware是express里面的功能。Nest的底层默认不是express嘛,还能切换fastify。

首先我们先创建一个项目。

npx nest new Aop
cd aop
npm run start

截屏2025-01-17 18.10.30.png 安装完之后将项目启动。 访问localhost:3000可以看到成功启动服务。

截屏2025-01-17 18.13.46.png

然后我们在main.ts里面全局启用Middleware。

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Response, Request, NextFunction } from 'express';

    async function bootstrap() {
        const app = await NestFactory.create(AppModule);
        app.use(function (req: Request, res: Response, next: NextFunction) {
         console.log(req.url, 'before');
        next();
        console.log('after');
     });
        await app.listen(process.env.PORT ?? 3000);
    }
bootstrap();

在路由里面也打印一下

截屏2025-01-17 18.24.20.png 我们再重启一下项目,访问一下页面。

截屏2025-01-17 18.25.49.png

可以看到打印结果。在cotroller之前和之后执行。不侵入业务代码。

在app.controller里面新增两个路由,aaa和bbb,访问localhost:3000/aaa和localhost:3000/bbb 可以看到控制台里面的打印结果。中间件在aaa路由和bbb路由的前后也会触发,说明可以高度服用逻辑。

截屏2025-01-17 18.32.06.png

这是全局使用Middleware,还可以在路由级别使用。 我们用nest cli创建一个路由中间件。

nest g middleware log --no-spec --flat

--no-spec是不生成测试文件。--flat是平铺,不生成文件夹。

截屏2025-01-17 18.47.58.png 我们分析一下怎么实现的。原来是继承实现了NestMiddleware类,用了@Injectable声明说明可以当成依赖注入。可以放到ioc容器中自动创建组装。

我们拿到app.module里面去使用。

import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';]
import { LogMiddleware } from './log.middleware';

@Module({
    imports: [],
    controllers: [AppController],
    providers: [AppService],
    })
    export class AppModule implements NestModule {
    configure(consumer: MiddlewareConsumer) {
    consumer.apply(LogMiddleware).forRoutes('aaa*');
    }
}

这样只有aaa开头的路由会生效这个中间件。

Guard

Guard相当于路由守卫,返回true或false来判断用户是否登录,有什么权限。

截屏2025-01-17 19.32.59.png

我们来看看怎么用。

截屏2025-01-17 19.34.03.png

Guards可以访问ExecutionContext实例,因此确切地知道接下来要执行的内容。它们的设计与异常过滤器、管道和拦截器一样,允许您在请求/响应周期的正确点插入处理逻辑,并声明性地这样做。这是官方说的,guard呢必须实现一个CanActivate类。然后根据获取到的ExecutionContext判断是否符合权限,返回true或false确定是否可以启用该路由。

那ExecutionContext里面又能获取到什么信息呢?

截屏2025-01-17 19.55.29.png 我们可以创建一个Guard看一下。

nest g guard login --no-spec --flat

截屏2025-01-17 20.01.40.png

这是生成的guard文件。它实现了canActive方法,里面有个参数ExecutionContext拿到我们想要的信息,进行权限验证,返回true或false决定是否执行这个路由。打印一下checked。

我们在controller里面用一下。

截屏2025-01-17 20.04.32.png

由于我们返回的是true,说明能走这个路由,再看一下打印的信息。

截屏2025-01-17 20.00.43.png

我们可以拿到请求的方式Get,请求的路由’/‘等等。

我们返回false试一下。 再访问这个路由就访问不通啦。

截屏2025-01-17 20.06.57.png

这就是Guard的作用。

Guard这种验证规则肯定要全局应用的。在main.ts里面全局应用这个实例。

截屏2025-01-17 20.09.28.png 这样所有的路由都可以使用了。

当然也可以作为provider全局启用。毕竟使用@Injectable装饰的。可以放进ioc容器,让ioc容器帮我们实例化并组装。

截屏2025-01-17 20.13.30.png 这样也能全局应用。而且可以注入其他的依赖。

Interceptor

Interceptor是拦截器,可以在controller之前或之后执行一些逻辑。

截屏2025-01-17 20.36.23.png

截屏2025-01-17 20.37.33.png

官方说要用@Injectable装饰器装饰,并且实现NestInterceptor接口。 我们来创建一个interceptor。

nest intorceptor 

截屏2025-01-17 20.56.46.png

我们可以看到生成的代码实现了NestInterceptore接口。并且有两个参数,一个是上下文信息,跟guard里面是一样的。一个是执行目标路由的函数。我们在里面添加该路由执行时间的代码。并在app.controller里面应用。

截屏2025-01-17 21.01.25.png 然后访问这个路由。

截屏2025-01-17 21.02.19.png 记得把前面全局应用的Guard注释掉,因为我们返回的是false,访问就是404报错了,不会走这个路由。 当然main.ts里面的Guard如果你没注释也要注释掉。

截屏2025-01-17 21.05.09.png 可以看到打印出开来的time。

ofcurse,也能全局应用,也有两种方式。

手动实例化 截屏2025-01-17 21.07.52.png 在module里面声明依赖关系

截屏2025-01-17 21.09.46.png 有时候我们需要对参数进行一些校验转换的一些通用逻辑,这时候要用上另一种AOP,pipe。而且用的特别多。对传入的表单数据进行校验之类的。

Pipe

截屏2025-01-17 21.21.34.png pipe是管道的意思,就是对参数做一些验证和转换。 我们先创建一个pipe。

截屏2025-01-17 21.24.18.png 这是我们生成的代码。value呢是从路由获取的参数,我们改一下。

截屏2025-01-17 21.42.03.png

对获取的参数value进行判断,如果可以被数字就转成数字乘10,不能就报错。 然后在controller里面写路由,用@Query获取url后面携带的参数。

截屏2025-01-17 21.44.44.png

截屏2025-01-17 21.40.56.png 拿到21的结果。我在url后面是num = 2.显然乘了10倍。

截屏2025-01-17 21.46.50.png 然后一样的可以用于某个参数,也可以用于全局。 手动实例化 截屏2025-01-17 21.49.59.png

还可以在app.module里面声明依赖。

截屏2025-01-17 21.51.59.png

ExceptionFilter

截屏2025-01-17 21.55.31.png exceptionFilter是用来抛出异常的。像前面我们用pipe的时候打印出来的异常就是它返回的。给出用户友好的错误反馈。

我们创建一个Exception filters。

nest g fliter test --no-spec --flat

截屏2025-01-17 21.59.13.png

用@Catch捕获错误。 对代码进行修改

import {
ArgumentsHost,
BadRequestException,
Catch,
ExceptionFilter,
} from '@nestjs/common';
import { Response } from 'express';


@Catch(BadRequestException)
    export class TestFilter implements ExceptionFilter {
    catch(exception: BadRequestException, host: ArgumentsHost) {
    const response: Response = host.switchToHttp().getResponse();
        response.status(400).json({
        statusCode: 400,
        message: 'test: ' + exception.message,
    });
    }
}

在handler上使用

截屏2025-01-17 22.09.11.png

可以看到刚才pipe抛出的错误信息发生了变化。 截屏2025-01-17 22.08.55.png

ExceptionFilter可以在controler层使用

截屏2025-01-17 22.11.53.png

也可以全局应用。 手动实例化。 截屏2025-01-17 22.14.19.png

也可以声明模块

截屏2025-01-17 22.15.54.png

各种AOP的执行顺序

我们可以看到,每种AOP有自己的功能,它的执行顺序呀肯定也是按照它的功能来决定的。我们画个图来捋一捋。

截屏2025-01-17 22.22.40.png 先中间件在controller之前可以做一些逻辑,然后是身份校验是否访问路由,请求拦截,对参数进行验证。通过路由处理业务逻辑,响应拦截,对前面抛出的错误进行响应,然后做出响应。

总结

nestjs中的AOP面向切面编程。可以在不改变业务逻辑的情况下,对一些通用逻辑进行一些切面的增删。提高了对通用逻辑的复用,又不影响业务逻辑。

❌
❌