MVC 架构
在 MVC 架构下,请求会先发送给 controller,由 controller 调度 model 层的 service 来完成 业务逻辑,然后,返回对应的 view。
在这个流程中,nest 还提供了 AOP(Aspect Oriented Programming)的能力,也就是面向切面编程的能力。
后端框架基本都是 AOP 架构
QA: ruby on rails 也是 AOP 架构吗?
💬点击展开/收起
严格来说,Ruby on Rails 并不标榜自己是一个“AOP(面向切面编程)框架”。它的核心架构身份是经典的 MVC 框架。
但是,Rails 在设计和日常开发中,非常深度且广泛地贯彻了 AOP 的核心思想。它并没有像 Java 的 Spring 或 Node.js 的 NestJS 那样显式地使用 Pointcut(切入点)、Advice(通知)这样的传统 AOP 术语,而是通过 Ruby 强大的动态语言特性(元编程)来实现同样的目标。
在 Rails 的语境中,AOP 的思想主要化身为了 Callbacks (回调)、Filters (过滤器) 和 Concerns (关注点)。
既然你在写关于 NestJS AOP 的文章 (2026-04-26-nest-aop.md),其实可以将 Rails 和 NestJS 的 AOP 概念做一个很好的映射:
1. 控制器层的“切面”(相当于 NestJS 的 Guards / Interceptors)
在 Rails 的 Controller 中,最经典的 AOP 体现就是 before_action、after_action 和 around_action。我们将横切关注点(如:身份验证、权限校验、日志记录)抽离出来,在不污染核心业务逻辑的情况下注入:
1
2
3
4
5
6
7
class ApplicationController < ActionController::Base
# 相当于 AOP 的 Before Advice / NestJS 的 Guard
before_action :require_login
# 相当于 AOP 的 Around Advice / NestJS 的 Interceptor
around_action :log_time_spent
end
2. 模型层的生命周期“切面”
Rails 的 ORM (ActiveRecord) 在数据的整个生命周期中预留了大量的切入点:
比如 before_validation, before_save, after_commit, before_destroy 等。这些钩子可以让你无侵入地在保存数据前后插入公共行为(例如密码哈希、自动生成编号、发送异步通知),这也是 AOP 典型的应用场景。
3. 横切关注点的组织:ActiveSupport::Concern
Rails 特意提供了一个名为 ActiveSupport::Concern 的特性。它的名字直译就是“关注点”,专门用来处理跨面关注点(Cross-cutting Concerns)。
你可以把在多个不同类中重复出现的逻辑(如:点赞功能、软删除逻辑)抽离成一个 Concern 模块,然后通过 include 直接混入到对应的 Model 或 Controller 中。这和 AOP 追求解藕和复用的理念完全一致。
4. 语言级支持:Module#prepend
不同于 Java/Spring 需要通过动态代理(Proxy)或字节码提升(CGLib)来实现 AOP,Ruby 本身是一门极其灵活的语言。通过原生的 Module#prepend(早期被称为 alias_method_chain),Rails 开发者或 Gem 包的作者可以轻松地包装一个正在运行的方法,在其周围包裹新的逻辑(拦截调用),从底层语言级别原生支持类似“环绕通知”的特性。
总结 Rails 并不是一个标准学术意义上的 AOP 架构,但它满身都是 AOP 模式的应用。
与 NestJS 相比,NestJS (受 Angular 和 Spring 启发)将 AOP 概念非常显性化、规范化,并拆分出了清晰的固定层级(Middleware、Guard、Interceptor、Pipe、Exception Filter)。而 Rails 则倾向于将这些思想“隐藏”在“Ruby 魔法”中,让你以为在用普通的方法回调(Callback),不知不觉间就吃到了 AOP 解藕的红利。
AOP是什么意思呢?什么是面向切面编程呢?
一个请求过来,可能会经过 Controller(控制器),Service(服务),Repository(数据库访问)的逻辑
如果想在这个调用链路里加入一些通用逻辑要怎么加呢?比如日志记录,权限控制,异常处理等。
容易想到的是直接改造 controller 层代码,加入这段逻辑。
这样可以,但是不优雅,应为这些通用的逻辑侵入到了业务逻辑里面。能不能透明的给这些业务逻辑加上日志,权限等处理呢?
那是不是可以在调用 controller 之前和之后加入一个执行通用逻辑的阶段呢?
比如这样
是不是就和切了一刀一样?
这样的横向扩展点就叫做切面,这种透明的加入一些切面逻辑的编程方式就叫做AOP(面向切面编程)。
其实 Express 的中间件的洋葱模型也是一种 AOP 的实现,应为你可以透明的在外面包一层,加入一些逻辑,内层感受不到。
QA: React 前端编程中有没有用到 AOP
💬点击展开/收起
在 React 前端编程中,虽然极少直接使用“AOP(面向切面编程)”这个名词,但 AOP 的核心思想(抽离横切关注点、无侵入式增强)在 React 中无处不在。
在后端(如 NestJS)中,AOP 常被用于鉴权、日志、异常截获等。在 React 中,处理这类非 UI 核心逻辑(横切关注点)时,演进出了几种非常经典的纯前端“AOP”模式:
1. 高阶组件 (HOC - Higher-Order Components)
这是 React 中最经典、最接近传统 AOP “代理/环绕通知 (Around Advice)” 概念的模式。 HOC 是一个函数,接收一个组件作为参数,返回一个包装后的新组件。你可以在包装过程中注入权限校验、埋点、主题等逻辑,而不修改原组件的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 典型的 AOP 思想:抽离鉴权逻辑
function withAuth(WrappedComponent) {
return function(props) {
const isAuthenticated = checkAuth();
if (!isAuthenticated) {
return <Redirect to="/login" />;
}
// 权限校验通过,渲染原组件,像极了 NestJS 的 Guard
return <WrappedComponent {...props} />;
};
}
// 使用切面增强
const ProtectedDashboard = withAuth(Dashboard);
2. Custom Hooks (自定义 Hook)
自从 React 16.8 引入 Hooks 后,Hooks 成为了目前分离横切关注点最主流的方式。 虽然 Hook 是“侵入式”地写在组件内部调用的,但它把复杂的副作用(数据获取、事件监听、埋点日志等)剥离到了独立的函数中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 剥离页面停留时间的埋点逻辑 (Cross-cutting concern)
function usePageTracking(pageName) {
useEffect(() => {
trackEvent('page_view', { page: pageName });
const timer = setInterval(() => trackTime(), 1000);
return () => clearInterval(timer);
}, [pageName]);
}
function HomePage() {
// 一行代码注入切面逻辑,不污染 UI 渲染
usePageTracking('Home');
return <div>Welcome Home</div>;
}
3. 状态级中间件 (例如 Redux Middleware)
如果你用过 Redux,它的 Middleware 是前端最标准的 AOP 实现。 在派发(Dispatch)一个动作(Action)到达状态(Reducer)之间,Redux 允许你插入一层或多层中间件。
这和 NestJS 的 Interceptor/Middleware 几乎是一模一样的设计。常用于:
*打印所有的状态变更日志(redux-logger)
*处理异步请求(redux-thunk, redux-saga)
*统一上报错误
4. 装饰器 (Decorators)
在 React 还在流行 Class Component(类组件)的时代,配合 Babel 的装饰器语法,前端的 AOP 写法和类似 Spring/NestJS 的写法可以说一模一样。比如 mobx 的 @observer,或者自定义的埋点装饰器:
1
2
3
4
5
@withAuth
@logRenderTime
class UserProfile extends React.Component {
render() { return <div>Profile</div>; }
}
5. 错误边界 (Error Boundaries) React 的 Error Boundary 专门用来捕获整个子组件树中发生的 JavaScript 错误,记录这些错误,并展示备用 UI。这实际上就是 AOP 中的 异常通知 (After Throwing Advice) 层面的应用(相当于 NestJS 里的 Exception Filters)。
总结
前端同学不爱把这些叫 AOP,主要是因为前端更推崇 FP(函数式编程) 和 Composition(组合) 的理念。
但在架构本质上: HOC** = 环绕代理 (Proxy / Around Advice) **Redux Middleware = 拦截器层 (Interceptor) Error Boundary** = 全局异常过滤器 (Exception Filter) **Custom Hook = 逻辑维度的切面注入
所以,React 早就把 AOP 思想融进了它的骨血里,只是换了一套前端更加习惯用语(Pattern)而已。
nest 有5种实现 AOP 的方式

