NestJS综述目录
第一步
在这组文章中,您将了解Nest的核心基础知识。 为了熟悉Nest应用程序的基本构建块,我们将构建一个基本的CRUD应用程序,其功能涵盖介绍性的大量基础知识。
语言
我们热爱TypeScript,但最重要的是,我们热爱Node.js。这就是Nest同时兼容TypeScript和 纯JavaScript的原因。 Nest利用了最新的语法特性,因此要将其用于纯JavaScript,我们需要一个Babel编译器。
我们将在提供的示例中主要使用TypeScript,但您始终可以将代码片段切换为普通JavaScript语法(只需单击每个片段右上角的语言按钮即可切换)
先决条件
请确保您的操作系统上安装了Node.js(版本>=16)
设置
使用 Nest CLI设置新项目非常简单。安装npm后,您可以再操作系统终端中使用以下命令创建一个新的Nest项目:
npm -i -g @nestjs/cli
nest new project-name
如果要使用TypeScript更严格的功能集创建新项目,请使用--strict标志传递给nest new命令。
将创建project-name目录,安装节点模块和一些其它样板文件,并将创建src/目录并填充几个核心文件。
src
|--app.controller.spec.ts
|--app.controller.ts
|--app.module.ts
|--app.service.ts
|--main.ts
以下是这些核心文件的简要概述:
| app.controller.ts | 具有单一路线的基本控制器 |
|---|---|
| app.controller.spec.ts | 控制器的单元测试 |
| app.module.ts | 应用程序的根模块 |
| app.service.ts | 具有单一方法的基础服务 |
| main.ts | 应用程序的入口文件,使用核心函数NestFactory创建Nest应用程序实例 |
main.ts包含一个异步函数,它将引导我们的应用程序。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
要创建Nest应用程序实例,我们需要使用核心NestFactory类。
NestFactory类提供了几个静态方法,用于创建应用程序实例。
create()方法会返回一个应用程序对象,该对象符合INestApplication接口。
该对象提供了一系列方法,这些方法将在接下来的章节中介绍。在上面的main.ts示例中,
我们只需启动HTTP监听器,让应用程序等待入站HTTP请求即可。
请注意,使用Nest CLI搭建的项目会创建一个初始化项目结构,鼓励开发人员遵循每个模块 保留在其自己的专用目录中的约定。
默认情况下,如果在创建应用程序时发生任何错误,
您的应用程序将退出并显示代码1。
如果您想让它抛出错误,请禁用abortOnError选项,例如
NestFactory.create(AppModule, {abortOnError: false})
平台
Nest的目标是成为一个与平台无关的框架。 平台独立性使得创建可重复使用的逻辑部件成为可能,开发人员可以再多种不同类型 的应用程序中利用这些逻辑部件。 从技术上讲,一旦创建了适配器,Nest就能与任何Node HTTP框架协同工作。 开箱即支持两种HTTP平台:express和fastify。您可以选择最适合您需求的一种。
-
express平台
Express是一个著名的极简网络节点框架。 它是一个经过实战检验、可用于生产的库,拥有大量由社区提供的资源。 默认使用
@nestjs/platform-express包。 许多 用户都能很好地使用Express,无需采取任何措施启用它。 -
fastify平台
Fastify是一个高性能、低开销的框架,高度专注于提供最大的效率和速度。 在这里阅读如何使用它
无论使用哪个平台,它都会公开自己的应用程序结构,它们分别被视为NestExpressApplication
和NestFastifyApplication。
如下例所示,当你向NestFactory.create()方法传递一个类型时,
应用程序对象将拥有该特定平台专用的方法。但请注意,除非您真的想访问底层平台API,否则无需指定类型。
const app = await NestFactory.create<NestExpressApplication>(AppModule);
运行应用程序
安装过程完成后,您可以再操作系统命令提示符下运行以下命令来启动应用程序侦听入站HTTP请求:
npm run start
为了加快开发过程(构建速度加快20倍),您可以通过将-b swc标志传递给启动脚本来使用SWC构建器,
如下所示npm run start -- -b swc
此命令将启动应用程序,HTTP服务器将监听src/main.ts文件中定义的端口。
应用程序运行后,打开浏览器并导航至http://localhost:3000。您将看到Hello World!!
要查看文件中的更改,可以运行以下命令启动应用程序:
npm run start:dev
该命令将监听您的文件,自动重新编译并重新加载服务器。
语法检查和格式化
CLI尽最大努力构建可靠大规模开发工作流程。因此,生成的Nest项目预装了代码linter和
formatter程序(分别为eslint和prettier)
不确定格式化程序与代码检查的作用?在这里了解差异
为了确保最大的稳定性和可扩展性,我们使用基本的eslint和prettier包。
此设置允许IDE在设计上与官方扩展完美集成。
# 使用eslint进行lint和自动修复
npm run lint
# 使用prettier设置的格式进行代码格式化
npm run format
对于IDE不相关的无头环境(持续集成、Git挂钩等),Nest项目附带了现成的npm脚本
控制器
控制器负责处理传入请求并向客户端返回响应。
控制器的作用是接收应用程序的特定请求。 路由机制控制哪个控制器接收哪些请求。 通常情况下,每个控制器都有不止一个路由,不同的路由可以指定不同的操作。
为了创建基本控制器,我们使用类和装饰器。 装饰器将类与所需的元数据关联起来,并使Nest能够创建路由映射(将请求绑定到相应的控制器)。
为了快速创建带有内置验证的CRUD控制器,您可以使用CLI的CRUD生成器:nest g resource [name]
路由
在下面的示例中,我们将使用@Controller()装饰器,这是定义基本控制器所必需的。
我们将指定一个可选的路径前缀:cats。在@Controller()装饰器中使用路径前缀
可以让我们轻松地将一组相关的路由分组,并最大限度地减少重复代码。
例如,我们可以选择将一组管理与猫实体交互的路由归类到路由/cats下。
在这种情况下,我们可以再@Controller()装饰器中指定路径前缀cats,
这样就不必为文件中的每个路由重复写路径前缀的这一部分。
import { Controller, Get } from '@nestjs/commom';
@Controller('cats')
export class CatsController {
@Get()
findAll(): string {
return 'This action returns all cats';
}
}
要使用CLI创建控制器,只需执行nest g controller [name]命令
findAll()方法之前的@Get() HTTP 请求方法装饰器告诉Nest为HTTP请求的特定端点创建处理程序。
端点对应于HTTP请求方法(在本例中为GET)和路由路径。路由路径是什么?
处理程序的路由路径是通过连接为控制器声明的(可选)前缀和方法装饰器中指定的任何路径来确定的。
由于我们已经为每个路由(cats)声明了一个前缀,并且没有在装饰器中添加任何路径信息,
因此Nest会将GET/cats请求映射到此处理程序。
如前所述,路径包括可选的控制器路径前缀和请求方法装饰器中声明的任何路径字符串。
例如,cats的路径前缀与装饰器@Get('breed')结合将为GET /cats/breed等请求生成路由映射。
在上面的示例中,当前此端点发出GET请求时,Nest将请求路由到我们用户定义的findAll()方法。
请注意,我们在这里选择的方法名称是完全任意的。显然,我们必须声明一个方法来绑定路由,
但Nest并不赋予所选方法名称任何意义。
此方法将返回200状态码和关联的响应,在本例中只是一个字符串。为什么会发生这种情况? 为了解释这一点,我们首先介绍Nest使用两种不同选项来操纵响应的概念:
-
标准(推荐)
使用此内置方法,当请求处理程序返回 JavaScript 对象或数组时,它将自动序列化为 JSON。 然而,当它返回 JavaScript 基本类型(例如字符串、数字、布尔值)时,Nest 将仅发送该值,而不尝试对其进行序列化。 这使得响应处理变得简单:只需返回值,Nest 就会处理其余的事情。
此外,默认情况下,响应的状态码始终为200,但使用 201 的 POST 请求除外。 我们可以通过在处理程序级别添加
@HttpCode(...)装饰器来轻松更改此行为(请参阅状态代码)。 -
特定库
我们可以 使用特定于库的(例如 Express)响应对象,可以使用方法处理程序签名中的
@Res()装饰器注入该对象(例如findAll(@Res() response))。 通过这种方法,您可以使用该对象公开的本机响应处理方法。例如,使用Express,您可以使用像response.status(200).send()这样的代码构建响应。
Nest 检测处理程序何时使用@Res()或@Next(),表明您已选择特定库的选项。
如果同时使用两种方法,则该单一路线的标准方法将自动禁用,并且将不再按预期工作。
要同时使用这两种方法(例如,通过注入响应对象以仅设置 cookie/headers,但仍将其余部分留给框架),
您必须在@Res({ passthrough: true })装饰器设置passthrough为true。
请求对象
处理程序通常需要访问客户端请求的详细信息。Nest 提供对底层平台(默认为 Express)请求对象的访问。 我们可以在处理程序的签名中添加 @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的类型(如上面的request: Request请求参数示例),请安装@types/express包
request对象代表HTTP请求,具有请求查询字符串、参数、HTTP头信息和正文的属性。
在大多数情况下,无需手动抓取这些属性,我们可以使用专有的装饰器,例如@Body()或@Query(),
这些装饰器开箱即用。下面列出所提供的装饰器以及它们所代表的特定平台对象。
@Request(), @Req() | req |
|---|---|
@Response(), @Res() * | res |
@Next() | next |
@Session() | req.session |
@Param(key?: string) | req.params / req.params[key] |
@Body(key?: string) | req.body / req.body[key] |
@Query(key?: string) | req.query / req.query[key] |
@Headers(name?: string) | req.headers / req.headers[name] |
@Ip() | req.ip |
@HostParam() | req.hosts |
*为了与底层HTTP平台(例如Express和Fastify)之间的类型兼容,Nest提供了@Res和@Response()装饰器。
@Res()只是@Response()的别名。两者都直接公开底层平台响应对象接口。
使用它们时,您还应该导入底层库的类型(例如@types/express)以充分利用他们。
请注意,当您在方法处理程序中注入@Res()或@Response()时,您会将Nest置于该处理程序的特定库的模式,
并且您将负责管理响应。
执行此操作时,您必须通过调用响应对象来发出某种类型的响应,否则HTTP服务器将挂起。
例如(res.json(...)或res.send(...))
资源
之前,我们定义了一个端点来获取cats资源(GET路由)。我们还希望提供一个创建新纪录的端点。 为此,我们创建POST处理程序。
import { Controller, Get, Post } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Post()
create(): string {
return 'This action adds a new cat';
}
@Get
findAll(): string {
return 'This action returns all cats';
}
}
就是这么简单。Nest为所有标准HTTP方法提供了装饰器:
@Get@Post@Put@Delete()@Patch()@Options()@Head()@All()其中All()定义了一个处理所有这些端点。
路由通配符
Nest还支持基于模式的路由。例如星号(asterisk)用作通配符,将匹配任意字符组合。
@Get('ab*cd')
findAll() {
return 'This route uses a wildcard';
}
ab*cd路由路径将匹配abcd,ab_cd,abecd。
字符?、+、*、()可以在路径中使用,它们是正则表达式的子集。
连字符(-)和点字符(.)在基于字符串的路径中按字面解释。
仅express支持路由中间的通配符
状态码
如前所述,默认情况下响应状态码始终为200,POST请求除外,该代码为201。
我们可以通过在处理程序级别添加@HttpCode(...)装饰器来轻松更改此行为。
@Post()
@HttpCode(204)
create() {
return 'This action adds a new cat';
}
从@nestjs/common包导入HttpCode
通常,您的状态码不是静态的,而是取决于各种因素。在这种情况下,您可以使用特定库的响应(使用@Res()注入)
对象(或者,如果发生错误,则抛出异常)。
信息头
要指定自定义响应信息头,您可以使用
@Header装饰器- 或特定库的响应对象(并直接调用
res.header())
@Post()
@Header('Cache-Control', 'none')
create() {
return 'This action adds a new cat'
}
从@nestjs/common包中导入Header
重定向
要讲响应重定向到特定URL,您可以使用
Redirect()装饰器- 或特定库的响应对象(并直接调用
res.redirect())
Redirect()有两个参数:
urlstatusCode两个都是可选的。 如果省略,statusCode的默认值为302(已找到)
@Get()
@Redirect('http://nestjs.com', 301)
有时您可能想要动态确定HTTP状态码或重定向URL。
通过返回HttpRedirectResponse接口的对象来完成此操作。
返回值会覆盖传递给@Redirect()装饰器的任何参数。例如
@Get('docs')
@Redirect('https://docs.nestjs.com', 302)
getDocs(@Query('version') version) {
if (version && version === '5') {
return { url: 'http://docs.nestjs.com/v5/'}
}
}
路由参数
当您需要接受动态数据作为请求的一部分时(例如,GET /cats/1以获得id 1的cat),
具有静态路径的路由将不起作用。为了定义带参数的路由,我们可以在路由的路径中添加路由参数标记,
以捕获请求URL 中该位置的动态值。下面的@Get()装饰器示例中的路由参数标记演示这种用法。
以这种方式声明的路由参数可以使用@Param()装饰器来访问,该装饰器应该添加到方法签名中。
带参数的路由应在任何静态路径之后声明。 这可以防止参数化路径拦截发往静态路径的流量。
@Get(':id')
findOne(@Param() params: any): string {
console.log(params.id);
return `This action returns a #${params.id} cat`;
}
@Param()用于装饰方法参数(上例中的params),并使用路由参数可用作方法体内该装饰方法参数的属性。
如上面的代码所示,我们可以通过引用params.id来访问id参数。
还可以向装饰器传入特定的参数token,然后在方法体中直接通过名称引用路由参数。
从@nestjs/common导入Param
@Get(':id')
findOne(@Param('id') id: string): string {
return `This action returns a #${id} cat`;
}
子域路由
@Controller装饰器可以采用host选项来要求传入请求的HTTP主机与某个特定值匹配。
@Controller({ host: 'admin.example.com' })
export class AdminController {
@Get()
index(): string {
return 'Admin page';
}
}
由于Fastify缺乏对嵌套路由器的支持,因此在使用子域路由时,应使用(默认)Express适配器。
与路由路径(path)类似,hosts选项可以使用tokens来捕获主机名中该位置的动态值。
下面的@Controller()装饰器示例中的主机参数tokens演示了这种用法。
以这种方法声明的主机参数可以使用@HostParam()装饰器来 访问,该装饰器应该添加到方法签名中。
@Controller({ host: 'account.example.com' })
export class AccountController {
@Get()
getInfo(@HostParam('account') account: string) {
return account;
}
}
作用域
对于来自不同编程语言背景的人来说,可能会意外地发现,在Nest中,几乎所有内容都在传入请求之间共享。 我们有一个数据库连接池、具有全局状态的单例服务等。 请记住,Node.js不遵循请求/响应多线程无状态模型。因此,使用单例实例对于我们的应用程序来说是安全的。
然而,在某些边缘情况下,控制器的基于请求的生命周期可能是所需的行为,例如GraphQL应用程序中的每个 请求缓存、请求跟踪或多租户。在此处了解如何控制范围。
异步性
我们喜欢现代JavaScript,并且知道数据提取大多是异步的。
这就是Nest支持异步函数并与异步(async)函数配合良好的原因。
在此处了解有关async/await功能。
每个异步函数都必须返回一个Promise。
这意味着您可以返回Nest能够自行解析的延迟值。让我们看一个例子:
@Get()
async findAll(): Promise<any[]> {
return [];
}
上述代码完全有效。此外,Nest路由处理程序还能返回RxJS可观察流,因此功能更加强大。 Nest会自动订阅下方的源,并获取最后发射的值(一旦流完成)。
@Get()
findAll(): Observable<any[]> {
return of([])
}
上述两种方法都有效,您可以使用适合您要求的任何方法。
请求负载
我们之前的POST路由处理程序示例不接受任何客户端参数。
让我们通过在此处添加@Body()装饰器来解决此问题。
但首先(如果您使用TypeScript),我们需要确定DTO(数据传输对象)架构。
DTO是一个定义数据如何通过网络发送的对象。
我们可以通过使用TypeScript接口或简单的类来确定DTO模式。
有趣的是,我们建议在这里 使用classes。
为什么?类是JavaScript ES6标准的一部分,因此它们在编译后的JavaScript中保留为真实实体。
另一方面,由于TypeScript接口在转译过程中被删除,Nest无法再运行时引用它们。
这很重要,因为管道等功能在运行时可以访问变量的元类型时可以提供额外的可能性。
让我们创建CreateCatDto类:
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
它只有三个基本属性。此后我们可以再CatsController中使用新创建的DTO:
@Post()
async create(@Body() createCatDto: CreateCatDto) {
return 'This action adds a new cat';
}
我们的ValidationPipe可以过滤掉方法处理程序不应接收的属性。
在这种情况下,我们可以将可接受的属性列入白名单,并且任何未包含在白名单中的属性都会自动从结果对象中删除。
在CreateCatDto示例中,我们的白名单是name,age,breed属性。
处理错误
将在处理错误(即处理异常)章节介绍
完整资源样本
下面是一个使用几个可用装饰器来创建基本控制器的示例。 该控制器公开了几种访问和操作内部数据的方法。
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`;
}
}
Nest CLI提供了一个生成器(原理图),可以自动生成所有的样板代码,帮助我们避免执行所有这些操作, 并使开发人员体验更加简单。
启动并运行
完全定义上述控制器后,Nest仍然不知道CatsController存在,因此不会创建此类的实例。
控制器始终属于一个模块,这就是为什么我们在@Module装饰器中包含控制器数组的原因。
由于除了根AppModule之外我们还没有定义任何其他模块,
因此我们将使用它来引入CatsController:
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
@Module({
controllers: [CatsController],
})
export class AppModule {}
我们使用@Module()装饰器将元素附加到模块类,Nest现在可以轻松反映必须安装哪些控制器。
库特定方法
到目前为止,我们已经讨论了Nest操作响应的标准方法。
操作响应的第二种方法是使用库特定的响应对象(response object)。
为了注入特定的响应对象,我们需要使用@Res()装饰器。
为了显示差异,我们将CatsController重写为以下内容:
import { Controller, Get, Post, Res, HttpStatus } from '@nestjs/common';
import { Response } from 'express';
@Controller('cats')
export class CatsController {
@Post()
create(@Res() res: Response) {
res.status(HttpStatus.CREATED).send();
}
@Get()
findAll(@Res() res: Response) {
res.status(HttpStatus.OK).json([])
}
}
尽管这种方法有效,并且实际上通过提供对响应对象的完全控制(头信息操作、特定库的功能等), 在某些方面确实允许更大的灵活性,但应谨慎使用。 一般来说,该方法不太清楚,并且确实有一些缺点。 主要缺点是您的代码变得依赖于平台(因为底层库在响应对象上可能有不同的API), 并且更难以测试(您必须模拟响应对象等)
此外,在上面的示例中,您将失去与依赖于Nest标准响应处理的Nest功能的兼容性,
例如拦截器(Interceptors)和@HttpCode() / @Header()装饰器。
要解决此问题,您可以将passthrough选项设置为true,如下所示:
@Get()
findAll(@Res({ passthrough: true }) res: Response) {
res.status(HttpStatus.OK);
return [];
}
现在您可以与本机响应对象交互(例如,根据某些条件设置cookie或headers),
但将其余的留给框架。
提供者
提供者是Nest中的一个基本概念。许多基本的Nest类可以被视为提供者:
- 服务(services)
- 存储库(repositories)
- 工厂(factories)
- 助手(helpers)
许多基本的Nest类可以被视为提供者-服务、存储库、工厂、助手等等。 提供者的主要思想是它可以作为依赖项注入。这意味着对象之间可以创建各种关系, 并且"连接"这些对象的功能很大程度上可以委托给Nest运行时系统。
在上一章节中,我们构建了一个简单的CatsController。
控制器应该处理HTTP请求并将更复杂的任务委托给提供者。
提供者在模块中声明为providers的纯JavaScript类。
由于Nest能够以更加面向对象的方式设计和组织依赖关系,因此我们强烈建议遵循SOLID原则
服务
让我们创建一个简单的CatsService开始。
这个服务将负责数据存储和检索,并且设计为由CatsController使用,
因此它是定义为提供者的良好候选者。
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;
}
}
要使用CLI创建服务,只虚执行nest g service cats命令
我们的CatsService事一个基本类,具有一个属性和两个方法。
唯一的新功能是它使用@Injectable装饰器。
@Injectable()装饰器附加元数据,它声明CatsService是一个可以由Nest IoC容器管理的类。
顺便说一句,这个例子也使用了Cat接口,它可能看起来像这样。
export interface Cat {
name: string;
age: number;
breed: string;
}
现在我们有了一个用于检索猫的服务类,让我们在CatsController中使用它:
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成员。
依赖注入
Nest是围绕通常称为依赖注入的强大设计模式构建的。 我们建议阅读Angular 官方文档中有关此概念的精彩文章。
在Nest中,借助TypeScript功能,管理依赖项变得非常容易,因为它们仅通过类型来解析。
在下面的示例中,Nest将通过创建并返回catsService(或者,在单例的正常情况下,如果已在
其他地方请求了现有实例,则返回现有实例)。
此依赖性已解决并传递给控制器的构造函数(或分配给指定的属性)。
constructor(private catsService: CatsService) {}
作用域
提供者通常具有与应用程序生命周期同步的生命周期("scope")。 当应用程序启动时,必须解决每个依赖项,因此必须实例化每个提供者程序。 同样,当应用程序关闭时,每个提供者程序都将被销毁。 但是,也有一些方法可以使您的提供者程序生命周期限定在请求范围内。
自定义提供者
Nest有一个内置的控制反转(IoC)容器,可以解析提供者之间的关系。 此功能是上述依赖注入功能的基础,但实际上比我们迄今为止所描述的功能要强大得多。 有多种方法可以定义提供者程序:您可以使用普通值、类以及异步或同步工厂。
可选提供者
有时,您可能存在不一定需要解决的依赖关系。 例如,您的类可能依赖于配置对象,但如果没有传递任何内容,则应使用默认值。 在这种情况下,依赖关系变得可选,因为缺少配置提供者程序不会导致错误。
要指明提供者程序时可选的,请在构造函数的签名中使用@Optional()装饰器。
import { Injectable, Optional, Inject } from '@nestjs/common';
@Injectable()
export class HtpService<T> {
constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
}
请注意,在上面的示例中,我们使用的事自定义提供者程序,这就是我们包含HTTP_OPTIONS自定义符号的原因。
前面的示例显示了基于构造函数的注入,指示通过构造 函数中的类的依赖关系。
基于属性的注入
到目前为止,我们使用的技术称为基于构造函数的注入,因为提供者程序是通过构造函数方法注入的。
在某些非常特殊的情况下,基于属性的注入可能很有用。例如,如果您的顶级类依赖于一个或多个提供者,
则通过从构造函数调用子类中的super()将它们一路向上传递可能会非常乏味。
为了避免这种情况,您可以再属性级别使用@Inject()装饰器。
import { Injectable, Inject } from '@nestjs/common';
@Injectable()
export class HttpService<T> {
@Inject('HTTP_OPTIONS')
private readonly httpClient: T;
}
如果您的类没有扩展另一个类,那么您应该始终更喜欢使用基于构造函数的注入。
提供者注册
现在我们已经定义了一个提供者(CatsService),并且有了该服务的使用者(CatsController),
我们需要向Nest注册该服务,以便它可以执行注入。
我们通过编辑模块文件(app.module.ts)并将服务添加到@Module()装饰器的提供者数组中来实现此目的。
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 {}
Nest现在能够解析CatsController类的依赖关系。
这就是我们的目录结构现在的样子:
src
|--cats
|--dto
|--create-cat.dto.ts
|--interfaces
|--cat.interface.ts
|--cats.controller.ts
|--cats.service.ts
|--app.module.ts
|--main.ts
手动实例化
到目前为止,我们已经讨论了Nest如何自动处理解决依赖关系的大部分细节。 在某些情况下,您可能需要跳出内置依赖注入系统并手动检索或实例化提供者程序。
要获取现有实例或动态实例化提供者程序,您可以使用模块引用。
在bootstrap()函数中获取提供者程序(例如,对于没有控制器的独立应用程序,
或在引导期间使用配置服务)。
模块
模块是用@Module()装饰器注解的类。
@Module()装饰器提供Nest用于组织应用程序结构的元数据。
每个应用程序至少有一个模块,即根模块。 根模块是Nest用于构建应用程序图的起点-Nest用于解析模块和提供者关系及依赖关系的内部数据结构。 虽然理论上非常小的应用程序可能只有根模块,但这不是典型情况。 我们要强调的是,强烈建议将模块作为组织组件的有效方式。 因此,对于大多数应用程序来说,最终的架构将采用多个模块, 每个模块封装一组密切相关的功能。
@Module()装饰器 采用单个对象,其属性描述模块:
providers | 将由Nest注入器实例化的提供者程序,并且至少可以再该模块中共享 |
|---|---|
controllers | 该模块中定义的必须实例化的控制器集 |
imports | 导入模块的列表,导出该模块所需的提供者程序 |
exports | 此模块提供的提供程序子集,并且应该在导入此模块的其他模块中可用 |
该模块默认封装了提供者。 这意味着不可能注入即不直接属于当前模块也不从导入模块导出的提供者程序。 因此,您可以将从模块导出的提供者程序视为模块的公共接口或API。
功能模块
CatsController和CatsService属于同一应用程序域。由于它们密切相关,因此将它们移至
功能模块中是有意义的。
功能模块只是组织与特定功能相关的代码,保持代码组织并建立清晰的边界。
这有助于我们管理复杂性并按照可靠的原则进行开发,特别是随着应用程序或团队规模的增长。
为了演示这一点,我们将创建CatsModule
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
要使用CLI创建模块,只需执行nest g module cats
上面,我们在cats.module.ts文件中定义了CatsModule,并将与该模块相关的所有内容移至cats目录中。
我们需要做的最后一件事是将此模块导入到根模块(AppModule,在app.module.ts文件中定义)。
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})
export class AppModule {}
这是我们的目录结构现在的样子:
src
|--cats
|--dto
|--create-cat.dto.ts
|--interfaces
|--cat.interface.ts
|--cats.controller.ts
|--cats.module.ts
|--cats.service.ts
|--app.module.ts
|--main.ts
共享模块
在Nest中,默认情况下模块是单例,因此您可以轻松地在多个模块之间共享任何提供者程序的同一实例。
每个模块自动成为共享模块。创建后,任何模块都可以重用它。
假设我们想要在其他几个模块之间共享CatsService的实例。
为此,我们首先需要将将CatsService提供者程序添加到模块的exports数组中来导出它,如下所示:
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 {}
现在,任何导入CatsModule的模块都可以访问CatsService,并将于导入它的所有其他模块共享同一个实例。
模块再次导出
如上所示,模块可以导出其内部提供者程序。此外,他们还可以重新导出导入的模块。
在下面的示例中,CommonModule被导入到CoreModule中导出,使其可用于导入此模块的其他模块。
@Module({
imports: [CommonModule],
exports: [CommonModule],
})
export class CoreModule {}