首页 > 技术文章 > NestJS 学习笔记

China-Dream 2021-12-03 15:03 原文

简介

Nest 是一个用于构建 Node.js 服务器端应用程序的框架。内置 TypeScript(也允许用纯 JavaScript 编写代码)并结合了 OOP(面向对象编程),FP(函数式编程)和 FRP(函数式响应编程)的元素及思想,基于装饰器的语言特性而创建,设计灵感来自于 Angular。
Angular 的很多模式又来自于 Java 中的 Spring 框架,依赖注入面向切面编程等,所以你可以认为: Nest 是 Node.js 版的 Spring 框架

同类型框架比较

  • Midway
  • Egg
  • Express

起步

相关命令

#安装脚手架
$ npm i -g @nestjs/cli

#使用脚手架创建一个项目
$ nest new [project-name]

#默认启动项目命令
$ npm run start

#打包项目
$ nest run build

脚手架初始化项目 - 选项

- Which package manager would you to use?(选择你想使用的包管理器)
  yarn | npm(默认) | pnpm

初始目录结构

src
 ├── app.controller.spec.ts  对于基本控制器的单元测试样例
 ├── app.controller.ts  带有单个路由的基本控制器示例
 ├── app.module.ts 应用程序的根模块
 ├── app.service.ts 带有单个方法的基本服务提供者
 └── main.ts 应用程序入口文件

其他指令

#查看帮助选项
$ nest --help|-h

#自动在目录中创建一个名为[module-name]的控制器模块文件
$ nest generate|g co [module-name]

#显示项目信息
$ nest info

#更新依赖包
$ nest update|u

#添加依赖包
$ nest add [library-name]
Generate命令说明(点击查看):
名称 别名 说明
application application 在当前目录创建新的应用(同 new 指令)
class cl 生成一个空 class 文件
configuration config 生成 CLI 配置文件
controller co 生成并声明一个控制器模块
decorator d 生成一个自定义装饰器文件
filter f 生成并声明一个过滤器模块
gateway ga 生成并声明一个网关模块
guard gu 生成并声明一个权限守卫模块
interceptor in 生成并声明一个拦截器模块
interface interface 生成并声明一个接口定义模块
middleware mi 生成并声明一个中间件模块
module mo 生成并声明一个模块管理文件
pipe pi 生成并声明一个管道模提供者
resolver r 生成并声明一个 GraphQL 解析器模块
service s 生成并声明一个服务模块
library lib 在 monorepo 中生成新库
sub-app app 在 monorepo 中生成新应用程序
resource res 生成一个完整的 CRUD 资源目录

定义

Controller(控制器)

通俗来说就是路由 Router,负责处理客户端传入的请求参数并向客户端返回响应数据,也可以理解是 HTTP 请求的逻辑处理。

注:要使用 CLI 创建控制器类,只需执行 $ nest g co [name] 命令。

示例:

/* dto/create-cat.dto.ts */

//定义一个Dto类,规定请求的入参格式。
export class CreateCatDto {
  readonly name: string;
  readonly age: number;
  readonly breed: string;
}
------------------------------------
/* cats.controller.ts */

import { Controller, Get, Query, Post, Body, Put, Param, Delete } from '@nestjs/common';
import { CreateCatDto, UpdateCatDto, ListAllEntities } from './dto';

@Controller('cats') //定义了一个前缀为'cats'的路由模块
export class CatsController {
  @Post() //通过Post方法访问到"/cats/"路由
  create(@Body() createCatDto: CreateCatDto) {
    return '此路由将添加一条新数据';
  }

  @Get() //通过Get方法访问到"/cats/"路由
  findAll(@Query() query: ListAllEntities) {
    return `此路由将返回所有数据 (limit: ${query.limit} items)`;
  }

  @Get(':id') //通过Get方法访问到"/cats/${id}"路由
  findOne(@Param('id') id: string) {
    return `此路由将根据${id}返回指定数据`;
  }

