NestJS上手指南(一)

3,427 阅读11分钟

NestJS的文档内容是比较多的,也有中文文档,但是概念比较多,而且文档各部分内容之间缺乏一点联系性方面的表述,加上一些概念描述得不慎明了,使得文档整体上看起来有点儿散。

本文就在读NestJS文档的过程中,进行了一些思维导图的组织,帮助把知识更结构化,更易于读者建立可视化的体系性认知。

NestJS的基本使用包括如下方面:

image.png

一、文档

docs.nestjs.cn/7

二、安装

npm i -g @nest/cli

三、创建项目

1、创建命令

nset new project-name

2、初始化得到的项目包含如下核心文件

image.png

这几个文件的相互依赖引用系如下:

image.png

下面简单看一下这几个文件:

  • main.ts 应用程序入口文件。它使用NestFactory用来创建Nest应用实例。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();
  • app.module.ts

应用程序的根模块。

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

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
  • app.controller.ts

带有带个路由的基本控制器示例。

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();
  }
}
  • app.controller.spec.ts

对于基本控制器的单元测试样例。

import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';

describe('AppController', () => {
  let appController: AppController;

  beforeEach(async () => {
    const app: TestingModule = await Test.createTestingModule({
      controllers: [AppController],
      providers: [AppService],
    }).compile();

    appController = app.get<AppController>(AppController);
  });

  describe('root', () => {
    it('should return "Hello World!"', () => {
      expect(appController.getHello()).toBe('Hello World!');
    });
  });
});
  • app.service.ts 带有单个方法的基本服务
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

四、运行项目

npm run start

打开浏览器并访问 http://localhost:3000/ 可查看

如果你要断点调试,可以这么操作:

先在VS Code的Terminal面板右上角的下拉箭头展开的菜单中选择“JavaScript Debug Terminal”(如下图所示),然后在打开的控制台中其中输入npm run start命令回车执行,就可以在编辑器中代码对应的行号前添加断点进行调试了。

image.png

五、Contorller(控制器)

主要包含如下这些方面的内容:

image.png

1、控制器是什么

控制器负责处理传入的请求和向客户端返回响应。路由机制控制哪个控制器接收哪些请求。通常每个控制器有多个路由。为了创建一个基本的控制器,我们使用类和装饰器。 装饰器将类与所需的元数据相关联,并使Nest能够创建路由映射(将请求绑定到相应的控制器)。

2、用Nest CLI创建控制器

nest g controller router-prefix-name

3、Controller装饰器

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

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

其中,使用 @Controller() 装饰器定义一个基本的控制器。@Controller() 装饰器括号中可以传入参数。传入的参数将作为路由的前缀,这样就不必为文件中的每个路由写重复路径的那部分。

4、客户端请求相关的装饰器

处理程序有时需要访问客户端的请求细节。Nest提供了对底层平台(默认为Express)的请求对象(request)的访问方式。我们可以在处理函的数签名中使用 @Req() 装饰器,指示Nest将请求对象注入处理程序。

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

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

为了在 express 中使用 Typescript (如 request: Request 上面的参数示例所示),请安装 @types/express 。

为了与底层 HTTP 平台(例如,Express 和 Fastify)之间的类型兼容, Nest 提供了 @Res()和 @Response() 装饰器。@Res() 只是 @Response() 的别名。

Nest 还提供了许多装饰器及其代表的底层平台特定对象的对照列表:

image.png

5、HTTP方法装饰器

Nest为所有标准的HTTP方法提供了相应的装饰器:

image.png

6、路由通配符

路由同样支持模式匹配,例如星号被用作通配符,将匹配任何字符组合。

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

7、状态码装饰器

可以通过在处理函数外添加 @HttpCode(...) 装饰器来更改返回的状态码。 就像下面这样:

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

8、响应头装饰器

要指定自定义响应头,可以使用 @Header() 装饰器或类库特有的响应对象,(并直接调用 res.header())。

