Skip to content

nestjs实践

1. nest是什么?

Nest (NestJS) 是一个用于构建高效、可扩展的 Node.js 服务器端应用程序的开发框架。它利用 JavaScript 的渐进增强的能力,使用并完全支持 TypeScript (仍然允许开发者使用纯 JavaScript 进行开发),并结合了 OOP (面向对象编程)、FP (函数式编程)和 FRP (函数响应式编程)。

在底层,Nest 构建在强大的 HTTP 服务器框架上,例如 Express (默认),并且还可以通过配置从而使用 Fastify !

Nest 在这些常见的 Node.js 框架 (Express/Fastify) 之上提高了一个抽象级别,但仍然向开发者直接暴露了底层框架的 API。这使得开发者可以自由地使用适用于底层平台的无数的第三方模块。

处理一个请求,Express与Nest对比示意图:image.png

2. nest基本使用

2.1 创建一个nestjs项目

请确保在您的操作系统上安装了 Node.js (>= 10.13.0,v13 除外)。

bash
## 1. 全局安装nest脚手架
npm i -g @nestjs/cli

## 2. 利用脚手架创建项目
nest new <project-name>

接着生成一个项目, src目录结构如下:

js
src
    app.controller.ts		// 单个路由的基本控制器。
    app.controller.spec.ts	// 针对控制器的单元测试。
    app.module.ts		// 应用根模块
    app.service.ts		// 具有单一方法的基本服务
    main.ts			// 项目入口文件,它使用核心函数 NestFactory 来创建 Nest 应用程序的实例。

2.2 nest里面的一些概念

nest里面的装饰器都可以从@nestjs.common这个包里面解构出来

1)控制器 controller

@Controller()装饰器装饰的类可以看做是一个控制器,控制器负责处理传入的请求并将响应返回给客户端。

js
import { Controller } from '@nestjs/common';

@Controller()
export class AppController {}

image.png

2) 服务 service或者providers

provider是nest里面的提供者,最典型的就是serviceprovider是nest里面数据的来源

@Injectable()装饰器装饰的类就是一个provider

js
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {}

3)模块 module

module可以看做是组件,类似于vue,react等里面的组件,它里面包含controller与provider,默认情况下,一个module里面的provider不能被其他module使用,若要使用,需要做一些导出导入的操作。

@Module()装饰器装饰的类可以看做一个模块,模块内整合了controllerproviders,还可以整合其他模块,通过imports

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

@Module({
  imports: [], // 整合其他module
  controllers: [AppController], // 控制器
  providers: [AppService], // service
  exports:[] // 里面的的子集providers由该模块提供,并且应该在导入该模块的其他模块中可用
})
export class AppModule {}

官方模块示意图: image.png不管有多少个模块,整个应用需要有一个根模块,用来整合其他模块,其他每一个模块下面也可有自己的子模块

(1)module之间的关系

image.png

2.3 nest快速创建命令

nest提供我们一些命令来快速创建控制器,服务,模块,等等代码块,nest g xxx xxx

若不知道具体命令有哪些, 可以通过命令nest --help查看:

sh
nest --help

image.png

例如: 创建一个use控制器:

bash
nest g co user
## 或者
nest g controller user

image.png

这个命令执行完成后, 不仅会在src下创建一个user文件夹(若没有此文件夹会创建),里面创建了一个user.controller.ts控制器文件以及一个测试文件,并且会在app.module.tscontrollers里面自动注入刚创建好的UserController

注意: 默认会在src目录下面创建文件或文件夹, 若想选择创建的位置, 可以带上路径例如:

js
## 此时我们所在的位置是项目根目录
nest g co /app/user	## app目录可以不存在,会自动创建
## 或者
nest g co app/user

## 也可以切换到需要建的目录运行nest g 命令
cd src/app/
nest g co user

其他的一些命令做的事基本类似,大同小异,使用起来非常方便

2.4 路由

1)设置路由

nestjs与egg或者其他的框架不一样,像egg里面,在app文件夹下面会有一个专门的router.js文件专门用来定整个应用的所有路由与controller之间的关系,在nest里面却不是,nest里面路由是直接定义在controller里面的。例如下面的代码:

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

@Controller('nest')	
export class AppController {
  @Get()		// 路由是 /nest
  getList(): string {
    return '这是第一个路由';
  }