  @Put(':id') //通过Put方法访问到"/cats/${id}"路由
  update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) {
    return `此路由将根据${id}更新指定数据`;
  }

  @Delete(':id') //通过Delete方法访问到"/cats/${id}"路由
  remove(@Param('id') id: string) { //delete为js保留关键字,可以使用remove命名
    return `此路由将根据${id}删除指定条目`;
  }
}

Providers(提供者)

Providers 是一个用@Injectable()装饰器注释的类。许多基本的 Nest 类可能被视为 provider,如 service, repository, factory, helper 等,通过 constructor(构造器) 注入依赖关系。

注:要使用 CLI 创建服务类,只需执行 $ nest g s [name] 命令。

最常见的是使用@Injectable()装饰器创建一个提供数据操作服务的类,即 service 模块。
示例:

/* interfaces/cat.interface.ts */

//声明一个接口,规定请求的入参格式。
export interface Cat {
  name: string;
  age: number;
  breed: string;
}
------------------------------
/* cats.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;
  }
}

然后在 CatsController 里使用它:

/* dto/create-cat.dto.ts */

export class CreateCatDto {
  readonly name: string;
  readonly age: number;
  readonly breed: string;
}
----------------------------------
/* cats.controller.ts */

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 catsService: CatsService) {} //在这里通过构造器注入服务模块

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    //Nest已经自动把构造器中注入的依赖转换为实例,这里就可以直接通过this调用了。
    this.catsService.create(createCatDto);
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

定义了服务模块(提供者)和控制器路由模块(使用者)之后,还需要在模块管理文件中进行注册。
示例:

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

然后我们就拥有了现在的目录结构:

src
├── cats
│    ├──dto
│    │   └──create-cat.dto.ts
│    ├── interfaces
│    │       └──cat.interface.ts
│    ├──cats.service.ts
│    └──cats.controller.ts
├──app.module.ts
└──main.ts

Module(模块)

模块是具有 @Module() 装饰器的类。 @Module() 装饰器提供了元数据,Nest 用它来组织应用程序结构。
要使用 CLI 创建模块,只需执行 $ nest g module cats 命令。

每个 Nest 应用程序至少有一个模块,即根模块(app.module)。一般情况下应用程序可以按照功能划分若干个子模块,比如 cats 模块。
示例:

/* 子模块 cats/cats.module.ts */

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

@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService] //若需要在其它模块中CatsService实例,就需要把它导出去。
})

//Provider也可以注入到模块的导出类中,方便用于其他配置。
export class CatsModule {
  constructor(private readonly catsService: CatsService) {}
  /// 可以通过注入Provider,在这里做一些其它事情...
}
------------------------------------------
/* 根模块 app.module.ts */

import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule], //在这里导入子模块
})
export class ApplicationModule {}

按照上述的导出模块以共享实例,可能不能够满足某些特殊情况。比如我们在很多地方中都用到了相同的模块,或者想要一些模块即取即用(比如 helper,数据库连接等),如果在每个地方都这样导入导出就太麻烦了。所以可以把这类模块,通过 @Global 装饰器注册到全局。
示例:

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

通过按照功能划分模块之后,我们的项目结构现在是这样的:

src
├──cats
│    ├──dto
│    │   └──create-cat.dto.ts
│    ├──interfaces
│    │     └──cat.interface.ts
│    ├─cats.service.ts
│    ├─cats.controller.ts
│    └─cats.module.ts
├──app.module.ts
└──main.ts

Middleware(中间件)

中间件是在路由处理程序(controller)之前调用的函数,使用@Injectable()装饰器定义类。提供三个回调参数,请求对象(request)、响应对象(response)和中间件函数(next())。
注:要使用 CLI 创建中间件函数,只需执行 $ nest g mi [name] 命令。
中间件功能:

  • 执行任何代码。
  • 对请求和响应对象进行更改。
  • 结束请求-响应周期。
  • 调用堆栈中的下一个中间件函数。
  • 如果当前的中间件函数没有结束请求-响应周期, 它必须调用 next() 将控制传递给下一个中间件函数。否则, 请求将被挂起。

