Nest.js 学习笔记

  • Post author:
  • Post category:其他




什么是nestjs?


NestJs 是一款用于构建高效且可伸缩 Web 应用程序的渐进式 Node.js 框架。看下官方给的简介,NestJs模块化的体系结构允许开发者使用任何其他的库,从而提供灵活性;为 Nodejs 提供一个适应性强大的生态系统;利用最新的js特性,为nodejs 提供更加方便的设计模式和成熟的解决方案。



核心组件

NestJs 主要有 8 个组件(Controller 控制器、Component 组件、Module 模块、Middlewares 中间件、Exception Filters 异常过滤器、Pipes 管道、Guards 守卫、Interceptors 拦截器),主要通过 Controller、Component、Module 三个最核心的组件构成。

  1. 其中 Controller 是传统意义的控制器,工程启动时,控制台可以清晰的看到应用的路由配置信息,Provider 一般做 Service,比如数据的 CRUD 都可以封装在 Service 中,每一个 Service 就是一个 Provider,也是主要的业务代码逻辑部分,Module 表示应用或者模块,一个 Module 可以拥有多个 Controller。
  2. Controller

    控制器层负责处理传入的请求,并返回对客户端的响应,这部分和传统的控制器没有差异

    NestJs 中通过 @Controller() 装饰器来创建控制器,可以通过 @Get()、@Post 等对类方法进行修饰,说明请求方式,以此实现 NestJs 路由的去中心化。

    NestJs 支持 asnyc/await,极大的方便开发者,同时,NestJs 为开发者提供了丰富的修饰器方法用于解析及处理 http 请求,使得开发者更快更便捷的构建自己的控制器。
import {
Body,
Controller,
Get,
Param,
Post,
} from '@nestjs/common';
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';
import { Cat } from './interfaces/cat.interface';
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
@Get(':id')
findOne(
@Param('id', new ParseIntPipe())
id,
) {
// logic
}
}
  1. Provider

    几乎所有的东西都可以被认为是 Provider(service, repository, factory, helper 等等)。他们都可以注入依赖关系 constructor,也就是说,他们可以创建各种关系。但事实上,提供者不过是一个用 @Injectable() 装饰器注解的简单类。

    在 NestJs 中,经常使用依赖注入,当我们设计好一个 Provider 之后,就可以通过 Controller 类构造函数注入。
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
create(cat: Cat) {
this.cats.push(cat);
}
findAll(): Cat[] {
return this.cats;
}
}
  1. Modules

    模块是具有 @Module() 装饰器的类。 @Module() 装饰器提供了元数据,NestJs 用它来组织应用程序结构。当我们定义编写完某块业务逻辑的 Controller 和 Provider 之后,我们就可以通过 @Module() 将对应模块的逻辑组织起来,NestJs 同时提供模块导出、动态模块、共享模块的支持,可以很方便的解决业务场景中常见的单例等问题。
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
  1. Middleware

    NestJs 的中间件对 express 的中间件进行了一层封装。中间件在路由处理器之前被调用。 中间件可以访问请求和响应对象,以及应用程序请求响应周期中的下一个中间件功能。下一个中间件函数通常由名为 next 的变量表示。与 express 不同的是,NestJs 的中间件在使用时,需要通过实现 NestModule 的 configure 方法来引入,中间件可以这对单独的路由或者控制器进行处理。
mport { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middlewares/logger.middleware';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})
export class ApplicationModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('/cats’); // ‘/cats’ 变更为 CatsController 则是对控制器进行中间件配置
}
}

与此同时,NestJs 同样也支持函数式的中间件、异步中间件。

// 异步中间件
import { Injectable, NestMiddleware, MiddlewareFunction } from '@nestjs/common';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
async resolve(name: string): Promise<MiddlewareFunction> {
await someAsyncJob();
return async (req, res, next) => {
await someAsyncJob();
console.log(`[${name}] Request...`); // [ApplicationModule] Request...
next();
};
}
}
// 函数式中间件
export function logger(req, res, next) {
console.log(`Request...`);
next();
};

全局中间件配置,需要在 NestFactory 创建的实例上进行配置,这里配置方法和 express 类似

const app = await NestFactory.create(ApplicationModule);
app.use(logger);
await app.listen(3000);
  1. Exception Filters

    健壮的应用应该有完善的异常处理逻辑,并且有针对的将异常划分为不同的层次,未捕获到异常时,需要给用户良好的响应,NestJs 提供 HttpException 来处理 http 请求的异常。

    NestJs 通过 @ UseFilters() 设置需要使用的过滤器,通过 @Catch() 来捕获异常抛给对应的异常过滤器处理。