  @Post('index')	// 路由是  /nest/index
  add(): string {
    return '新增接口';
  } 
}

若在@controller('nest')里面写了东西,则代表当前这个controller里面的路由都是nest/xxx, 并且,路由处理函数名称和路由无关,只要在同一个controller里面名称不重复就可以

注意:装饰器后面不能带分号。

通配符路由

nest也支持基于模式的路由。例如,星号用作通配符,将匹配任何字符组合。

js
@Get('ab*cd')
findAll() {
  return 'This route uses a wildcard';
}

'ab*cd'路由路径将匹配、abcdab_cdabecd。字符?, +, *, 和()可以在路由路径中使用,并且是它们的正则表达式对应物的子集。连字符 ( -) 和点 ( .) 由基于字符串的路径逐字解释。

2)统一路由前缀

main.js里面设置统一的路由前缀

js
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.setGlobalPrefix('v1'); // 统一设置路由前缀,这里为v1
  await app.listen(3000);
}
bootstrap();

核心代码:app.setGlobalPrefix('v1'),设置完成后,请求这个nestjs服务的所有路由,都必须为/v1/xxxx

3)请求方式

nest里面提供了一堆装饰器,其中http请求方法(动词)就是由@nestjs/common这个模块提供的,

js
import {Get, Post, Put, Delete, Options,... } from '@nestjs/common';

Nest 为所有标准 HTTP 方法提供装饰器:@Get()@Post()@Put()@Delete()@Patch()@Options()@Head(). 此外,@All()定义一个处理所有这些的端点。

4)获取请求参数

提示: 为了利用express类型(如request: Request的参数示例),请安装@types/express包。 import { Request } from 'express';

http请求有不同的请求方式,get, post, delete, put 等等。服务端如果想取到不同请求的请求参数,大多数情况下,不需要我们手动获取,nest提供了一些装饰器用于很方便的获取不同的参数,如下:

下面是nest提供的装饰器列表以及它们所代表的普通平台特定对象 image.png

使用示例:

  1. @Query()获取query参数,@Query()是一个参数装饰器
js
import { Controller, Get, Query } from '@nestjs/common';

@Controller('nest')
export class AppController {
  @Get('getList')
  getList(@Query() query): string {
    console.log(query);
    return '这是第一个路由';
  }
}

效果image.png

其他的参数装饰器使用方式就是照猫画虎

5)设置响应

(1)设置状态码

默认情况下响应状态代码始终为200,但 POST 请求除外201@HttpCode(...)我们可以通过在处理程序级别添加装饰器来轻松更改此 行为。

js
import { HttpCode } from '@nestjs/common';

@Post()
@HttpCode(204)
create() {
  return 'This action adds a new cat';
}

若需要根据情况设置http状态码,可以通过注入@Res()装饰器,然后通过res.status(code)来设置状态码,但是需要注意,若是注入了@Res() res响应对象,controller里面需要通过res.send或者res.json等等底层平台(默认是Express)提供的响应方法来响应,直接return是不行的。

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

@Controller('nest')
export class AppController {
  @Get('getList')
  getList(@Req() req: Request, @Res() res: Response) {
    res.status(300);
    res.send('OK');
    // return 123;  (此时,这样是错误的,无法响应客户端)
  }
}

(2)设置响应头

要设置自定义响应头,可以使用@Header()装饰器或特定于库的响应对象(通过res.header()直接调用

js
import { Header } from '@nestjs/common';
@Post()
@Header('Cache-Control', 'none') 
create() {
  return 'This action adds a new cat';
}

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

@Controller('nest')
export class AppController {
  @Get('getList')
  getList(@Req() req: Request, @Res() res: Response) {
    res.setHeader('Cache-Control', 'none');
    res.send('OK');
    // return 123;  (此时,这样是错误的,无法响应客户端)
  }
}

2.5 controllerprovider

image.png

1)一对一

controller里面要使用service, 需要在controller里面引入service,并在constructor注入,就可以通过this调用即可。

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

@Controller()
export class AppController {
  // 2. 注入service
  constructor(private readonly appService: AppService) {}

  @Get()
  getList() {
    // 3. 使用
    return this.appService.findAll();
  }
}

