Nest.js 框架入门

Nest 提供了一个开箱即用的应用程序架构,支持Typescript,和 Angular 架构十分相似,基本模式如下:

src
├── app.controller.ts
├── app.module.ts
└── main.ts
// main.ts
import { NestFactory } from '@nestjs/core';
import { ApplicationModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(ApplicationModule);
  await app.listen(3000);
}
bootstrap();

然后根模块里导入各种功能模块。

1、控制器

控制器可理解为路由,专门负责处理各种HTTP请求(GET,POST等)

1.1、基本用法

使用 Controller 装饰器定义一个控制器(可接受字符串作为路径),配合 Get,Post 等装饰器形成路由:

import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'test';
  }
}
// 访问,output:test
curl -X GET localhost:3000/cats

Get等方法装饰器也可以接受参数:

import { Controller, Get } from '@nestjs/common';

@Controller('v1')
export class CatsController {
  @Get('cats')
  findAll(): string {
    return 'test';
  }
}
// 访问,output:test
curl -X GET localhost:3000/v1/cats

注意:这里的 findAll 的名称可以是任意的,从访问地址中也可看出。也不能用箭头函数(装饰器不支持),最后一点就是 controller 必须依附在 module 里:

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

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

1.2、路由相关

1、获取参数

import { Controller, Get, Req,Res } from '@nestjs/common';
import { Request,Response } from 'express';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(@Req() request: Request,@Res() response:Response): string {
    return 'This action returns all cats';
  }
}

还有诸如: @Body,@Param,@Query,@Headers. @Put,@Delete,@Patch,@Options,@Head,@All

2、动态参数 & 路由注册顺序(路由参数都是可选的)

@Get(':id')
findOne(@Param() params): string {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}
// GET /cats 请求不会命中第二个处理程序,因为所有路由参数都是可选的,应该调换位置
@Controller('cats')
export class CatsController {
  @Get(':id')
  findOne(@Param('id') id: string) {
    return `This action returns a #${id} cat`;
  }

  @Get()
  findAll() {
    // This endpoint will never get called
    // because the "/cats" request is going
    // to be captured by the "/cats/:id" route handler
  }
}

3、异步接口 Async / await

// Promise
@Get()
async findAll(): Promise<any[]> {
  return [];
}
// Observable
@Get()
findAll(): Observable<any[]> {
  return of([]);
}

4、请求负载 DTO
这里使用类而不是接口来定义 DTO,因为接口在TS编译时会被删除,而类不会。这有助于其他特性的使用(管道等特性能够在访问变量的元类型时提供更多的可能性)

export class CreateCatDto {
  readonly name: string;
  readonly age: number;
  readonly breed: string;
}

@Post()
async create(@Body() createCatDto: CreateCatDto) {
  return 'This action adds a new cat';
}

2、提供者

控制器在处理 HTTP 请求时,应该将更复杂的任务委托给提供者。service、helper等都可以称为提供者。

2.1、定义

使用 @Injectable 装饰器定义一个提供者

// cat.service.ts
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;
  }
}

2.2、注册该提供者

和控制器一样,提供者也需要依附于module中才能使用

// 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 ApplicationModule {}

2.3、调用该提供者

import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
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();
  }
}

3、模块

Nest 用它来组织应用程序结构,每个 Nest 应用程序至少有一个模块,即根模块。然后根模块包含各种功能模块,从而组织起应用程序的结构

3.1、功能模块

CatsController 和 CatsService 属于同一个应用程序域。 应该考虑将它们移动到一个功能模块下,即 CatsModule:

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
 controllers: [CatsController],
 providers: [CatsService],
 exports: [CatService]  // 每个导入CatsModule的模块都可以访问 CatsService
})
export class CatsModule {}

// app.module.ts 中引入即可
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';

@Module({
 imports: [CatsModule],
})
export class ApplicationModule {}

3.2、全局模块

由于Nest 将提供者封装在模块范围,故想使用一些诸如 Helper 之类很通用的提供者时必须导入封装它们的模块。这有时候很不方便,故可以使用全局模块:

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 {}

@Global 装饰器使模块成为全局作用域,然后 CatsService 提供者可以在任意地方注入

3.3、动态模块

动态模块扩展了模块元数据。当您需要动态注册组件时,这个实质特性非常有用。

// database.module.ts
import { Module, DynamicModule } from '@nestjs/common';
import { createDatabaseProviders } from './database.providers';
import { Connection } from './connection.provider';

