NestJS的文档内容是比较多的,也有中文文档,但是概念比较多,而且文档各部分内容之间缺乏一点联系性方面的表述,加上一些概念描述得不慎明了,使得文档整体上看起来有点儿散。
本文就在读NestJS文档的过程中,进行了一些思维导图的组织,帮助把知识更结构化,更易于读者建立可视化的体系性认知。
NestJS的基本使用包括如下方面:
一、文档
二、安装
npm i -g @nest/cli
三、创建项目
1、创建命令
nset new project-name
2、初始化得到的项目包含如下核心文件
这几个文件的相互依赖引用系如下:
下面简单看一下这几个文件:
- 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
命令回车执行,就可以在编辑器中代码对应的行号前添加断点进行调试了。
五、Contorller(控制器)
主要包含如下这些方面的内容:
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 还提供了许多装饰器及其代表的底层平台特定对象的对照列表:
5、HTTP方法装饰器
Nest为所有标准的HTTP方法提供了相应的装饰器:
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()
装饰器有两个可选参数,url
和 statusCode
。 如果省略,则 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(提供者)
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(模块)
1、模块是什么?
模块是具有 @Module()
装饰器的类。 @Module()
装饰器提供了元数据,Nest 用它来组织应用程序结构。每个 Nest 应用程序至少有一个模块,即根模块。根模块是 Nest 开始安排应用程序树的地方。
2、@module()
装饰器
@module()
装饰器接受一个描述模块属性的对象:
- providers
由 Nest 注入器实例化的提供者,并且可以至少在整个模块中共享
-
controllers
-
imports
-
exports
3、功能模块
1)什么是功能模块?
如果CatsController
和 CatsService
属于同一个应用程序域。 应该考虑将它们移动到一个功能模块下,即 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 {}