对于前端开发来说,Nest中有很多全新的概念,这里我们来学习一下nest中AOP思想,并且学习Nest中提供的AOP方式。
AOP
Nest 实现 AOP 的方式更多,一共有五种,包括 Middleware、Guard、Pipe、Interceptor、ExceptionFilter。他们其实都是中间件的概念,只是分成了具体的内容,不同中间件做不同事情。
这张图很形象的介绍了nest整个运行周期所经过的核心逻辑流程。
中间件 Middleware
默认情况下由于nest底层是express提供服务,所以nest中间件就是express中的中间件。在nest中中间件有两种。一般用于处理请求和响应的,在nest提供的AOP服务中,请求最先到达的就是中间件处理层。
- 全局中间件,他作用于nest任何一个controller方法。
js
// main.ts
app.use((req, res, next) => {
console.log('global middleware before', req.url);
next();
console.log('global middleware after');
});
- 路由和控制器中间件。 中间件的定义就是继承自
NestMiddleware
接口,并实现use方法。我们一般在app.module.ts
中进行使用。
js
export class AppModule implements NestModule {
// 使用中间件
configure(consumer: MiddlewareConsumer) {
/**
* apply可以指定多个中间件函数或者类
* exclude 可以设置规则匹配排出的路由,一般用于设置控制器中间件。
*/
// 1.可以设置路由中间件(匹配规则,限定方法)
// consumer.apply(LoggerMiddleware).forRoutes({
// path: 'geturl*',
// method: RequestMethod.GET,
// });
// 1.可以设置特定路由
// consumer.apply(LoggerMiddleware).forRoutes('api-test/geturlparams/1');
// 2.设置控制器中间件(作用域当前控制器所有路由)
consumer.apply(LoggerMiddleware, fnMiddleWare).forRoutes(ApiTestController);
}
}
守卫 Guard
守卫是一个用 @Injectable()
装饰器的类,它实现了 CanActivate
接口。通过@UseGuards()
使用。在所有中间件之后、任何拦截器或管道之前执行。他相较于middleware
而言可以拿到程序执行的上下文。
-
全局守卫,nest提供了两种注册全局守卫的方式。我们来看看区别
- 直接在
main.ts
中进行注册。这种方式需要我们自己手动实例化,并不能交给nest的ioc容器。这样就导致我们不能在LoginGuard
中进行注入其他依赖。
jsapp.useGlobalGuards(new LoginGuard());
- 在
app.module.ts
中的provides
进行注入。这种方式就是是让LoginGuard
类交给nest去实例化和创建,并且可以注入其他依赖。
jsproviders: [ { provide: APP_GUARD, useClass: LoginGuard, }, ],
- 直接在
-
控制器和路由守卫 这种守卫,直接使用
UseGuards
装饰器在对应的controller
和方法
使用即可。
js
@UseGuards(LoginGuard)
getUrlParam(@Param('id') id: string) {
console.log('获取get请求url param参数');
return `获取get请求url param参数: ${id}`;
}
拦截器 Interceptor
拦截器是用 @Injectable()
装饰器的类并实现 NestInterceptor
接口。他和guard
一样都可以拿到程序执行的上下文。并且和Guard
的作用级别一样,有全局,控制器,路由级别。他区别于其他AOP,提供了前置和后置拦截器,可以在路由处理程序前后做一些逻辑处理。例如设置当前项目的接口标准返回值格式。
js
// 拦截器
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LogInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// context和guard传入的对象一样,可以获取当前执行上下文,next内部提供了一个handle方法,用于调用控制器方法
console.log('interceptor before...');
return next.handle().pipe(tap(() => console.log(`interceptor after...`)));
}
}
- 全局拦截器,也有两种方式,区分的作用和
Guard
一样。
js
// 方式一 main.ts
app.useGlobalInterceptors(new LogInterceptor());
// 方式二 app.module.ts
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LogInterceptor,
},
]
- 控制器和路由 这种拦截器,直接使用
UseInterceptors
装饰器在对应的controller
和方法
使用即可。
js
@UseInterceptors(LogInterceptor)
getUrlParam(@Param('id') id: string) {
console.log('获取get请求url param参数');
return `获取get请求url param参数: ${id}`;
}
了解了拦截器的用法后,我们就来看看如何实现企业级项目的接口的返回值格式。
js
// 接口一般都会返回和谐信息
{
data, // 数据
status: 0, // 接口状态值
extra: {}, // 拓展信息
message: 'success', // 异常信息
success:true // 接口业务返回状态
}
实现,就是拿到路由处理程序返回值做数据映射。
js
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
interface Response<T> {
data: T;
}
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<Response<T>> {
// 拿到路由处理程序返回值做数据映射
return next.handle().pipe(
map((data) => ({
data,
status: 0,
extra: {},
message: 'success',
success: true,
})),
);
}
}
管道 pipe
管道是用 @Injectable()
装饰器的类,它实现了 PipeTransform
接口。
管道有两个典型的用例:
- 转型:将输入数据转换为所需的形式(例如,从字符串到整数)。
- 验证:评估输入数据,如果有效,只需将其原样传递;否则抛出异常。
管道和其他AOP模型一样,都可以作用于路由,路由控制器和全局,并且管道还可以作用于路由处理器参数
。
js
import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException,
} from '@nestjs/common';
@Injectable()
export class TestPipe implements PipeTransform {
// 接受原始数据和方法参数的元素据
transform(value: any, metadata: ArgumentMetadata) {
if (typeof value !== 'number') {
throw new BadRequestException('url param不是数字');
}
return value * 10;
}
}
- 全局管道,也有两种方式,区分的作用和
Guard
一样。
js
// main.ts
app.useGlobalPipes(new TestPipe());
// app.module.ts
providers: [
{
provide: APP_PIPE,
useClass: TestPipe,
},
],
- 控制器,路由,路由处理函数参数管道
直接使用UsePipes
装饰器在对应的controller
和方法
和参数解析器(Param, Query, Body等等
)中使用即可。
Nest内置了九个开箱即用的管道,除了ParseFilePipe
外转化错误会抛出BadRequestException
异常,ParseFilePipe
中大小错误将抛出PayloadTooLargeException
异常,类型错误将抛出UnsupportedMediaTypeException
异常。
ValidationPipe
基于类验证器(class - validator)库,对传入的请求数据(通常是 DTO - Data Transfer Object 中的数据)进行全面验证。可以验证数据的格式、类型、范围等各种规则。
ParseIntPipe
专门用于将输入的值转换为整数类型。在处理需要整数类型数据的场景下非常有用,如处理路由参数、查询参数中的数字标识。
ParseFloatPipe
与 ParseIntPipe 类似,主要用于将输入的值转换为浮点数类型。适用于涉及金额、比例、坐标等需要浮点数表示的数据场景。
ParseBoolPipe
用于将输入的值转换为布尔类型。可以处理来自请求中的各种形式表示的布尔值,如字符串形式的 'true'、'false' 或者数字形式的 1、0 等。
ParseArrayPipe
能够将输入的数据解析为数组形式。可用于处理以特定格式传递的数组数据,如逗号分隔的字符串转换为数组,或者解析复杂格式的数组数据。
ParseUUIDPipe
专门用于解析通用唯一识别码(UUID)类型的数据。在处理与唯一标识相关的场景,如数据库主键为 UUID 类型或者处理外部系统传递的 UUID 标识时非常有用。
ParseEnumPipe
用于将输入的值转换为对应的枚举类型。在 NestJS 中,当业务逻辑使用枚举类型来表示特定的状态或类型时,此管道可确保传入的数据与定义的枚举类型相匹配。
DefaultValuePipe
当传入的参数可能为空或者未定义(null, undefined)时,为该参数设置一个默认值。这在处理可选参数或者数据可能缺失的情况下非常有用。
ParseFilePipe
用于处理文件上传相关的操作。它可以对上传文件的大小、类型等进行限制和验证,确保上传的文件符合应用的要求。
异常过滤器 ExceptionFilter
Nest 带有一个内置的异常层,负责处理应用中所有未处理的异常。当你的应用代码未处理异常时,该层会捕获该异常,然后自动发送适当的用户友好响应。
Nest 提供了一组继承自基 HttpException
的标准异常。
BadRequestException
UnauthorizedException
NotFoundException
ForbiddenException
NotAcceptableException
RequestTimeoutException
ConflictException
GoneException
HttpVersionNotSupportedException
PayloadTooLargeException
UnsupportedMediaTypeException
UnprocessableEntityException
InternalServerErrorException
NotImplementedException
ImATeapotException
MethodNotAllowedException
BadGatewayException
ServiceUnavailableException
GatewayTimeoutException
PreconditionFailedException
内置的异常处理层已经可以满足处理异常了,但是你可能希望根据某些动态因素添加日志记录或使用不同的 JSON 模式。异常过滤器正是为此目的而设计的。它们让你可以控制准确的控制流和发送回客户端的响应内容。
异常过滤器都需要实现ExceptionFilter
接口。使用@Catch()
修饰类,并且指定要处理的异常类型,如果未传递异常类型,那么该过滤器将捕获所有类型的异常。 这对于一个健壮的程序来说非常重要,返回统一且明确的信息,可以很快定位错误。
下面我们来看看如何来实现企业级程序的异常处理器。
我们需要处理程序中抛出的所有异常,所以我们就需要实现一个捕获全部异常的过滤器。
js
// base.exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpStatus,
ServiceUnavailableException,
LoggerService,
Inject,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
constructor(
// 该模块需要等到logger初始化完成之后在进行初始化。
@Inject(WINSTON_MODULE_NEST_PROVIDER)
private logger: LoggerService, // 可以注入日志系统
) {}
catch(exception: Error, host: ArgumentsHost) {
const ctx = host.switchToHttp(); // 获取请求上下文
const response = ctx.getResponse<Response>(); // 获取响应对象
const request = ctx.getRequest<Request>(); // 获取请求对象
this.logger.error(exception); // 输出错误日志
// 非 HTTP 标准异常的处理。
response.status(HttpStatus.SERVICE_UNAVAILABLE).send({
statusCode: HttpStatus.SERVICE_UNAVAILABLE,
timestamp: new Date().toISOString(),
path: request.url,
message: new ServiceUnavailableException().getResponse(),
});
}
}
如果程序抛出HttpException相关异常,我们就需要设置HttpException统一的异常过滤器,返回更加信息的错误内容。
js
// http.exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { BusinessException } from './business.exception';
import { Request, Response } from 'express';
@Catch(HttpException) // 指定HttpException,只捕获所有http相关的异常
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.getStatus(); // 获取当前错误的状态码。由于所有http异常都继承自HttpException,所以这里可以拿到
// 处理业务异常(主动抛出的异常)
if (exception instanceof BusinessException) {
const error = exception.getResponse(); // BusinessException 异常内部提供error, message属性
response.status(HttpStatus.OK).send({
data: null,
status: error['code'],
extra: {},
message: error['message'],
success: false,
});
return;
}
response.status(status).send({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: exception.getResponse(),
});
}
}
还有业务相关主动抛出异常我们也可以设置一种过滤器来进行处理。
js
// business.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';
import { BUSINESS_ERROR_CODE } from './business.error.codes';
type BusinessError = {
code: number;
message: string;
};
export class BusinessException extends HttpException {
constructor(err: BusinessError | string) {
if (typeof err === 'string') {
err = {
code: BUSINESS_ERROR_CODE.COMMON,
message: err,
};
}
super(err, HttpStatus.OK);
}
static throwForbidden() {
throw new BusinessException({
code: BUSINESS_ERROR_CODE.ACCESS_FORBIDDEN,
message: '抱歉哦,您暂无权限!',
});
}
}
// business.error.codes.ts
export const BUSINESS_ERROR_CODE = {
// 公共错误码
COMMON: 10001,
// 特殊错误码
TOKEN_INVALID: 10002,
// 禁止访问
ACCESS_FORBIDDEN: 10003,
// 权限已禁用
PERMISSION_DISABLED: 10003,
// 用户已冻结
USER_DISABLED: 10004,
};
这样我们项目中的异常处理就很健壮了,下面我们来测试一下相关功能。
调试
浏览器中调试(npm run start:debug
)
对于前端开发者来说,可能更习惯于在浏览器中进行调试。nest
也方便的提供了调试运行命令。npm run start:debug
。然后会启动一个ws
服务,我们就可以在内部进行断点调试了。
vscode中调试(npm run start:dev
)
可是一般开发后台服务并不需要关心页面内容,所以可能在编译器中调试会更合适,所以我们可以直接使用vscode
提供的调试服务。
创建一个launch.json
配置文件。然后通过debug运行项目。
js
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "debug nest",
"runtimeExecutable": "npm", // 运行命令
"args": [ // 命令参数
"run",
"start:dev",
],
"skipFiles": [
"<node_internals>/**"
],
"console": "integratedTerminal", // 用 vscode 的内置终端来打印日志
}
]
}
或者一个更简单的方式,ctrl + shift + p
搜索Toggle Auto Attach
。
然后启动运行命令调试程序即可。
提供器
提供器有两种方式注入到其他提供器和控制器中。
- 基于构造函数
js
constructor(private readonly apiTestService: ApiTestService) {}
- 基于属性
js
@Inject()
private readonly ApiTestService: ApiTestService;
提供器可以由多种方式构成,并且提供器的token可以是类和字符串构成,如果是字符串构成,我们在@Inject
时需要制定该字符串
- 基于
@Injectable
标识的类,使用useClass
引用。或者直接写类。
js
providers: [
ProviderTestService,
// ProviderTestService 等价于
{
provide: ProviderTestService,
useClass: ProviderTestService,
},
],
- 基于任意值,使用
useValue
引用。
js
{
provide: 'CUSTOM_VALUE',
useValue: 'CUSTOM_VALUE',
},
- 基于工厂函数,使用
useFactory
引用。这个可以注入其他提供器供其使用。并且支持异步。
js
{
provide: 'CUSTOM_FN',
async useFactory() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('00000s');
});
});
},
},
- 如果想要修改提供器的token,我们还可以使用
useExisting
设置别名。为已存在的provide设置别名。
js
providers: [
ProviderTestService,
// ProviderTestService 等价于
{
provide: ProviderTestService,
useClass: ProviderTestService,
},
{
provide: 'CUSTOM_VALUE',
useValue: 'CUSTOM_VALUE',
},
{
provide: 'CUSTOM_FN',
async useFactory() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('00000s');
});
});
},
},
// 修改ProviderTestService提供器的名称,但是之前的名称还是可以使用的
{
provide: 'aliasProvide',
useExisting: ProviderTestService,
},
],
往期年度总结
往期文章
- 半年没看vue官网,3.5刚刚发布,趁机整理下
- 啊,你还在找一款强大的表格组件吗?
- 前端大量数据层级展示及搜索定位预览
- 如何从0开始认识m3u8(提取,解析及下载)
- 展示大量数据节点(tree),引发的一次性能排查
- ts装饰器的那点东西
- 这是你所知道的ts类型断言和类型守卫吗?
- TypeScript官网内容解读
- 经常使用ts的你,知道这些内容?
- 你有了解过原生css的scope?
- 现在比较常用的移动端调试你知道哪些?
- 众多跨标签页通信方式,你知道哪些?(二)
- 众多跨标签页通信方式,你知道哪些?
- 反调试吗?如何监听devtools的打开与关闭
- 因为原生,选择一家公司(前端如何防笔试作弊)
- 结合开发,带你熟悉package.json与tsconfig.json配置
- 如何优雅的在项目中使用echarts
- 如何优雅的做项目国际化
- 近三个月的排错,原来的憧憬消失喽
- 带你从0开始了解vue3核心(运行时)
- 带你从0开始了解vue3核心(computed, watch)
- 带你从0开始了解vue3核心(响应式)
- 3w+字的后台管理通用功能解决方案送给你
- 入职之前,狂补技术,4w字的前端技术解决方案送给你(vue3 + vite )
专栏文章
🔥如果此文对你有帮助的话,欢迎💗关注 、👍点赞 、⭐收藏 、✍️评论, 支持一下博主~
公众号:全栈追逐者,不定期的更新内容,关注不错过哦!