@Module({
  providers: [Connection],
})
export class DatabaseModule {
  static forRoot(entities = [], options?): DynamicModule {
    const providers = createDatabaseProviders(options, entities);
    return {
      module: DatabaseModule,
      providers: providers,
      exports: providers,
    };
  }
}

// app.module.ts
import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { User } from './users/entities/user.entity';

@Module({
  imports: [DatabaseModule.forRoot([User])],
  exports: [DatabaseModule],
})
export class ApplicationModule {}

4、中间件

中间件是一个在路由处理器之前被调用的函数,Nest中有两种写法:

4.1、@Injectable 中间件

必须实现 NestMiddleware 类的 use 方法

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

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: Function) {
    console.log('Request...');
    next();
  }
}
//2、使用
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class ApplicationModule implements NestModule {  // 必须实现 NestModule 的 configure 方法
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .exclude(
        { path: 'cats', method: RequestMethod.GET },
        { path: 'cats', method: RequestMethod.POST }
      )
      .forRoutes(CatsController);
  }
}

4.2、纯函数中间件

中间件没有任何依赖关系时,我们可以考虑使用函数式中间件

// 1、定义
export function logger(req, res, next) {
  console.log(`Request...`);
  next();
};
// 2、使用(同上)

4.3、多中间件 & 全局中间件

// 多个中间件
consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);
// 全局中间件
const app = await NestFactory.create(ApplicationModule);
app.use(logger);
await app.listen(3000);

5、异常过滤器、管道、守卫、拦截器

5.1、异常过滤器

Nest 内置的异常层会处理应用程序中抛出的所有异常,若未捕获到,则返回500错误,能保证用户将收到友好的响应。

@Get()
async findAll() {
  throw new HttpException({
    status: HttpStatus.FORBIDDEN,
    error: 'This is a custom message',
  }, 403);
}

也就是说,我们只需要负责抛出异常即可,异常种类很多,Nest 内置了一些:
UnauthorizedException、NotFoundException、RequestTimeoutException、NotImplementedException等等。Nest还提供了自定义异常过滤器以及设置过滤器的作用范围(方法范围的,控制器范围的,也可以是全局范围的)

5.2、管道

管道的两个作用:1、数据验证;2、转换数据
语法上,管道是具有 @Injectable() 装饰器的类。管道应实现 PipeTransform 接口的 transform 方法

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

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    return value;
  }
}

Nest 内置了 ValidationPipe和ParseIntPipe,使用ValidationPipe 时,需要同时安装:

npm i --save class-validator class-transformer

使用步骤:
1、在 dto 文件中 加入验证装饰器

import { IsString, IsInt } from 'class-validator';

export class CreateCatDto {
  @IsString()
  readonly name: string;
  @IsInt()
  readonly age: number;
  @IsString()
  readonly breed: string;
}

2、建立 ValidationPipe 类

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);
    }
}

3、应用 ValidationPipe

// 方法级别范围
@Post()
@UsePipes(ValidationPipe)
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}
// 全局级别范围
async function bootstrap() {
  const app = await NestFactory.create(ApplicationModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();
// 如果想做依赖注入
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_PIPE,
      useClass: CustomGlobalPipe,
    },
  ],
})
export class ApplicationModule {}

5.3、守卫

1、守卫有一个单独的责任。它们确定请求是否应该由路由处理程序处理,守卫在每个中间件之后执行的,但在拦截器和管道之前。
2、语法上,守卫是一个使用 @Injectable() 装饰器的类。 守卫应该实现 CanActivate 接口的canActivate方法。

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

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);
  }
}

3、角色认证
守卫和管道、异常过滤器一样,都可以是方法级别、全局级别,也可以做依赖注入

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.getClass()); // 通过反射获取元数据
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    const hasRole = () => user.roles.some((role) => roles.includes(role)); // 从user.roles中判断该用户是否拥有该权限,从而实现角色认证
    return user && user.roles && hasRole();
  }
}

5.4、拦截器

一条请求会经历:中间件=>守卫=>拦截器=>请求开始=====请求完成=>拦截器=>中间件,从拦截器作用位置可以看出拦截器的功能:
1、在函数执行之前/之后绑定额外的逻辑
2、转换从函数返回的结果
3、转换从函数抛出的异常
4、根据所选条件完全重写函数 (例如, 缓存目的)
语法上,拦截器是一个使用 @Injectable() 装饰器的类。 拦截器应该实现 NestInterceptor 接口的intercept方法。

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> {
    console.log('Before...');

    const now = Date.now();
    return next
      .handle()
      .pipe(
        tap(() => console.log(`After... ${Date.now() - now}ms`)),
      );
  }
}

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容