Nest.js 持续学习之提供者、模块和中间件

387 阅读12分钟
前言:小编之前以一个新手的身份初步学习了 Nest.js 的基本概念及基本的接口编写,接下来我将继续深入学习相关知识,此篇推文作为笔记兼知识点分享,如有错误欢迎各位掘友在评论区批评指正,小编将不胜感激。在阅读之前,建议大家去扫一眼小编的 Nest.js 小白入门这篇再回头看这篇衔接起来会好一点,链接放在下面啦...

Nest.js 小白入门初体验

一. 提供者 Providers
1.1 概念:Providers 是 Nest 的一个基本概念。许多基本的 Nest 类都可能被视为 provider ,比如 service, repository, factory, helper 等等。 他们都可以通过 constructor 注入依赖关系。 这意味着对象可以彼此创建各种关系,并且“连接”对象实例的功能在很大程度上可以委托给 Nest 运行时系统。 Provider 只是一个用 @Injectable() 装饰器注释的类。

微信图片_20240609153611.png

在前面的章节中,我们已经创建了一个简单的控制器 CatsController 。控制器处理 HTTP 请求并将更复杂的任务委托给 providers。Providers 是纯粹的 JavaScript 类,在其类声明之前带有 @Injectable() 装饰器。
1.2 服务
现在,我们从创建一个简单的 CatsService 开始。该服务将负责数据存储和检索,其由 CatsController 使用,因此把它定义为 provider,是一个很好的选择。因此,我们用 @Injectable() 来装饰这个类 。如果我们要使用 CLI 创建服务类,只需执行 nest g service cats 命令。但是注意,它会在src下面创建一个cats文件夹,里面包含如下两个文件:1. cats.service.ts 2. cats.service.spec.ts 小编这里为了统一管理,将其移动到了如下目录中:

微信图片_20240609155244.png

当然,上面的服务调取了一个 Cat 接口,我们需要定义一下这个接口,后面进行增删改查时规定了接口数据所应该有的字段,具体实现如下:

微信图片_20240609160257.png

微信图片_20240609155620.png

现在我们有一个服务类来检索 cat ,让我们在 CatsController 里使用它 :
// 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();
  }
}
上面第10行体现的是依赖注入的思想,在 `Nest` 中,借助 **TypeScript** 功能,
管理依赖项非常容易,因为它们仅按类型进行解析。在下面的示例中,`Nest` 将 `catsService`
通过创建并返回一个实例来解析 `CatsService`(或者,在`单例`的正常情况下,如果现有实例已在
其他地方请求,则返回现有实例)。解析此依赖关系并将其传递给控制器的构造函数(或分配给指定的属性):
小编的代码实现如下:

微信图片_20240609160745.png

1.3 注册提供者
现在我们已经定义了提供者(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 {}
二. 模块
2.1 概念: 模块是具有 @Module() 装饰器的类。 @Module() 装饰器提供了元数据,Nest 用它来组织应用程序结构。

微信图片_20240609163011.png

@Module() 装饰器接受一个描述模块属性的对象:对象属性及其解释如下
providers由 Nest 注入器实例化的提供者,并且可以至少在整个模块中共享
controllers必须创建的一组控制器
imports导入模块的列表,这些模块导出了此模块中所需提供者
exports由本模块提供并应在其他模块中可用的提供者的子集。
2.2 功能模块
CatsController 和 CatsService 属于同一个应用程序域。 应该考虑将它们移动到一个功能模块下,即 CatsModule,与我们日常开发中的模块化思想相通,我们喜欢把单独负责某个模块的功能的逻辑代码放在一起,方便我们后期的维护和理解
代码实现demo如下,我们如果打算使用 CLI 创建模块,只需执行  nest g module cats 命令,小编为了遵循个人项目中的代码,就把创建好的模块移动到相关目录了,请继续看下面
// 执行 nest g module cats 之后 cats.module.ts 里面的代码如下
// 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 {}
小编具体的实现:

微信图片_20240609164120.png

现在,我们已经创建了 cats.module.ts 文件,并把与这个模块相关的所有东西都移到了 cats 目录下。我们需要做的最后一件事是将这个模块导入根模块 (AppModule)。

微信图片_20240609164839.png

微信图片_20240609165143.png

微信图片_20240609164843.png

现在 Nest 知道除了 AppModule 之外,注册 CatsModule 也是非常重要的。 这就是我们现在的目录结构:
src
├── cats
│    ├── dto
│    │   └── create-cat.dto.ts
│    ├── interfaces
│    │     └── cat.interface.ts
│    ├─ services
│    │     └── cats.service.ts
│    ├─ cats.controller.ts
│    └── cats.module.ts
├── app.module.ts
└── main.ts
2.3 共享模块
在 Nest 中,默认情况下,模块是单例,因此您可以轻松地在多个模块之间共享同一个提供者实例。

微信图片_20240609165643.png

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

现在,每个`导入` `CatsModule` 的模块都可以访问 `CatsService` ,并且它们将共享相同的 `CatsService` 实例。注意:导入才能使用哦,不然报错。
2.4 模块导出
模块可以导出他们的内部提供者。 而且,他们可以再导出自己导入的模块。

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

2.5 依赖注入
提供者也可以注入到模块(类)中(例如,用于配置目的):
// 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 {
  constructor(private readonly catsService: CatsService) {}
}

2.5 全局模块
有没有觉得2.4中模块导出每次都得先导入再使用有点麻烦,特别是同一个模块得在其他地方多次使用的时候,其实模块是可以在全局范围内注册的,一旦被定义,他们将到处可用。
@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 {}

