Nest.js
对于Nest.js 通常的文件结构如下:一个文件夹下放了三个文件,分别是 .controller.ts
.module.ts
.service.ts
。这三个文件通常会定义一个功能模块的控制器、模块和服务。
- module:是用来对外暴露当前模块,以及该模块下需要用到的controller和service都需要在module中引入并注册,最终在app.module中集中引入。
- controller:nestjs作为web服务对外暴露请求接口路径,controller就是用来处理这些请求路径的控制器,一个controller对应一个模块模块下有多个具体接口。
- service:controller中每个独立请求的具体处理逻辑,每个controller在使用service的时候需要在constructor注入该service。
控制器
要创建一个Nest.js控制器,你可以使用Nest CLI的nest generate controller
命令,或者手动创建一个带有@Controller()
装饰器的类。下面是个例子:
ts
import { Controller, Get } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
findAll() {
return 'This action returns all cats';
}
}
控制器通常会调用服务来处理业务逻辑。例如,你可能有一个CatsService
提供了一个findAll()
方法,你可以在控制器中注入这个服务并使用这个方法:
ts
import { Controller, Get } from '@nestjs/common';
import { CatsService } from './cats.service';
@Controller('cats')
export class CatsController {
constructor(private catsService: CatsService) {}
@Get()
findAll() {
return this.catsService.findAll();
}
}
在这个例子中,CatsController
通过构造函数注入了CatsService
,并在findAll()
方法中调用了catsService.findAll()
来获取所有的猫。
服务
在Nest.js中,服务(Service)通常是一个类,它包含了应用的业务逻辑。服务可以被注入到控制器中,控制器将使用服务来处理HTTP请求。如下示例:
ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
create(cat: Cat) {
this.cats.push(cat);
}
findAll(): Cat[] {
return this.cats;
}
}
注:
@Injectable()
装饰器是必需的,它将类标记为可以被注入到其他类中(例如,控制器)。
模块
在Nest.js中,模块是一个带有@Module()
装饰器的类,它封装了一组相关的功能,如服务、控制器、提供者等。
每个Nest.js应用都有至少一个模块,即根模块。你可以将应用想象为一个树结构,根模块是树根,其他模块都是从根模块派生出来的。
模块的主要目的是组织代码,并通过封装来促进代码重用。模块可以导入其他模块,并可以导出它们自己的一部分,如服务,以便其他模块可以使用。如下示例:
ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
在这个例子中,CatsModule
是一个模块,它提供了CatsController
和CatsService
。@Module()
装饰器接收一个对象,这个对象描述了模块的元数据:
controllers
是一个数组,包含模块中的所有控制器。providers
是一个数组,包含模块中的所有服务和其他可以注入的提供者。
你还可以在@Module()
装饰器中指定imports
和exports
字段。imports
字段用于导入其他模块,exports
字段用于导出服务和其他可以在其他模块中使用的元素。如下示例:
ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
import { DogsModule } from './dogs.module';
@Module({
imports: [DogsModule],
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService],
})
export class CatsModule {}
imports
字段包含了DogsModule
。这意味着CatsModule
依赖于DogsModule
,并且可以使用DogsModule
导出的所有提供者(例如服务)。但是,你不能访问到被导入模块中的控制器。exports
字段包含了CatsService
。这意味着CatsService
可以被其他模块使用,前提是这些模块导入了CatsModule
。 这是DogsModule
的可能实现:
ts
import { Module } from '@nestjs/common';
import { DogsController } from './dogs.controller';
import { DogsService } from './dogs.service';
@Module({
controllers: [DogsController],
providers: [DogsService],
exports: [DogsService],
})
export class DogsModule {}
请求处理流程
在Nest.js中,请求的处理流程通常遵循以下步骤:
- 中间件(Middleware):请求首先通过中间件,这是一些函数,可以访问请求对象(
req
)、响应对象(res
),以及应用程序的请求-响应循环中的下一个中间件函数(next
)。中间件可以执行代码、修改请求和响应对象、结束请求-响应循环,或者调用堆栈中的下一个中间件。 - 守卫(Guards): 守卫是Nest.js中负责授权的特殊组件。如果守卫决定请求不应该继续执行,它可以结束请求流程。
- 拦截器(Interceptors) : 拦截器可以在函数执行之前或之后添加额外的逻辑。它们对于添加日志、转换返回结果、添加额外的错误处理逻辑等非常有用。
- 管道(Pipes) : 管道用于处理请求数据的转换和验证。例如,它们可以将客户端发送的数据转换为服务器期望的格式,或者验证输入数据是否符合预期的模式。
- 路由处理器(Route Handler) : 在通过了所有上述层之后,请求到达路由处理器,这里是定义业务逻辑的地方。
- 异常过滤器(Exception Filters) : 如果在处理请求的过程中抛出异常,异常过滤器可以捕获这些异常,并根据需要进行处理。
- 响应(Response) : 最后,服务端处理完请求后,会生成响应并发送回客户端。
装饰器
在Nest.js中,装饰器是一种特殊的声明,可以附加到类、方法、访问器、属性或参数上。装饰器使用表达式形式,该表达式在运行时执行,返回一个函数,这个函数会在目标上调用,以实现装饰的效果。
1、内置装饰器
Nest.js中的内置的装饰器
及其可以接受的参数:
-
@Request() 或 @Req():这个装饰器通常不带参数,用于获取完整的请求对象。
-
@Body():这个装饰器可以接受两个参数。
- 第一个参数是你想要获取的请求体中的属性名称(字符串),这个参数是可选的。如果你不提供这个参数,那么它会返回整个请求体。
- 第二个参数是一个管道类,这也是可选的。你可以使用管道来转换或验证请求体中的数据。
-
@Query():这个装饰器可以接受两个参数,和
@Body()
类似。- 第一个参数是你想要获取的查询字符串中的属性名称(字符串),这个参数是可选的。如果你不提供这个参数,那么它会返回整个查询字符串。
- 第二个参数是一个管道类,这也是可选的。你可以使用管道来转换或验证查询字符串中的数据。
-
@Param():这个装饰器可以接受两个参数,和
@Body()
类似。- 第一个参数是你想要获取的路由参数中的属性名称(字符串),这个参数是可选的。如果你不提供这个参数,那么它会返回所有的路由参数。
- 第二个参数是一个管道类,这也是可选的。你可以使用管道来转换或验证路由参数中的数据。
-
@Headers():这个装饰器可以接受一个参数,就是你想要获取的头部信息中的属性名称(字符串)。这个参数是可选的,如果你不提供这个参数,那么它会返回所有的头部信息。
-
@Session():这个装饰器通常不带参数,用于获取
session
对象。 -
@UploadedFile() 和 @UploadedFiles():这两个装饰器是用于文件上传的,通常不带参数。
2、自定义装饰器
装饰器接受三个参数:
- target:被装饰的元素所属的对象。在下面例子中,target 是 Person 类的原型。
- key:被装饰的元素的名称。在下面例子中,key 是 "getName"。
- descriptor:被装饰的元素的属性描述符。在下面例子中,descriptor 是 getName 方法的属性描述符。
descriptor
是一个对象,它包含了被装饰的方法的一些元数据,例如它的值(value,即方法本身)、是否可写(writable)、是否可配置(configurable)等。这个对象是由 Object.getOwnPropertyDescriptor 方法返回的。
注: 装饰器的参数取决于它装饰的是什么。
- 如果装饰的是类,那么参数就是类的构造函数;
- 如果装饰的是类的方法,那么参数就包括目标类的原型(下面的例子中就是
Person
类)、方法名以及方法的属性描述符; - 如果装饰的是类的属性,那么参数就是目标类的原型和属性名。
例子:
js
function nameDecorator(target, key, descriptor) {
descriptor.value = () => {
return 'Tom';
};
return descriptor;
}
class Person {
constructor() {
this.name = 'Lily';
}
@nameDecorator
getName() {
return this.name;
}
}
let p1 = new Person();
console.log(p1.getName());
// nameDecorator 的作用是修改 getName 方法的行为。在装饰器中,它将 descriptor.value(也就是 getName 方法)修改为一个新的函数,这个函数返回 "Tom",而不是原来的 this.name。然后,装饰器返回修改后的 descriptor。
// 是否可写(writable)
function readonly(target, key, descriptor) {
descriptor.writable = false;
return descriptor;
}
class Person {
@readonly
getName() {
return "Person's Name";
}
}
// readonly 装饰器将 getName 方法的 descriptor.writable 属性设置为 false。这意味着你不能再修改 getName 方法了,例如你不能将其重新赋值为一个新的函数。如果你尝试这样做,JavaScript 会抛出一个错误。
如果你想给自定义装饰器中传入额外的参数,你可以如下操作,这个函数接受你的参数,然后返回实际的装饰器函数。
js
function nameDecorator(modify) {
return function(target, key, descriptor) {
// 使用 modify 参数
};
}
class Person {
@nameDecorator('Tom')
getName() {
// ...
}
}
拦截器 Interceptor
在 NestJS 中,拦截器是一种可以拦截并修改处理请求的控制器方法的返回结果的工具。它们还可以用来处理函数执行之前的操作,例如记录日志、错误处理等。拦截器可以被应用到整个应用、特定模块、特定控制器或者特定路由处理程序。你可以根据需要选择不同的地方使用拦截器。
拦截器作用:
- 在函数执行前/后,绑定额外逻辑
- 转换从函数返回的结果
- 转换从函数抛出的异常
- 重写函数
拦截器的主要目的是包装和控制从路由处理程序返回的数据流。这通常通过在intercept()
方法中调用next.handle()
来完成,它会返回一个表示数据流的Observable
对象,然后你可以使用RxJS 操作符来转换或处理这个数据流。 例如,你可以使用map()
操作符来转换返回的数据:
ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class MyInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(map(data => ({ data })));
}
}
在这个例子中,MyInterceptor
会把从路由处理程序返回的数据包装在一个对象中,对象的data
属性就是原始数据。
intercept()
方法接收两个参数
- 第一个是
ExecutionContext
对象,它封装了处理请求的所有信息。- 第二个参数是
CallHandler
对象,它表示下一个处理程序在处理链中的位置。
1、全局拦截器
如果你想让一个拦截器应用到你的整个应用,你可以在你的主模块(通常是 AppModule
)中使用 app.useGlobalInterceptors()
方法:
ts
import { MyInterceptor } from './my.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new MyInterceptor());
await app.listen(3000);
}
bootstrap();
2、模块拦截器
如果你想让一个拦截器应用到一个特定模块,你可以在模块的 providers
数组中添加它:
ts
import { MyInterceptor } from './my.interceptor';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
// 为了让拦截器在整个模块级别生效,我们需要使用特殊的标记 APP_INTERCEPTOR
useClass: MyInterceptor,
},
],
})
export class MyModule {}
3、控制器或路由处理程序拦截器
如果你想让一个拦截器应用到一个特定控制器或者特定路由处理程序,你可以使用 @UseInterceptors()
装饰器:
ts
import { MyInterceptor } from './my.interceptor';
@Controller('my')
@UseInterceptors(MyInterceptor)
export class MyController {
@Get()
@UseInterceptors(AnotherInterceptor)
getSomething() {
return 'something';
}
}
注: 在这个例子中,MyInterceptor
将会应用到 MyController
的所有路由处理程序,而 AnotherInterceptor
只会应用到 getSomething()
方法。
Guard 守卫
Nest.js中的守卫(Guards)是一种负责控制路由请求是否应该继续进行的特殊类。它们可以决定一个请求是否应该被处理,或者是否应该拒绝并返回一个错误。守卫在路由处理器之前运行,因此可以用于鉴权或任何其他类型的前置条件检查。Guard 守卫是一个类,它实现了 CanActivate
接口。这个接口要求提供一个 canActivate()
方法,该方法应该返回一个 boolean 值或一个返回 boolean 值的 Promise 或 Observable。
下面是一个基础的 Guard 守卫的例子:
ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
@Injectable()
export class MyGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
// 从请求头中获取认证令牌
const token = request.headers.authorization;
// 验证令牌是否有效
const isValidToken = this.validateToken(token);
// 如果令牌有效,则允许请求继续处理
// 否则,拒绝请求
return isValidToken;
}
private validateToken(token: string): boolean {
// 这里应该是你的令牌验证逻辑
// 在这个例子中,我们简单地检查令牌是否存在
return !!token;
}
}
canActivate
方法的参数是一个ExecutionContext
实例。ExecutionContext
是一个包含了所有关于执行流程的详细信息的类,包括请求对象、响应对象、路由处理器等。
Nest.js 的 ExecutionContext
提供了不同的方法来处理不同类型的上下文。
switchToHttp()
这个方法会返回一个HttpArgumentsHost
对象,你可以从中获取请求对象(getRequest()
)和响应对象(getResponse()
).switchToRpc()
: 当你在处理微服务(RPC)请求时使用。返回一个RpcArgumentsHost
对象,你可以从中获取数据(getData()
)和上下文(getContext()
)。switchToWs()
: 当你在处理 WebSocket 事件时使用。返回一个WsArgumentsHost
对象,你可以从中获取客户端(getClient()
)和数据(getData()
)。
更多关于ExecutionContext
的内容:点击以下链接 execution-context
和拦截器一样,Guard 守卫可以被应用到整个应用、特定模块、特定控制器或者特定路由处理程序。和拦截器不同的是,Guard 守卫使用的是 APP_GUARD
标记,而不是 APP_INTERCEPTOR
。
全局守卫、模块守卫、控制器守卫写法...
ts
// 全局守卫
app.useGlobalGuards(new MyGuard());
// 模块守卫
@Module({
providers: [
{
provide: APP_GUARD,
useClass: MyGuard,
},
],
})
// 控制器或路由处理程序守卫
@UseGuards(MyGuard)
@Controller('my')
export class MyController {
@Get()
@UseGuards(AnotherGuard)
getSomething() {
return 'something';
}
}
中间件
中间件 ------ 客户端 和 路由处理 的中间,在路由处理程序之前,调用的函数。中间件函数可以访问:请求对象、响应对象、请求响应周期中的 next()
中间件函数。中间件只能在模块中注册。
Nest.js中间件的主要特点:
- 中间件函数是最早执行的函数,可以在路由处理之前执行一些代码。
- 中间件函数可以执行任何代码。
- 中间件函数可以修改请求和响应对象。
- 中间件函数可以结束请求-响应周期。
- 中间件函数如果不结束请求-响应周期,必须调用next(),以将控制权传递给下一个中间件函数。
next() 中间件函数,通常由名为 next 的变量表示,它决定了请求-响应的循环系统
一个中间件的例子:
ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('Request...');
next();
}
}
use方法的三个参数
- req: 这是Express.js中的Request对象,代表了HTTP请求。它包含了请求头、请求参数、请求体等信息,你可以通过它来获取或者修改请求的相关信息。
- res: 这是Express.js中的Response对象,代表了HTTP响应。你可以通过它来设置响应头、响应状态码或者发送响应内容。
- next: 这是一个函数,用于将控制权传递给下一个中间件。如果你的中间件函数不是结束请求-响应周期的,那么你必须调用next()函数,否则请求将会被挂起。 在 Nest.js 中,你可以通过
app.use()
方法来为整个应用添加中间件。对于特定的模块、控制器或路由,你可以在模块的configure()
方法中使用forRoutes()
方法。
scssnext(); // 传递控制权给下一个中间件
在模块、控制器、路由中使用中间件
ts
import { Module, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { LoggerMiddleware } from './logger.middleware';
import { MyController } from './my.controller';
// 在模块中应用中间件:
@Module({
controllers: [MyController],
})
export class MyModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('*'); // 应用到所有路由
}
}
// 在模块中应用中间件到特定控制器:
@Module({
controllers: [MyController],
})
export class MyModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes(MyController); // 应用到 MyController 中的所有路由
}
}
// 在模块中应用中间件到特定路由:
@Module({
controllers: [MyController],
})
export class MyModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes({ path: 'myroute', method: RequestMethod.GET }); // 应用到 GET /myroute
}
}
管道
管道本质:一个实现了 PipeTransform
接口,并用 @Injectable(
) 装饰器 修饰的类
管道作用:
- 转换:将输入数据转换为所需的格式输出。
- 验证:验证输入的内容是否满足预先定义的规则,当数据不正确时可能会抛出异常。
- 无论是全局管道,控制器级别的管道,还是方法级别的管道,每次都只处理一个参数。NestJS的管道设计是基于每个单独的参数来工作的。
- 这意味着,如果你在一个方法中有多个参数,并且你为每个参数应用了管道,那么每个管道都会分别对每个参数进行处理。
- 如果你在控制器或全局级别应用了管道,那么这个管道会被应用到该控制器或全局范围内的每个方法的每个参数上。也就是说,如果一个方法有三个参数,管道将会被执行三次,每次处理一个参数。
例子:
ts
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
if (!value) {
throw new BadRequestException('Value not provided');
}
return value;
}
}
transform函数的两个参数
- 第一个参数是要转换的值,也就是参数的原始值
- 第二个参数是一个
ArgumentMetadata
对象它包含了有关参数的元数据信息,如参数的类型(是否是路由参数、请求体等)和元数据字符串(例如参数的名称)。
管道可以应用到模块、控制器和路由。
ts
// 应用到整个应用
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { MyPipe } from './my.pipe';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new MyPipe());
await app.listen(3000);
}
bootstrap();
// 应用到特定模块
import { Module, ValidationPipe } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
],
})
export class AppModule {}
// 应用到特定路由处理程序
import { Controller, Get, UsePipes } from '@nestjs/common';
import { MyPipe } from './my.pipe';
@Controller('my')
export class MyController {
@Get()
@UsePipes(MyPipe)
getSomething() {
return 'something';
}
}
异常过滤
异常过滤器是一种特殊类型的管道,用于处理由路由处理程序抛出的异常。你可以使用异常过滤器来自定义应用程序如何处理不同类型的异常。 创建一个简单的异常过滤器:
ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp(); // 获取当前的HTTP上下文
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception instanceof HttpException ? exception.getStatus() : 500;
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: exception['response'] || exception['message'] || null,
});
}
}
@Catch()
装饰器用于指定过滤器应捕获哪些异常类型。如果没有提供任何参数,则过滤器将捕获所有异常。
*
- 捕获指定的异常类型:@Catch(HttpException)
- 捕获多种异常类型:@Catch(HttpException, NotFoundException)
catch()
方法是ExceptionFilter
接口的一部分,它接收两个参数:第一个是抛出的异常,第二个是ArgumentsHost
对象。ArgumentsHost
是一个包装了原始参数的工具类,提供了一些有用的方法来获取处理程序方法参数的类型(例如,HTTP具体上下文)。
在应用、模块、控制器和路由处理程序级别使用异常过滤器的示例:
ts
// 整个项目中:
app.useGlobalFilters(new AllExceptionsFilter());
// 模块级别
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { AllExceptionsFilter } from './all-exceptions.filter';
@Module({
providers: [
{
provide: APP_FILTER,
useClass: AllExceptionsFilter,
},
],
})
export class AppModule {}
// 控制器级别
import { Controller, UseFilters } from '@nestjs/common';
import { AllExceptionsFilter } from './all-exceptions.filter';
@Controller('my-controller')
@UseFilters(new AllExceptionsFilter())
export class MyController {}
//路由处理程序级别
import { Controller, Post, UseFilters } from '@nestjs/common';
import { AllExceptionsFilter } from './all-exceptions.filter';
@Controller('my-controller')
export class MyController {
@Post()
@UseFilters(new AllExceptionsFilter())
create() {
// ...
}
}
常见的RxJS操作符
RxJS(Reactive Extensions for JavaScript)是一个用于处理异步数据流的库,它提供了很多操作符,可以用来创建、转换、过滤、合并等操作数据流。
以下是一些常见的RxJS操作符:
map()
: 对源Observable的每个输出应用给定的投影函数,并将结果值作为Observable返回。filter()
: 只有当给定的过滤函数返回true时,才将源Observable的值传递给输出Observable。tap()
: 对源Observable的每个值应用给定的函数,用于在不改变源Observable的情况下执行副作用。catchError()
: 捕获Observable中的错误,可以返回一个新的Observable或者抛出一个错误。switchMap()
: 对源Observable的每个值应用一个函数,该函数返回一个内部Observable。然后每次内部Observable发出值,就将这些值发出到输出Observable中。当新的内部Observable被发出时,先前的内部Observable的订阅将被取消。mergeMap()
或flatMap()
: 对源Observable的每个值应用一个函数,该函数返回一个内部Observable。然后将这些Observable合并到一个单一的Observable,无论这些Observables何时完成。concatMap()
: 对源Observable的每个值应用一个函数,该函数返回一个内部Observable。然后将这些Observables逐一地发出到输出Observable,等待每个Observable完成后再订阅下一个。take()
: 只从源Observable中发出前N个值,然后完成。first()
: 从源Observable中发出第一个值(或满足某个条件的第一个值),然后完成。last()
: 从源Observable中发出最后一个值(或满足某个条件的最后一个值),然后完成。
更多可以参考官方文档 RxJs.API