// 设置异常过滤器
@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
// 捕获异常,并给异常过滤器处理
import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';
import { HttpException } from '@nestjs/common';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
response
.status(status)
.json({
statusCode: exception.getStatus(),
timestamp: new Date().toISOString(),
path: request.url,
});
}
}

NestJs 支持开发者自定义异常、设置异常的处理顺序、设置全局异常处理等。

import { HttpExceptionFilter } from './exceptions/http-exception.filter';
import { AnyExceptionFilter } from './exceptions/any-exception.filter’;
const app = await NestFactory.create(ApplicationModule);
// 异常捕获顺序根据 useGlobalFilters 顺序决定
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalFilters(new AnyExceptionFilter());
await app.listen(3000);
  1. Pipe

    管道是具有 @Injectable() 装饰器的类。管道应实现 PipeTransform 接口。

    管道将输入数据转换为所需的输出,比如请求数据格式化、请求数据校验抛异常处理等等。
// create-cat.dto.ts
import { IsString, IsInt } from 'class-validator';
export class CreateCatDto {
@IsString()
readonly name: string;
@IsInt()
readonly age: number;
@IsString()
readonly breed: string;
}
// validation.pipe.ts
import { PipeTransform, Pipe, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value, metadata: ArgumentMetadata) {
const { metatype } = metadata;
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToClass(metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
throw new BadRequestException('Validation failed');
}
return value;
}
private toValidate(metatype): boolean {
const types = [String, Boolean, Number, Array, Object];
return !types.find((type) => metatype === type);
}
}
// cats.controler.ts
@Post()
@UsePipes(new ValidationPipe())
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}

同样地,管道也可以设置对全局路由进行处理(@useGlobalPipes)。

  1. Guard

    守卫有一个单独的责任。它确定请求是否应该由路由处理程序处理。到目前为止, 访问限制逻辑大多在中间件内。这样很好, 因为诸如 token 验证或将 req 对象附加属性与特定路由没有强关联。但中间件是非常笨拙的。它不知道调用 next() 函数后应该执行哪个处理程序。另一方面, 守卫可以访问 ExecutionContext 对象, 所以我们确切知道将要评估什么。


注:守卫是在每个中间件之后执行的, 但在管道之前。

守卫是一个使用 @Injectable() 装饰器的类。 守卫需要实现 canActivate 方法。

// 角色装饰器 roles.decorator.ts
import { ReflectMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => ReflectMetadata('roles', roles);
// 用角色装饰器修饰路由 cats.controller.ts
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
// 基于权限判断的守卫: roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Reflector } from '@nestjs/core';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly 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;
const hasRole = () => user.roles.some((role) => roles.includes(role));
return user && user.roles && hasRole();
}
}

从这里可以看出,首先拿到执行上下文的关键就是这里的反射器,通过反射拿到待执行处理的路由访问权限信息,判断该请求是否具备访问改路由的权限,从而很好的将权限校验的逻辑从业务逻辑中分离出来。

  1. Interceptor

    拦截器是 @Injectable() 装饰器注解的类。拦截器应该实现 NestInterceptor 接口。 拦截器具有一系列有用的功能,这些功能受面向切面编程(AOP)技术的启发。它们可以:

在函数执行之前/之后绑定额外的逻辑 转换从函数返回的结果 转换从函数抛出的异常 根据所选条件完全重写函数 (例如, 缓存目的)

NestJs 的拦截器借助于 rxjs 强大功能来实现。

// 请求/响应日志记录 logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
call$: Observable<any>,
): Observable<any> {
console.log('Before...');
const now = Date.now();
return call$.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}
// UseInterceptors来用装饰控制器
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
  1. 响应顺序

    综上所述,NestJs 各部件调用顺序可以整理如下:

    在这里插入图片描述

  2. 总结:

    根据上述的各部件功能拆分,NestJs 工程组织结构大致可以形成如下的组织结构:

├── jest.json

├── node_modules

├── package-lock.json

├── package.json

├── src

│ ├── app.module.ts // 模块聚合

│ ├── cats // 业务模块

│ │ ├── cats.controller.spec.ts

│ │ ├── cats.controller.ts

│ │ ├── cats.module.ts

│ │ ├── cats.service.ts

│ │ ├── dto

│ │ └── interfaces

│ ├── common

│ │ ├── decorators // 自定义装饰器等等

│ │ ├── filters // 过滤器:异常处理等等

│ │ ├── guards // 守卫:权限校验等等

│ │ ├── interceptors // 拦截器:AOP处理

│ │ ├── middlewares // 中间件处理

│ │ └── pipes // 管道处理,DTO数据处理等等

│ └── main.ts // 主入口

├── tsconfig.json

├── tslint.json

└── yarn.lock



版权声明:本文为weixin_41772346原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。