示例:

/* logger.middleware.ts */

import { Injectable, NestMiddleware } from "@nestjs/common";
import { Request, Response, NextFunction } from "express";
import { CatsService } from "./cats/cats.service";

//使用类的方式创建中间件
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  //同样支持依赖注入
  constructor(private readonly catsService: CatsService) {}
  use(req: Request, res: Response, next: NextFunction) {
    console.log(this.catsService.findAll());
    next();
  }
}

//使用函数的方式创建中间件
export function logger(req, res, next) {
  console.log(`Request...`);
  next();
}

然后在 app.module 中挂载(中间件并不能在@Module()装饰器中像 controller 数组一样列出,必须在实现 NestModule 接口的类上使用 configure 方法来设置和挂载)。
示例:

/* app.module.ts */

import { Module, NestModule, MiddlewareConsumer } from "@nestjs/common";
import {
  LoggerMiddleware,
  logger,
} from "./common/middleware/logger.middleware";
import { CatsModule } from "./cats/cats.module";

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware, logger) //可以指定一个或多个中间件
      .forRoutes("cats"); //通过forRoutes可以将中间件限制在制定路由下面使用。
  }
}

MiddlewareConsumer 是专门用来管理中间件的帮助类。forRoutes 方法支持传入字符串、对象或控制器类,并支持正则表达式的匹配。并且可以通过前置方法 exclude 来标记需要排除的路由。
示例:

consumer
  .apply(LoggerMiddleware)
  .exclude(
    { path: "cats", method: RequestMethod.GET },
    { path: "cats", method: RequestMethod.POST },
    "cats/(.*)"
  )
  .forRoutes(CatsController);

// forRoutes({ path: 'cats', method: RequestMethod.GET })
// forRoutes({ path: 'ab*cd', method: RequestMethod.ALL });

也可以使用入口文件(main)的 app.use()方法,全局挂载中间件到所有路由上:

const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);

Filter(过滤器)

过滤器其实是 Nest 提供的内置方法类,例如 HttpException(基础异常类),当我们想在控制器中抛出异常时,就可以使用 new HttpException 方法来提示客户端发生错误。
注:要使用 CLI 创建中间件函数,只需执行 $ nest g f [name] 命令。
示例:

/* cats.controller.ts */

@Get()
async findAll() {
  throw new HttpException({
    status: HttpStatus.FORBIDDEN,
    error: '这里可以自定义错误信息',
  }, HttpStatus.FORBIDDEN);
}

------------------------------------
/* 当客户端访问上述路由方法时,HTTP请求就会响应如下错误 */
{
  "status": 403,
  "error": "这里可以自定义错误信息"
}

我们可以把通用的基础异常信息封装到一个类(从HttpException继承)里,这样就不需要每次在调用的时候传值了。
示例:

/* forbidden.exception.ts */

export class ForbiddenException extends HttpException {
  constructor() {
    super({
      status: HttpStatus.FORBIDDEN,
      error: '这里可以自定义错误信息',
    }, HttpStatus.FORBIDDEN);
  }
}

---------------------------
/* cats.controller.ts */
@Get()
async findAll() {
  throw new ForbiddenException();
}
----------------------------
/* 当客户端访问上述路由方法时,HTTP请求就会响应如下错误 */
{
  "status": 403,
  "error": "这里可以自定义错误信息"
}
Nest内置的继承自HttpException的异常(点击查看):
  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ForbiddenException
  • NotAcceptableException
  • RequestTimeoutException
  • ConflictException
  • GoneException
  • PayloadTooLargeException
  • UnsupportedMediaTypeException
  • UnprocessableException
  • InternalServerErrorException
  • NotImplementedException
  • BadGatewayException
  • ServiceUnavailableException
  • GatewayTimeoutException