@Post()
@Header('Cache-Control', 'none')
create() {
  return 'This action adds a new cat';
}

9、重定向装饰器

要将响应重定向到特定的 URL,可以使用 @Redirect() 装饰器或特定于库的响应对象(并直接调用 res.redirect())。

@Redirect() 装饰器有两个可选参数,urlstatusCode。 如果省略,则 statusCode 默认为 302。

@Get()
@Redirect('https://nestjs.com', 301)

10、路由参数相关的控制器

定义带参数的路由,我们可以在路由路径中添加路由参数标记(token)以捕获请求 URL 中该位置的动态值。

@Param() 用于修饰一个方法的参数(上面示例中的 params),并在该方法内将路由参数作为被修饰的方法参数的属性。如上面的代码所示,我们可以通过引用 params.id 来访问(路由路径中的) id 参数。

@Get(':id')
findOne(@Param() params): string {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}

您还可以将特定的参数标记传递给装饰器,然后在方法主体中按参数名称直接引用路由参数。

@Get(':id')
findOne(@Param('id') id): string {
  return `This action returns a #${id} cat`;
}

11、子域路由

@Controller 装饰器可以接受一个 host 选项,以要求传入请求的 HTTP 主机匹配某个特定值。由于 Fastify 缺乏对嵌套路由器的支持,因此当使用子域路由时,应该改用(默认) Express 适配器(Express adapter)。

@Controller({ host: 'admin.example.com' })
export class AdminController {
  @Get()
  index(): string {
    return 'Admin page';
  }
}

12、异步性

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

通过返回 RxJS observable 流,Nest 路由处理程序将更加强大。 Nest 将自动订阅下面的源并获取最后发出的值(在流完成后)。

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

13、请求负载 ——DTO(数据传输对象)

这里通过添加 @Body() 参数来获取。

首先,需要确定 DTO(数据传输对象)模式。

DTO是一个对象,它定义了如何通过网络发送数据。我们可以通过使用 TypeScript 接口(Interface)或简单的类(Class)来定义 DTO 模式。有趣的是,我们在这里推荐使用类。为什么?类是 JavaScript ES6 标准的一部分,因此它们在编译后的 JavaScript 中被保留为实际实体。另一方面,由于 TypeScript 接口在转换过程中被删除,所以 Nest 不能在运行时引用它们。这一点很重要,因为诸如管道(Pipe)之类的特性为在运行时访问变量的元类型提供更多的可能性。

现在,我们来创建 CreateCatDto 类:

/*
  create-cat.dto.ts
*/
export class CreateCatDto {
  readonly name: string;
  readonly age: number;
  readonly breed: string;
}

然后,我们可以在 CatsController 中使用新创建的DTO:

/* cats.controller.ts */

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

14、一个完整的控制器实例

/* cats.controller.ts */

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

@Controller('cats')
export class CatsController {
  @Post()
  create(@Body() createCatDto: CreateCatDto) {
    return 'This action adds a new cat';
  }

  @Get()
  findAll(@Query() query: ListAllEntities) {
    return `This action returns all cats (limit: ${query.limit} items)`;
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return `This action returns a #${id} cat`;
  }

  @Put(':id')
  update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) {
    return `This action updates a #${id} cat`;
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return `This action removes a #${id} cat`;
  }
}

15、将控制器注入到模块中

控制器总是属于模块,这就是为什么我们在 @Module() 装饰器中包含 controllers 数组的原因。 由于除了根模块 AppModule之外,我们还没有定义其他模块,所以我们将使用它来介绍 CatsController。

/* app.module.ts */

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

@Module({
  controllers: [CatsController],
})
export class AppModule {}

六、Providers(提供者)

image.png

1、Providers是什么?

Providers 是纯粹的 JavaScript 类,只是在其类声明之前带有 @Injectable()装饰器。许多基本的 Nest 类可能被视为 provider,如service, repository, factory, helper 等等。

2、Service(服务)

