文章说明:此文仅记录博主初次学习nestjs之后的认识和理解,经验尚浅,文章内容仅供参考,如若有误,敬请指正。
阅读建议:搭配文章的侧边栏目录进行食用,体验会更佳哦!
一:多模块应用结构
0.多模块目录结构示例
src
├──cats
│ ├──dto
│ │ └──create-cat.dto.ts
│ ├──interfaces
│ │ └──cat.interface.ts
│ ├─cats.service.ts
│ ├─cats.controller.ts
│ └──cats.module.ts
├──dogs
│ ├──dto
│ │ └──create-dog.dto.ts
│ ├──interfaces
│ │ └──dog.interface.ts
│ ├─dogs.service.ts
│ ├─dogs.controller.ts
│ └──dogs.module.ts
├──app.module.ts
└──main.ts
1.模块划分和定义
模块划分
- 划分原则:后端通常对一个业务对象的增删改查等操作的完整业务流程(三层)进行统一管理,在nestjs中,统一管理不仅仅是将相关的所有资源放入同一个文件夹,而且引入了模块作为管理单位。
- nestjs模块主要内容:模块管理文件、三层架构实现文件(controller、service、dao)、相关pojo类文件(dto、vo、bo、do)
普通 / 全局模块案例
- 模块管理文件
import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
// @Global()// 全局模块,应该只注册一次,最好由根或核心模块注册。
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService],
})
export class CatsModule {}
2.模块整合
- 根模块(app.module.ts)
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';
import { DogsModule } from './dogts/dogs.module';
@Module({
imports: [CatsModule , DogsModule],
})
export class ApplicationModule {}
3.模块通信
模块数据的私有与暴露
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService]// 模块暴露:引入CatsModule模块的其它模块可以访问CatsService。模块私有:模块未暴露的即为私有
})
export class CatsModule {}
模块之间的引入与导出
// 在核心模块下引入并导出公共模块
@Module({
imports: [CommonModule],
exports: [CommonModule],
})
export class CoreModule {}
二:控制器(controller)
控制器的职责:
- 接收请求并获取请求数据
- 传递请求数据进行业务调用
- 根据业务调用结果构造返回响应
控制器的配置:根模块(app.module.ts)中配置控制器 controllers: [AppController],
0.单线程处理
Node.js
不遵循请求/响应多线程无状态模型,每个请求都由主线程处理。- 当请求完整周期内涉及到或可能涉及到异步操作时,可使用 async / await进行异步处理
1.接收请求并获取请求数据
匹配请求
- 匹配处理:装饰器建立请求与controller的映射。
- 匹配规则:请求前缀prefix(class前缀、method前缀,支持通配符*) 、请求方法 GET/POST/…
- 动态url匹配
- 子域路由:根据请求的host来判断接口的调用权限
访问request
请求信息 | 装饰器方式访问 | 普通对象访问 |
---|---|---|
请求 | @Request() | req |
请求session | @Session() | req.session |
请求参数 | @Param(key?: string) | req.params/ req.params[key] |
请求参数 | @Body(key?: string) | req.body/ req.body[key] |
请求参数 | @Query(key?: string) | req.query/ req.query[key] |
请求头 | @Headers(name?: string) | req.headers/ req.headers[name] |
请求IP | @Ip() | req.ip |
- 案例
import { Controller, Get ,Req, Ip, Post,Query,Param,Body} from '@nestjs/common';
import { AppService } from './app.service';
import { Request, query } from 'express';
// import {CreateCatDto} from './DTO/catDto'
class CreateCatDto {
readonly name: string;
readonly age: number;
readonly breed: string;
}
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('test')
getHello(@Req() request: Request, @Query('name') name:String,@Ip() ip:String): string {
console.log(`请求参数name为(普通对象): ${request.query['name']}`)
console.log(`请求参数name为(装饰器): ${name}`)
console.log(`ip地址为(普通对象): ${request.ip}`)
console.log(`ip地址为(装饰器): ${ip}`)
return this.appService.getHello();
}
@Get(':id')
findOne(@Param('id') id: string) {
return `This action returns a #${id} cat`;
}
@Post()
async create(@Body() createCatDto: CreateCatDto) {// POST请求参数获取,@Body()配合DTO类
return 'This action adds a new cat';
}
}
2.传递请求数据进行业务调用
- 引入业务类:import { AppService } from ‘./app.service’;
- 调用业务类的方法: return this.appService.getHello();
3.根据业务调用结果构造返回响应
访问response
响应信息 | 装饰器方式访问 | 普通对象访问 |
---|---|---|
响应 | @Response() @Res()* | res |
构造状态行
- 状态码:@HttpCode(204)
构造响应头
- 响应头:@Header(‘Cache-Control’, ‘none’)
构造响应体
- 返回页面/基本类型
- 返回对象/数组:自动序列化为JSON
重定向
- 基本语法:@Redirect(‘https://nestjs.com’)
- 动态url:controller方法 return { url: ‘https://docs.nestjs.com/v5/’ };
二:控制反转IOC
- 基本理解:可以看作为工厂模式的升华,能够解开对象引用与对象创建之间的耦合。
- 实现方式:通过在全局维护一个IOC容器,来统一管理对象的配置和创建(依赖注入)。
1.IOC创建对象案例
1):定义一个provider类(cats.service.ts)
普通provider,注入时等同于 new CatsService(httpClient)
import { Injectable } from '@nestjs/common';
@Injectable()
export class CatsService {
constructor(
private readonly httpClient: HttpClientClass
) {}
test(): String {
return 'test provider1';
}
}
泛型Provider,注入时等同于 new DogsService< HttpClientClass>(httpClient)
- 基于构造函数的注入
import { Injectable } from '@nestjs/common';
// 2.多级provider = new DogsService(HttpClientClass1)
@Injectable()
export class DogsService<T>{
constructor(
//@Optional() 可选注入
@Optional() @Inject('HTTP_OPTIONS') private readonly httpClient: T
) {}
test(): String {
return 'test provider2';
}
}
- 基于属性的注入
import { Injectable } from '@nestjs/common';
@Injectable()
export class DogsService<T>{
@Inject('HTTP_OPTIONS')
private readonly httpClient: T;
test(): String {
return 'test provider2';
}
}
2):将该provider类注册至IOC容器中(app.module.ts)
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class AppModule {}
3):从IOC容器中获取该provider类对象(cats.controller.ts)
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CatsService } from './cats.service';
@Controller('cats')
export class CatsController {
// provider可通过`constructor` **注入**依赖关系
constructor(private catsService: CatsService) {}
@Post()
async test() {
this.catsService.test();
}
}
四:切面Middleware(常用于请求预处理、响应后处理)
1.作为切面
1):切面类型:NestMiddleware接口实现类
2):切面输入
- Request对象
- Response对象
- next函数
3):切面输出:next进入下一个切面
4):切面使用场景
- 执行任何代码。
- 对请求和响应对象进行更改。
- 结束请求-响应周期。
- 调用堆栈中的下一个中间件函数。
- 如果当前的中间件函数没有结束请求-响应周期, 它必须调用 next() 将控制传递给下一个中间件函数。否则, 请求将被挂起。
2.简单案例
1):中间件定义
- 方式1:类中间件
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
// 中间件定义:必须是NestMiddleware接口的实现类并且实现use方法,参数为请求、响应、转发到下一个中间件的方法
use(req: Request, res: Response, next: Function) {
console.log('Request...');
next();
}
}
- 方式2:函数式中间件
export function logger(req, res, next) {
console.log(`Request...`);
next();
}
2):中间件注册
- 模块注册(app.module.ts)
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';
import { CatsController } from './cats/cats.controller.ts';
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
// 中间件注册:不在@module中配置,而是在NestModule接口的configure方法中注册一个中间件消费者(MiddlewareConsumer)实例,在该实例中配置该中间件消费者的消费特征(应用哪个中间件、处理哪些请求、排除哪些请求)。
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)// 可配置多个中间件
.exclude(
{ path: 'cats', method: RequestMethod.GET },
{ path: 'cats', method: RequestMethod.POST },
'cats/(.*)',
)
// forRoutes配置经过中间件的请求有哪些,可接受一个字符串、多个字符串、对象、一个控制器类甚至多个控制器类,传对象时path属性支持通配符*。
.forRoutes(CatsController);
}
}
- 全局注册(main.ts)
const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);
五:切面Guard(常用于权限管理)
1.作为切面
此文暂不对细节展开探讨
1):切面类型:CanActivate接口实现类
2):切面输入
ExecutionContext对象:执行上下文对象,继承至参数主机对象ArgumentsHost
此文关注点不在ExecutionContext对象,在此不做细节探讨。
export interface ExecutionContext extends ArgumentsHost {
getClass<T = any>(): Type<T>;
getHandler(): Function;
}
3):切面输出:继续请求 or 返回响应
- return boolean | Promise | Observable
4):切面使用场景
- 权限管理
2.权限管理案例
权限管理可通过守卫技术实现,守卫之名顾名思义,可负责鉴权,符合权限的请求通过,不符合的直接响应。
1):给控制器绑定权限(给门贴上通过规则)
- 自定义元数据装饰器
// 自定义权限装饰器,@SetMetadata()装饰器拥有将定制元数据附加到路由处理程序的能力,可用于控制器绑定权限以便读取
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
- 使用元数据装饰器
@Post()
// @SetMetadata('roles', ['admin']) // 方式1:使用@SetMetadata装饰器
@Roles('admin') // 方式2:使用自定义装饰器封装@SetMetadata装饰器
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
2):给控制器绑定守卫者(给门分配站岗门卫)
- 定义守卫者
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@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);
}
}
- 绑定守卫者
// 方式1:方法守卫(controller上)
@UseGuards(new RolesGuard())
// 方式2:全局守卫(app.module.ts)
@Module({
providers: [
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
3):守卫者鉴权失败(被门卫叉刀拦下)
- 默认响应
{
"statusCode": 403,
"message": "Forbidden resource"
}
- 自定义响应
// 守卫者抛出异常
throw new UnauthorizedException();
六:切面Interceptor
1.作为切面
1):切面类型:NestInterceptor接口实现类
2):切面输入
ExecutionContext对象:执行上下文对象,继承至参数主机对象ArgumentsHost
next函数
3):切面输出:next进入下一个切面(可携带后拦截器逻辑)
4):切面使用场景
- 在函数执行之前/之后绑定额外的逻辑
- 转换从函数返回的结果
- 转换从函数抛出的异常
- 扩展基本函数行为
- 根据所选条件完全重写函数 (例如, 缓存目的)
2.拦截器简单案例
1):拦截器定义
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// return之前:前拦截器逻辑
console.log('Before...');
const now = Date.now();
return next
.handle()// handle之后:后拦截器逻辑
.pipe(
// catchError(err => throwError(new BadGatewayException())),// 覆盖异常,在异常过滤器切面之前
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}
2):拦截器绑定
- 绑定到方法
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
- 绑定到全局(app.module.ts)
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}
七:切面Pipe(常用于数据校验、数据转换)
1.作为切面
1):切面类型:PipeTransform接口实现类
2):切面输入
value:流入管道的值
ArgumentMetadata:该value的元数据信息
export interface ArgumentMetadata {
type: 'body' | 'query' | 'param' | 'custom';
metatype?: Type<unknown>;
data?: string;
}
3):切面输出:管道流出的值 or 抛出异常返回响应
4):切面使用场景
- 数据校验
- 数据转换
2.数据校验案例(类验证器方式)
输入数据通过验证管道验证,成功返回原数据,失败则请求结束返回数据校验异常响应。
1):自动校验工具(声明式校验)
- 安装工具
npm i --save class-validator class-transformer
- 验证管道(通常内置管道ValidationPipe即可满足需求,当然也可使用自定义管道)
2):校验规则声明
import { IsString, IsInt } from 'class-validator';
// CreateCatDto 实例的校验规则
export class CreateCatDto {
@IsString()
name: string;
@IsInt()
age: number;
@IsString()
breed: string;
}
3):数据绑定校验管道
- 参数级别绑定
@Post()
async create(@Body(new ValidationPipe()) createCatDto: CreateCatDto类中) {// 使用内置验证管道ValidationPipe验证数据createCatDto,规则定义在createCatDto对象的类定义中
this.catsService.create(createCatDto);
}
- 方法级别绑定
@Post()
@UsePipes(ValidationPipe)// provider依赖注入
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
- 全局级别绑定(main.ts)
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
3.数据转换案例
1):管道函数
- 使用内置转换管道
- 自定义转换管道
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
// 此管道ParseIntPipe已经内置
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('Validation failed');
}
return val;
}
}
2):数据绑定转换管道
@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
return await this.catsService.findOne(id);
}
八:切面ExceptionFilter(常用于全局异常处理)
1.作为切面
1):切面类型:ExceptionFilter接口实现类
2):切面输入
HttpException异常对象
ArgumentsHost参数主机对象
3):切面输出:构造异常响应
4):切面使用场景
- 异常处理
2.异常处理
内置的异常层负责处理整个应用程序中的所有抛出的异常。当捕获到未处理的异常时,最终用户将收到友好的响应。
1):不处理
- 全局捕获后返回的响应结构
{
"statusCode": 500,
"message": "Internal server error"
}
2):一般需求处理
- 手动抛出
// 方式1:直接抛出HttpException,异常信息为forbidden
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
// 方式2:直接抛出HttpException,异常信息为object
throw new HttpException({
status: HttpStatus.FORBIDDEN,
error: 'This is a custom message',
}, HttpStatus.FORBIDDEN);
// 方式3:抛出HttpException子类(内置异常/自定义异常)
throw new UnAuthenticationException();
// 自定义异常类UnAuthenticationException
// export class UnAuthenticationException extends HttpException {
// constructor() {
// super('not authentication', 30000);
// }
// }
- 全局捕获后返回的响应结构
// 方式1
{
"statusCode": 403,
"message": "Forbidden"
}
// 方式2
{
"status": 403,
"error": "This is a custom message"
}
// 方式3
{
"statusCode": 30000,
"message": "not authentication"
}
3):复杂需求处理
在异常捕获到具体响应之前,如果有需要添加aop切面处理的需求(如日志记录等),可以通过添加异常过滤器的形式实现。
手动抛出(此切面不关注)
异常过滤器捕获
- 异常过滤器定义
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)// @Catch()会捕获不经手动抛出的异常(即第一种不作处理的异常)
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(HttpExceptionFilter) // 通过@UseFilters这个装饰器,使被装饰的方法绑定HttpExceptionFilter这个异常过滤器
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
// 另一种:也可绑定为全局/模块异常过滤器
全局捕获后返回的响应结构
{
statusCode: 'status:xxx',
timestamp: 'date:xxx',
path: 'url:xxx',
}