除了上述内置的基础异常类之外,我们还可以自己实现一个ExceptionFilter异常类,以便于自定义返回信息的格式。自定义的异常过滤器负责捕获Catch装饰器定义的异常类所抛出的异常,可以通过express提供的Request和Response对象获取当前的上下文请求响应数据。
示例:

/* http-exception.filter.ts */

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

//使用Catch装饰器定义一个异常类,实现ExceptionFilter接口
//用Catch参数声明要捕获的异常类别,该示例声明捕获基础异常类(HttpException),如不填,则捕获所有应用异常。
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter { 
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>(); //获取res对象
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();
    const exceptionRes: any = exception.getResponse(); //获取额外的返回数据
    const {
        message,
      } = exceptionRes;

    response
      .status(status)
      .json({ //自定义异常返回格式
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
        message,
      });
  }
}

-----------------------------------
/* cats.controller.ts */

import { Controller, Get, HttpStatus, } from '@nestjs/common';
import { CatsService } from './cats.service';
import { HttpExceptionFilter } from './http-exception.filter';

//HttpExceptionFilter过滤器通过UseFilters装饰器绑定到类或原型法上
@Controller('cats')
@UseFilters(HttpExceptionFilter) //也可以传递实例@UseFilters(new HttpExceptionFilter())
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Get()
  //@UseFilters(HttpExceptionFilter) 也可以绑定到方法上
  async findAll() {
    throw new HttpException({ //使用基础异常类实例抛出异常,等待捕获。
      status: HttpStatus.FORBIDDEN,
      message: '这里可以自定义错误信息',
    }, HttpStatus.FORBIDDEN);
  }
}

还可以把过滤器设置为全局,挂载到入口文件(main)的app上:

/* main.ts */

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

如果把过滤器通过app挂载到全局,则不能在控制器中使用依赖注入。如果想使用依赖注入,我们还可以把过滤器通过useClass绑定在根模块上:

/* app.module.ts */

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_FILTER, //该提供者的类型
      useClass: HttpExceptionFilter,
    },
  ],
})
export class AppModule {}

Pipe(管道)

管道应提供"数据转换"或"数据校验"的功能,用@Injectable()装饰器来声明,实现一个PipeTransform接口。
注:要使用 CLI 创建管道,只需执行 $ nest g pi [name] 命令。

Nest提供了8个内置管道(点击查看):
  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • DefaultValuePipe
  • ParseEnumPipe
  • ParseFloatPipe

示例:

/* CustomPipe.pipe.ts */

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

@Injectable() 
export class CustomPipe implements PipeTransform { //实现PipeTransform接口
  //必须提供一个transform方法
  transform(value: any, metadata: ArgumentMetadata) { 
    if (!isNaN(parseInt(value)) { //校验通过则直接返回值
    
      /* 在这里可以对原始值做一些处理,然后返回处理后的值。 
      const val = parseInt(value)
      return val
      */

      return value;
    }else{ //如不通过,则抛出一个异常。
      throw new HttpException(`值必须为整型!`, HttpStatus.BAD_REQUEST)
    }
  }
}

transform方法有两个参数,value(当前要处理的参数),metadata(当前参数的元数据对象)。

metadata包含的属性(点击查看)
参数 描述
type 说明该参数是一个 body @Body(),query @Query(),param @Param() 还是自定义参数。
metatype 参数的数据类型,例如 String。 如果在函数签名中省略类型声明,或使用原生 JavaScript,则为 undefined。
data 传递给装饰器的字符串,例如 @Body('string')。 如果将括号留空,则为 undefined。
然后在控制器类中使用管道,可以装饰类也可以装饰方法或在参数装饰器传入。 示例:
/* dto/create-cat.dto.ts */

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

----------------------------------
/* cats.controller.ts */

import { Controller, Post, Body, UsePipes } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CustomPipe } from './CustomPipe.pipe';

@Controller('cats')
//@UsePipes(CustomPipe) //可以装饰类
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Post()
  //@UsePipes(new CustomPipe()) //可以装饰方法
  async create(@Body(new CustomPipe()) createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }
}