/** 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;
  }
}

在Controller中使用Service:

/** 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) {
    this.catsService.create(createCatDto);
  }

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

其中,CatsService 是在类的构造函数中注入的。注意这里使用了私有的只读语法。这意味着我们已经在同一位置创建并初始化了 catsService 成员。

3、Optional Provider(可选提供者)

要指示 provider 是可选的,请在 constructor 的参数中使用 @Optional() 装饰器。

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

@Injectable()
export class HttpService<T> {
  constructor(
    @Optional() @Inject('HTTP_OPTIONS') private readonly httpClient: T
  ) {}
}

4、基于属性的注入

上面使用的技术称为基于构造函数的注入,即通过构造函数方法注入 Providers。在某些非常特殊的情况下,基于属性的注入可能会有用。例如,如果顶级类依赖于一个或多个 Providers,那么通过从构造函数中调用子类中的 super() 来传递它们就会非常烦人了。因此,为了避免出现这种情况,可以在属性上使用 @Inject() 装饰器。

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

@Injectable()
export class HttpService<T> {
  @Inject('HTTP_OPTIONS')
  private readonly httpClient: T;
}

5、注册Provider

现在我们已经定义了提供者(CatsService),并且已经有了该服务的使用者(CatsController),我们需要在 Nest 中注册该服务,以便它可以执行注入。为此,我们可以编辑模块文件(app.module.ts),然后将服务添加到 @Module() 装饰器的 providers 数组中。

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

七、Module(模块)

image.png

1、模块是什么?

模块是具有 @Module() 装饰器的类。 @Module() 装饰器提供了元数据,Nest 用它来组织应用程序结构。每个 Nest 应用程序至少有一个模块,即根模块。根模块是 Nest 开始安排应用程序树的地方。

image.png

2、@module() 装饰器

@module() 装饰器接受一个描述模块属性的对象:

  • providers

由 Nest 注入器实例化的提供者,并且可以至少在整个模块中共享

  • controllers

  • imports

  • exports

3、功能模块

1)什么是功能模块?

如果CatsControllerCatsService 属于同一个应用程序域。 应该考虑将它们移动到一个功能模块下,即 CatsModule。这就是功能模块的概念。

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

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

2)如何将模块导入根模块 (ApplicationModule)?

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

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

4、共享模块

在 Nest 中,默认情况下,模块是单例,因此您可以轻松地在多个模块之间共享同一个提供者实例。实际上,每个模块都是一个共享模块。一旦创建就能被任意模块重复使用。假设我们将在几个模块之间共享 CatsService 实例。 我们需要把 CatsService 放到 exports 数组中。然后,每个导入 CatsModule 的模块都可以访问 CatsService,并且它们将共享相同的 CatsService 实例。

/** 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]
})
export class CatsModule {}

5、模块导出

模块可以导出它们的内部提供者(Provider)。 而且,他们可以再导出自己导入的模块。

@Module({
  imports: [CommonModule],
  exports: [CommonModule],
})
export class CoreModule {}

6、依赖注入

Provider也可以注入到模块(类)中(例如,用于配置目的)。但是,由于循环依赖性,模块类不能注入到提供者中。

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

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {
  constructor(private readonly catsService: CatsService) {}
}

全局模块:

@Global 装饰器使模块成为全局模块。全局模块应该只注册一次,最好由根或核心模块注册。 在下面的例子中,CatsService 组件将无处不在,而想要使用 CatsService 的模块则不需要在 imports 数组中导入 CatsModule。但要注意使一切全局化并不是一个好的解决方案。 全局模块可用于减少必要模板文件的数量。 imports 数组仍然是使模块 API 透明的最佳方式。

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

7、动态模块

以下是一个动态模块定义的示例 DatabaseModule:

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

如果要在全局范围内注册动态模块,请将 global 属性设置为 true。但要注意,如上所述,将所有内容全局化不是一个好的设计决策:

{
  global: true,
  module: DatabaseModule,
  providers: providers,
  exports: providers,
}

DatabaseModule 可以被导入,并且被配置以下列方式:

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

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