2)一个controller内使用多个provider

若需要在一个controller里面使用多个service,只需要先将模块导出exports,然后我们controller对应的modules里面通过imports都引入,在controller的constroctor里面并都注入即可。

js
import { Controller, Get } from '@nestjs/common';
import { AService } from './A.service'; // 1. 引入 AService 
import { BService } from './B.service'; // 1. 引入 BService 

@Controller()
export class AppController {
  // 2. 一次注入多个service
  constructor(
     private readonly aService: AService,
     private readonly bService: BService
  ) {}

  @Get()
  getList() {
    // 3. 使用
    let a = this.aService.findAll();
    let b = this.aService.findAll();
    return [a, b]
  }
}

3)exportsimports@Global()

(1)exportsimports

一般情况下,一个模块内会包含多个controller,多个service,在模块内部,可以像上一步一样在controller正常引用注入service即可使用,但是业务不一定全都这样简单常规,很多时候,一个module往往需要用到其他module里面的一些provider,这个时候,仅仅通过上一步的方式还不够,可以认为一个module里面的东西,默认只能在模块内部使用,若要将模块内部的东西提供给外部使用,模块内部需要exports,m外面的模块需要imports

若一个模块里面的controller需要引入另外模块内的provider,需要在被引入的模块的module.ts里面,将这个模块下的service导出,自己模块内imports,这是框架的规定,否则无法正常启动。

js
// 提供provider的module

// user/user.module.ts
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';

@Module({
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService], // 只有导出, 才能在模块外部使用
})
export class UserModule {}
js
// 使用provider的module

// app.controller.ts 
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { UserService } from './user/user.service'; // 1. 引入userService

@Controller()
export class AppController {
  // 注入service
  constructor(
    private readonly appService: AppService,
    private readonly userService: UserService, // 2. 注入userService
  ) {}

  @Get()
  getList() {
    const user = this.userService.findOne(); // 3. 使用userService
    const app = this.appService.findAll();
    return {
      user,
      app,
    };
  }
}

(2)@Global()

上面的exportsimports虽然可以在一个module内使用其他模块的provider,但是,也有自己的缺点,就是麻烦 假如某个模块内的provider需要在100个模块内都使用,按照上面的方法,那么我们在这个模块内通过exports导出这个provider,并且在其他100个模块内都需要用imports:[]将这个模块内的provider引入才行,这显然是一个体力活,针对这种情况,框架提供了一个装饰器@Global(),在要被共用的模块(module.ts)里面使用这个装饰器,其他任何模块都可以使用这个模块的provider,并且不用引入。

这种方式针对与全局通用的模块来说非常友好,例如,帮助程序、数据库连接等。

ts
import { Global, Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';

@Global() // 将provider编程全局可用的,还需要看exports里面的内容
@Module({
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService], // 若不写,默认全部provider都变成全局的,写谁,谁编程全局可用
})
export class UserModule {}

3)日志输出

(1)常规日志输出

默认情况下,我们会使用console.log在控制台打印一些信息,这种方式很普通

image.png

nest框架允许我们定义输出信息的格式,例如:

image.png

(2)输出带自定义前缀的日志信息

若需要知道是哪个文件输出的日志,可以在在new Logger时候传入自定义名称,例如:

image.png

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

@Controller('user')
export class UserController {
  private readonly logger;
  constructor() {
    this.logger = new Logger('user controller');
  }
  @Get()
  index(): string {
    console.log('我是user模块的控制器');
    this.logger.log('我是自定义日志');
    return 'user index';
  }
}

(3)不同类型的日志打印

image.png

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

@Controller('user')
export class UserController {
  private readonly logger: Logger;
  constructor() {
    this.logger = new Logger('user controller');
  }
  @Get()
  index(): string {
    console.log('我是user模块的控制器');
    this.logger.debug('我是自定义debug日志');
    this.logger.log('我是自定义log日志');
    this.logger.warn('我是自定义warn日志');
    this.logger.error('我是自定义error日志');
    this.logger.verbose('我是自定义verbose日志');
    return 'user index';
  }
}

3. nest面向切面编程

3.1 请求流程【关卡】

image.png

从上图可以看到,当接收到一个请求的时候,nest从前到后都会经过那些关卡

他们都有着自己的作用