1.Middleware
中间件是 express 里的概念,nest 的底层是 express,自然也可以使用中间件,但是做了进一步的细分, 分为全局中间件和路由中间件。
1.全局中间件
在 main.ts 通过 app.use 使用
1
2
3
4
5
6
7
app.use(function(req: Request, res: Response, next: NextFunction) {
console.log('before', req.url); // middleware
next(); // handler
console.log('after'); // middleware
})
可以在多个路由件复用中间件
在 handler 前后动态增加一些可复用的逻辑,就是 AOP 的切面编程的思想。
2.路由中间件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
@Injectable()
export class LogMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: () => void) {
console.log('before2', req.url);
next();
console.log('after2');
}
}
在 AppModule 中消费中间件
在 configure 方法里配置 LogMiddleware 在哪些路由生效
2.Guard
guard 是路由守卫的意思,可以用于在调用某个 controller 之前判断权限,返回 true 或者 false 来 决定是否放行
1
2
3
4
5
6
7
8
9
10
11
12
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class LoginGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
console.log('login check')
return false;
}
}
guard 要实现 CanActivate 接口,实现 canActivate 方法,可以冲 context 拿到请求的信息, 然后,做一些权限校验校验处理后返回 true 或 false。
1.在 AppController 中启用
- @UseGuards()
controller 本身不需要做什么修改,却透明的加上了权限判断的逻辑,这就是 AOP 架构的好处。
2.app.useGlobalGuards 全局启用guard
手动 new的 Guard 实例,不在 IoC 容器中
3.在 AppModule 中全局启用 guard
用 provider 的方式声明的 guard 在 IoC 容器里,可以注入别的 provider
- APP_GUARD
注入 AppService 到 guard 中
3.Interceptor
拦截器,可以在目标 controller 方法前后加入一些逻辑
Interceptor 要实现 NestInterceptor 接口,实现 intercept 方法,调用 next.handle() 就会 调用目标 controller,可以在之前和之后加入一些逻辑。处理逻辑可能是异步的,nest 里通过 rxjs 来 组织它们,所以可以使用 rxjs 的各种 operator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, tap } from 'rxjs';
@Injectable()
export class TimeInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const startTime = Date.now();
return next.handle().pipe(
tap(() => {
console.log('time: ', Date.now() - startTime)
})
);
}
}
tap() 操作符,它会在可观察流正常或异常终止时调用我们的匿名日志记录函数,但不会干扰响应周期。
@UseInterceptor 启用 interceptor
interceptor 和 middleware 之间的区别,在于参数不同,interceptor 可以拿到调用的 controller 和 handler
在 controller 和 handler 上加一些 metadata,这种只有 interceptor 和 guard 里可以取出来,middleware 不行
interceptor 支持每个路由单独启用,只作用于莫个 handler
可以在 controller 级别启用,作用于下面所有 handler
也可以全局启用,作用于所有 controller
4.Pipe
对参数的处理逻辑也是一个通用的逻辑,nest 抽出了对应的切面 - Pipe
Pipe 管道,用来对参数做一些校验和转换

Pipe 要实现 PipeTransform 接口,实现 transform 方法,可以对传入的参数值 value 做参数校验, 比如格式,类型是否正确,不正确抛出异常。也可以做转换,返回转换后的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common';
@Injectable()
export class ValidatePipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
if(Number.isNaN(parseInt(value))) {
throw new BadRequestException(`参数${metadata.data}错误`)
}
return typeof value === 'number' ? value * 10 : parseInt(value) * 10;
}
}
- @UsePipe
- app.useGlobalPipes
- APP_PIPE
5.Exception Filter
对抛出的异常进行处理,返回对应的响应

实现 ExceptionFilter 接口,catch 方法,就可以拦截异常了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
})
}
}
- @UseFilters
- app.useGlobalFilters
- APP_FILTER