NestJS 核心概念

2023 年 8 月 10 日 星期四(已编辑)
/
854
6
这篇文章上次修改于 2024 年 8 月 11 日 星期日,可能部分内容已经不适用,如有疑问可询问作者。

NestJS 核心概念

主要讲下 NestJS 中 IOC 和 五种 AOP 的相关知识点,即 Middleware, Guard, Pipe, Interceptor, ExceptionFilter 的使用和概念

Provider

可以先创建下项目

nest new xxx

providers 是可以注入的对象,我们可以把带有 @Injectable()的 class 放到 Module 的 providers 里声明,因为 Nest 实现了 IOC,这样就会被它给识别到,从而实现依赖注入。

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

当然这是一种简写,原本是这样:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    {
      provide: AppService,
      useClass: AppService,
    },
  ],
})
export class AppModule {}

这样就实现了依赖注入,我们就可以通过 @Inject() 在其他地方使用了,比如在 Controller 里使用

import { Controller, Get, Inject } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(@Inject(AppService) private readonly appService: AppService) {}
  //或者直接写,不用构造器
  // @Inject(AppService) private readonly appService: AppService

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

但因为我们的 provide 和 useClass 是相同的,所以可以省略,所以可以简写成如下:

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}
  
  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

我们可以实验一下,把 provide 的值改为一个字符串

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    {
      provide: 'suemor',
      useClass: AppService,
    },
  ],
})
export class AppModule {}

这样就没办法省略了,就得老老实实写 @Inject()

import { Controller, Get, Inject } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(@Inject('suemor') private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

除了在 providers 里指定 class,我们也可以指定值或动态的对象,分别对应 useValueuseFactory,如下为 useFactory写法

import { Inject, Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    {
      provide: 'suemor',
      useClass: AppService,
    },
    {
      provide: 'suemor2',
      useFactory(appService: AppService) {
        return {
          age: 19,
          gender: 'male',
          say: appService.getHello(),
        };
      },
      inject: ['suemor'],
    },
  ],
})
export class AppModule {}

修改 controller

import { Controller, Get, Inject } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(
    @Inject('suemor') private readonly appService: AppService,
    @Inject('suemor2')
    private readonly user: { age: number; gender: string; say: string },
  ) {}

  @Get()
  getHello() {
    return this.user;
  }
}
image-20230531005247226

image-20230531005247226

五种 AOP

​ 因为 NestJS 使用的 MVC 架构,所以有 AOP 的能力,其中 Nest 的实现主要包括如下五种(按执行顺序排列

  • Middleware
  • Guard
  • Interceptor
  • Pipe
  • ExceptionFilte

Middleware

即中间件,但这并不是 Nest 独有的,你用 Express 或者 Fastify 当请求的库他们本身也都拥有 middleware,概念基本相同,我们一般会用它处理些通用逻辑(如日志)。

nest g middleware logger --no-spec --flat

应该会生成如下代码,这里的 req 和 res 都是 any 是因为 Nest 并不知道你用的是 Express 还是 Fastify 当请求库,所以类型得自行补全。

import { Injectable, NestMiddleware } from '@nestjs/common';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: any, res: any, next: () => void) {
    next();
  }
}

修改类型,并添加下 console.log,然后我们得去注册这个 middleware

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: () => void) {
    console.log('start');
    next();
  }
}

app.module.ts里全局注册

import { MiddlewareConsumer, Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { NestModule } from '@nestjs/common';
import { LoggerMiddleware } from './logger.middleware';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    {
      provide: 'suemor',
      useClass: AppService,
    },
    {
      provide: 'suemor2',
      useFactory(appService: AppService) {
        return {
          age: 19,
          gender: 'male',
          say: appService.getHello(),
        };
      },
      inject: ['suemor'],
    },
  ],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes('*');
  }
}

然后去浏览器访问,我们可以看到控制台正确输出了

image-20230531204334476

image-20230531204334476