在很多情况下,我们所创建的管道是通用的方法,所以可以把它设置为全局的,使用方法和过滤器一样。
示例:

/* main.ts */

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 可以使用app.useGlobalPipes方法挂载到全局应用上。
  app.useGlobalPipes(new CustomPipe());

  await app.listen(3000);
}
bootstrap();

--------------------

/* app.module.ts */

//也可以通过模块绑定到全局
@Module({
  providers: [
    {
      provide: APP_PIPE,
      useClass: CustomPipe
    }
  ]
})
export class AppModule {}

Guard(路由守卫)

Guard应使用@Injectable()装饰器定义,实现一个CanActivate接口,提供控制器路由的前置防卫功能。使用方式类似于过滤器、管道和拦截器。
注:要使用 CLI 创建Guard,只需执行 $ nest g gu [name] 命令。
示例:

/* auth.guard.ts */

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

@Injectable()
export class AuthGuard implements CanActivate { //实现CanActivate接口
  // 必须提供一个canActivate方法,返回一个布尔值。
  // 返回值 true:允许本次请求,false:忽略本次请求并返回HttpException异常。
  canActivate(
    context: ExecutionContext, //获取上下文
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest(); //获取当前请求

    ///可以在这里做一些事情,比如校验Token之类...

    return true; //返回true则允许本次请求
  }
}

然后在控制器中使用UseGuards装饰器声明。像过滤器一样,可以装饰类也可以装饰方法,也可以设置全局。
示例:

/* dto/create-cat.dto.ts */

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

----------------------------------
/* cats.controller.ts */

import { Controller, Post, Body, UseGuards } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { AuthGuard } from './auth.guard';

@Controller('cats')
// @UseGuards(AuthGuard)
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Post()
  @UseGuards(AuthGuard)
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }
}

全局守卫:

/* main.ts */

const app = await NestFactory.create(AppModule);

//使用app.useGlobalGuards方法挂载到全局
app.useGlobalGuards(new RolesGuard());

-------------------------------------

/* app.module.ts */

import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';

//也可以使用模块绑定到全局
@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
  ],
})
export class AppModule {}

下面举一个完整的例子,现在有一个路由模块(控制器类),需要按照不同的用户角色来执行不同的逻辑,而分辨角色的逻辑不应该直接写在控制器里,我们可以把它先在路由守卫里分类出来,然后再使请求继续。
示例:

/* roles.guard.ts */

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;

    //自定义的matchRoles方法,可根据实际情况编写逻辑。
    return matchRoles(roles, user.roles); 
  }
}

----------------------------------
/* roles.decorator.ts */

import { SetMetadata } from '@nestjs/common';

//这里我们为了方便,自定义了一个设置Roles元数据的装饰器
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

----------------------------------
/* cats.controller.ts */

@Post()
@Roles('admin') //也可以直接在这里设置元数据@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}
----------------------------------
/* app.module.ts */

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
  ],
})
export class AppModule {}

Interceptor(拦截器)

Interceptor应使用@Injectable()装饰器定义,实现一个NestInterceptor接口。使用方式类似于过滤器、管道和路由守卫。

Decorator(自定义装饰器)

自定义装饰器的使用方法其实就是js/ts原生的装饰器使用方式,然后根据Nest提供的上下文对象(ExecutionContext),自己写装饰器的函数内容,区别于直接import导入使用的Nest内置的装饰器。
示例:

/* user.decorator.ts */

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator((data: string, ctx: ExecutionContext) => {
  const request = ctx.switchToHttp().getRequest();
  const user = request.user;

  return data ? user && user[data] : user;
});

---------------------------------

/* cats.controller.ts */

import { Controller, Get } from '@nestjs/common';
import { User } from './user.decorator.ts';

@Controller('cats')
export class CatsController {
  @Get()
  async findOne(@User('firstName') firstName: string) {
    console.log(`Hello ${firstName}`);
  }
}

推荐阅读