在Nest+GrqphQL+Prisma+React全栈开发这个系列中,我将会带领着大家从零到一,一步一步简单学会使用这些技术栈,并实际开发一个实战应用。
这篇是第三篇文章,Nest篇。
一、简介
Nestjs是一个高效的nodejs服务端应用开发框架,扩展性高,性能好,优点还包括原生支持TS,以及底层构建在强大的Http框架上,你可以自己选择是使用express或者fastify,以及支持模块化,微服务,装饰器语法,依赖注入(将自己的依赖的实例化交给外部容器控制)等等特性。
可以说非常强大好用。
二、结构
Nest主要有以下这几个部分组成:
- 控制器
- 提供者
- 模块
- 中间件
- 异常过滤器
- 管道
- 守卫
- 拦截器
- 装饰器
下面我们一一来简单的介绍下这些模块:
(一)控制器
控制器负责处理传入的请求和向客户端返回响应,一个控制器包含多个路由。
/* cats.controller.ts */
import { Controller, Get, Post, Res, HttpStatus } from '@nestjs/common';
import { Response } from 'express';
@Controller('cats')
export class CatsController {
@Post()
create(@Res() res: Response) {
res.status(HttpStatus.CREATED).send();
}
@Get()
findAll(@Res() res: Response) {
res.status(HttpStatus.OK).json([]);
}
}
复制代码
(二)提供者
用 @Injectable()
装饰器注释的类,例如nest中的service
, repository
, factory
, helper
等都可以被视为提供者。Nest中的依赖管理是通过依赖注入这个设计模式实现的,一般是通过控制器的构造函数注入的:
constructor(private readonly catsService: CatsService) {}
复制代码
也可以使用@Inject()
基于属性注入:
import { Injectable, Inject } from '@nestjs/common';
@Injectable()
export class HttpService<T> {
@Inject('HTTP_OPTIONS')
private readonly httpClient: T;
}
复制代码
(三)模块
模块是具有 @Module()
装饰器的类。 @Module()
装饰器提供了元数据,Nest 用它来组织应用程序结构。每个 Nest 应用程序至少有一个模块,即根模块。根模块是 Nest 开始安排应用程序树的地方。
创建一个你自己的功能模块:
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
复制代码
然后在app.module.ts
中导入:
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})
export class ApplicationModule {}
复制代码
如果想要模块共享的话,只需在exports
中导出:
exports: [CatsService]
复制代码
如果你想在任何地方导入相同的模块,可以设置为全局模块:
@Global()
@Module({
...
})
export class CatsModule {}
复制代码
Nest还支持动态模块,这些模块可以动态注册和配置提供程序,使用forRoot
进行配置:
@Module({
...
})
export class DatabaseModule {
static forRoot(entities = [], options?): DynamicModule {
const providers = createDatabaseProviders(options, entities);
return {
module: DatabaseModule,
providers: providers,
exports: providers,
};
}
}
复制代码
(四)中间件
中间件是在路由处理程序 之前 调用的函数。Nest 中间件实际上等价于 express 中间件。
中间件有以下作用:
- 对请求和响应对象进行更改。
- 结束请求-响应周期。
- 调用堆栈中的下一个中间件函数。
- 如果当前的中间件函数没有结束请求-响应周期, 它必须调用
next()
将控制传递给下一个中间件函数。否则, 请求将被挂起。
我们可以在函数中或在具有 @Injectable()
装饰器的类中实现自定义 Nest
中间件,只需要实现 NestMiddleware
接口即可。
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('Request...');
next();
}
}
复制代码
不过中间件的使用需要注意以下几点:
- 中间件不能在
@Module()
装饰器中列出 - 必须使用模块类的
configure()
方法来设置它们 - 包含中间件的模块必须实现
NestModule
接口
@Module({
...
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('cats');
}
}
复制代码
如果是多个中间件,可以这样写:
consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);
复制代码
可以直接在app上直接注册全局中间件:
const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);
复制代码
(五)异常过滤器
处理整个应用程序中的所有抛出的异常。Nest提供了一系列继承自核心异常 HttpException
的可用异常:
BadRequestException
UnauthorizedException
NotFoundException
ForbiddenException
NotAcceptableException
RequestTimeoutException
ConflictException
GoneException
PayloadTooLargeException
UnsupportedMediaTypeException
UnprocessableException
InternalServerErrorException
NotImplementedException
BadGatewayException
ServiceUnavailableException
GatewayTimeoutException
不过我们也可以自定义异常过滤器,使用@Catch()
装饰器绑定所需的元数据到异常过滤器上,如果@Catch()
参数列表为空,将会捕获每一个未处理的异常(不管异常类型如何)。
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
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();
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
复制代码
绑定过滤器:
@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
复制代码
你也可以使用全局范围的过滤器:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();
复制代码
(六)管道
具有 @Injectable()
装饰器的类,负责对数据转换或者验证。Nest
自带八个开箱即用的管道,即
ValidationPipe
ParseIntPipe
ParseBoolPipe
ParseArrayPipe
ParseUUIDPipe
DefaultValuePipe
ParseEnumPipe
ParseFloatPipe
如果想要自定义管道,只应实现 PipeTransform
接口:
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value;
}
}
复制代码
使用@UsePipes()
进行管道的绑定:
@Post()
@UsePipes(new JoiValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
复制代码
(七)守卫
使用 @Injectable()
装饰器的类,根据运行时出现的某些条件(例如权限,角色,访问控制列表等)来确定给定的请求是否由路由处理程序处理。
守卫在每个中间件之后执行,但在任何拦截器或管道之前执行。
守卫必须实现一个canActivate()函数。此函数应该返回一个布尔值,指示是否允许当前请求。它可以同步或异步地返回响应:
- 如果返回
true
, 将处理用户调用。 - 如果返回
false
, 则Nest
将忽略当前处理的请求。
例如实现一个授权守卫:
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return validateRequest(request);
}
}
复制代码
使用也很简单:
@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}
复制代码
也可以设置全局守卫:
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());
复制代码
我们还可以为守卫增加反射器,获取不同路由的不同元数据,以此做出决策。
先使用@SetMetadata()
设置元数据:
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
复制代码
使用反射器获取设置的元数据:
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
return matchRoles(roles, user.roles);
}
}
复制代码
(八)拦截器
使用 @Injectable()
装饰器注解的类,可以转换、扩展、重写函数,以及在函数之前之后绑定额外逻辑。
每个拦截器都有 intercept()
方法,它接收2个参数。 第一个是 ExecutionContext
实例(与守卫完全相同的对象)。第二个参数是 CallHandler``返回一个
Observable`。
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');
const now = Date.now();
return next
.handle()
.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}
复制代码
绑定拦截器:
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
复制代码
绑定全局拦截器:
const app = await NestFactory.create(ApplicationModule);
app.useGlobalInterceptors(new LoggingInterceptor());
复制代码
(九)装饰器
Nest
是基于装饰器这种语言特性而创建的,你还可以自定义自己的装饰器。
例如我们可以自定义一个参数装饰器:
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
});
复制代码
然后使用它:
@Get()
async findOne(@User() user: UserEntity) {
console.log(user);
}
复制代码
Nest
还可以聚合多个装饰器。例如,假设您要将与身份验证相关的所有装饰器聚合到一个装饰器中。这可以通过以下方法实现:
import { applyDecorators } from '@nestjs/common';
export function Auth(...roles: Role[]) {
return applyDecorators(
SetMetadata('roles', roles),
UseGuards(AuthGuard, RolesGuard),
ApiBearerAuth(),
ApiUnauthorizedResponse({ description: 'Unauthorized"' })
);
}
复制代码
使用聚合装饰器:
@Get('users')
@Auth('admin')
findAllUsers() {}
复制代码
三、使用
$ npm i -g @nestjs/cli
$ nest new nest-demo
复制代码
使用nest的cli
将会创建一个最简单的新项目目录,在src
目录中文件组织如下:
src
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
└── main.ts //应用程序入口文件
复制代码
main.ts
内容如下:
/* main.ts */
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
复制代码
可以看到主要就是使用NestFactory
创建了一个应用实例,监听了3000端口。