2.6 动态模块
Nest 模块系统包括一个称为动态模块的强大功能。此功能使您可以轻松创建可自定义的模块,这些模块可以动态注册和配置提供程序。以下是一个动态模块定义的示例 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,
    };
  }
}

`forRoot()` 可以同步或异步(`Promise`)返回动态模块。
此模块 Connection 默认情况下(在 @Module() 装饰器元数据中)定义提供程序,但此外-根据传递给方法的 entities 和 options 对象 forRoot() -公开提供程序的集合,例如存储库。请注意,动态模块返回的属性扩展(而不是覆盖)@Module() 装饰器中定义的基本模块元数据。这就是从模块导出静态声明的 Connection 提供程序和动态生成的存储库提供程序的方式。
如果要在全局范围内注册动态模块,将 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 {}

如果要依次重新导出动态模块,则可以 forRoot() 在导出数组中省略方法调用:

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

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

三. 中间件
3.1 概念:中间件是在路由处理程序 之前 调用的函数。 中间件函数可以访问请求和响应对象,以及应用程序请求响应周期中的 next() 中间件函数。 next() 中间件函数通常由名为 next 的变量表示。

微信图片_20240609171932.png

Nest 中间件实际上等价于 Express 中间件。 下面是Express官方文档中所述的中间件功能:
  • 执行任何代码。
  • 对请求和响应对象进行更改。
  • 结束请求-响应周期。
  • 调用堆栈中的下一个中间件函数。
  • 如果当前的中间件函数没有结束请求-响应周期, 它必须调用 next() 将控制传递给下一个中间件函数。否则, 请求将被挂起。
我们可以在函数中或在具有 @Injectable() 装饰器的类中实现自定义 Nest 中间件。 这个类应该实现 NestMiddleware 接口, 而函数没有任何特殊的要求。 让我们首先使用类方法实现一个简单的中间件功能。
// logger.middleware.ts

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log('Request...');
    next();
  }
}

3.2 依赖注入
Nest中间件完全支持依赖注入。 就像提供者和控制器一样,它们能够注入属于同一模块的依赖项(通过 constructor )。
中间件不能在 @Module() 装饰器中列出。我们必须使用模块类的 configure() 方法来设置它们。包含中间件的模块必须实现 NestModule 接口。我们将 LoggerMiddleware 设置在 AppModule 层上。

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } 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)
      .forRoutes('cats');
  }
}

小编具体的项目实现:

微信图片_20240609172851.png

我们还可以在配置中间件时将包含路由路径的对象和请求方法传递给 forRoutes() 方法。我们为之前在CatsController 中定义的 /cats 路由处理程序设置了 LoggerMiddleware 。我们还可以在配置中间件时将包含路由路径的对象和请求方法传递给 forRoutes() 方法,从而进一步将中间件限制为特定的请求方法。在下面的示例中,请注意我们导入了 RequestMethod 来引用所需的请求方法类型。
// app.module.ts

import {
  Module,
  NestModule,
  MiddlewareConsumer,
  RequestMethod,
} from '@nestjs/common';
import { LoggerMiddleware } 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)
      .forRoutes({ path: 'cats', method: RequestMethod.GET });
  }
}

可以使用 `async/await`来实现 `configure()`方法的异步化(例如,可以在 `configure()`方法
体中等待异步操作的完成)。
3.3 中间件消费者
MiddlewareConsumer 是一个帮助类。它提供了几种内置方法来管理中间件。他们都可以被简单地链接起来。forRoutes() 可接受一个字符串、多个字符串、对象、一个控制器类甚至多个控制器类。在大多数情况下,我们只会传递一个由逗号分隔的控制器列表。以下是单个控制器的示例:
// app.module.ts

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

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes(CatsController);
  }
}

该 `apply()` 方法可以使用单个中间件,也可以使用多个参数来指定多个多个中间件。
有时我们想从应用中间件中排除某些路由。我们可以使用该 exclude() 方法轻松排除某些路由。此方法可以采用一个字符串,多个字符串或一个 RouteInfo 对象来标识要排除的路由,如下所示:

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

该 `exclude()` 方法使用 `path-to-regexp` 包,支持通配符参数。
3.4 函数式中间件
我们使用的 LoggerMiddleware 类非常简单。它没有成员,没有额外的方法,没有依赖关系,因此可以使用一个简单的函数代替,这种类型的中间件称为函数式中间件
// logger.middleware.ts

export function logger(req, res, next) {
  console.log(`Request...`);
  next();
};


现在在 `AppModule` 中使用它。
// app.module.ts

consumer
  .apply(logger)
  .forRoutes(CatsController);

3.5 多个中间件
如前所述,为了绑定顺序执行的多个中间件,我们可以在 apply() 方法内用逗号分隔它们。

consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);

3.6 全局中间件
如果我们想一次性将中间件绑定到每个注册路由,我们可以使用由 INestApplication 实例提供的 use()方法:

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

四.接口测试

微信图片_20240609174615.png

###### post 接口测试:

微信图片_20240609174837.png

###### get 接口测试:

微信图片_20240609174946.png

对比小编前一篇可知,增加CatsService服务之后,我们具有了数据存储功能,可以返回一个数组的data数据,存储我们之前 post 增加的数据
结语:到这里小编本次的分享就结束了,感谢各位掘友的阅读,哪里有误或者有什么更好的见解还请在评论区多多交流,小编的项目源码还是贴在下面,需要的小伙伴可以看一看,后续我还会继续完善这个Nest.js学习的项目的。再次感谢各位的阅读!!!

项目GitHub地址传送门