Guard

即路由守卫的意思,一般在 Controller 之前鉴权,返回 true 和 false

nest g guard roles --no-spec --flat
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class RolesGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    return true;
  }
}

修改 Controller,UseGuards 和 SetMetadata。

import {
  Controller,
  Get,
  Inject,
  SetMetadata,
  UseGuards,
} from '@nestjs/common';
import { AppService } from './app.service';
import { RolesGuard } from './roles.guard';

@Controller()
export class AppController {
  constructor(
    @Inject('suemor') private readonly appService: AppService,
    @Inject('suemor2')
    private readonly user: { age: number; gender: string; say: string },
  ) {}

  @Get()
  @UseGuards(RolesGuard)
  @SetMetadata('roles', ['admin'])
  getHello() {
    return this.appService.getHello();
  }
}

修改 RolesGuard,这里我们会引 Reflector 的 Metadata 概念,这目前还没有被 ES 标准化,还处于草案阶段,Nest 应该是使用 reflect-metadata 这个 polyfill 包,这可以说是 Nest 的核心了,它的 IOC 基本都是靠这个实现的,这里大意就是从 ExecutationContext 取到 handle,然后注入 reflector,通过 reflector.get 取出 handler 上的 metadata,这样就可以获取到当前路由的权限了。

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    console.log(this.reflector.get<string[]>('roles', context.getHandler())); //    [ 'admin' ]
    return true;
  }
}

我们现在可以获得当前路由的权限了,那接下来就要用这个与请求的权限进行比较了,关于解析请求的权限要用到 @nestjs/passport库,就是通过 token 获取到当前的 user,但这不是本文重点,我们假设已经在 req 赋上 user 字段了,就可以通过 context.switchToHttp().getRequest().user 获取到当前的请求的 user 了,然后进行 if 判断下就可以了。

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const routerRole = this.reflector.get<string[]>(
      'roles',
      context.getHandler(),
    );
    const ReqUser = context.switchToHttp().getRequest().user;
    console.log(routerRole, ReqUser);
    if (routerRole[0] == ReqUser.role) {
      return true;
    }
    return false;
  }
}
image-20230531220921807

image-20230531220921807

Interceptor

即拦截器,它可以在 Controller 方法前后加入一些逻辑,其实和 Middleware 挺像的,也可以用来记录日志等,不过它也可以在 Controller 之后进行拦截,比如统一返回结果之类的。

nest g interceptor logging --flat --no-spec

它还有些优势就是可以使用 rxjs 的各种 operator,且也可以使用 reflector 和 ExecutionContext。

import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('before');
    return next.handle().pipe(tap(() => console.log('after')));
  }
}

然后去 app 注册

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { LoggingInterceptor } from './logging.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalInterceptors(new LoggingInterceptor());
  await app.listen(3000);
}
bootstrap();

Pipe

即管道,可以对参数进行校验,比如我们说 age 不能是字符串,就可以这样写。

import { Controller, Get, Inject, ParseIntPipe, Query } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(@Inject('suemor') private readonly appService: AppService) {}

  @Get()
  getHello2(@Query('age', ParseIntPipe) age: number) {
    return age;
  }
}

但实际业务我们一般会用 class-validator 这个库,它就是利用的 Pipe。

ExceptionFilter

即异常过滤器,它是很强大的,除了 middleware 以外,只要出现异常,都可以被它给捕获到。

import { ArgumentsHost, ExceptionFilter } from '@nestjs/common';
import { Catch, 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();
    const status = exception.getStatus();
    
    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}

去 app 注册

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './any-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
bootstrap();

我们 throw 个异常

import { BadRequestException, Controller, Get, Inject } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(@Inject('suemor') private readonly appService: AppService) {}

  @Get()
  getHello2() {
    throw new BadRequestException();
    return this.appService.getHello();
  }
}

被正确捕获

image-20230531225850021

image-20230531225850021

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...