likes
comments
collection
share

一次讲清 Nest.js 的 Module 模块

作者站长头像
站长
· 阅读数 6

模块的概念就是家里面的不同房间。每个房间都有特定的用途和功能,比如厨房用于烹饪,卧室用于休息,书房用于工作或学习。这些房间包含了完成特定任务所需的所有工具和设施,同时也各自独立,互不干扰。

Nest.js 的模块就是一个个的房间,至少是一居吧, 所以Nest.js 也至少有一个模块就是 AppModule。每个模块它们各自封装了完成特定功能所需的控制器、服务和提供者。通过这样的组织方式,让代码更好维护和复用。

怎么建一个空房间呢,需要用 @Module({}) 标记一个普通的 class,那么这个 class 就可以被称为一个模块了。

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

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

@Controller('hello')
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

@Module({
  imports: [], // 这里可以导入其他模块
  controllers: [AppController], // 控制器列表
  providers: [AppService], // 提供者列表
  exports: [] // 这里可以导出提供者,以供其他模块使用
})
export class AppModule {}

如果你之前对 controller, provider 这些编程模式没有概念,不用慌,下面来解释一下模块里面这些信息。

imports

先从 Line 21 imports 说起吧,imports 主要为了模块功能之间的复用。也就是说 模块A 想用 模块B 的功能,那你 Module A 就可以 imports: [ModuleB]。这个场景很常见,比如你家有一个清洁间,里面放了一些扫帚,拖把之类的清洁工具。但是你的卧室也是需要清扫工具的,你又不想把工具放卧室,这时你就可以直接"导入"清扫间以便使用它里面的工具(service)。

以程序世界来举例的话,假设我们有一个 UsersModule 和一个 PostsModule。UsersModule 提供了一些处理用户的服务,而 PostsModule 需要使用这些服务来获取用户信息。在这种情况下,我们就可以在 PostsModule 中导入 UsersModule。这样,PostsModule 就可以使用 UsersModule 提供的所有服务了。

controllers

在 Nest.js 中,控制器(Controller)是处理路由请求的部分。也就是说房子里面的路线规划,去卧室该怎么走?去厨房该怎么走?去客厅该怎么走?怎么定义这个路径可以通过 @Get(),@Post(),@Put(),@Delete() 等装饰器来定义,装饰器的参数是路由的路径。

例如,你可能有一个名为 @Get('kitchen') 的房间,当用户访问 'kitchen' 路径时,他们可能会得到一些食物(即服务返回的数据),它通常依赖于服务(Service)来处理业务逻辑并返回响应。那服务就是下面的 providers。

providers

providers 是 Nest.js 中最基本的构建元素,非常广泛,它可以是一个类,一个值,一个方法。也就是意味着只要你写了任何一个东西,你想在本模块里“注入”(可以理解为引用),你就把它写在 providers 里。

举个例子:

provider 是 service 类型

在Nest.js中,一个模块中的服务A如果想要调用服务B的提供者,可以通过依赖注入(DI)来实现(后面单开一篇讲)。首先,需要确保服务B的提供者在同一模块中被注册,然后在服务A的构造函数中注入服务B的提供者,这么说太抽象了,这么说吧,你现在经营一个大保健会所,提供了 2 种服务: ServiceA 是洗脚,ServiceB 是采耳

import { ServiceA } from './a.service';
import { ServiceB } from './b.service';
@Module({
  providers: [ServiceA, ServiceB]
})
export class MyModule {}

这是 A 和 B 分别的实现

// a.service.ts
@Injectable()
export class ServiceA {
  doSomething(): string {
    return '洗脚';
  }
}

// b.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class ServiceB {
  doSomething(): string {
    return '采耳';
  }
}

现在 ServiceB 服务升级了,现在也可以洗脚了,但是 ServiceB 不需要自己服务,她只要让 ServiceA来洗脚就好了

import { Injectable } from '@nestjs/common';
import { ServiceA } from './a.service';
@Injectable()
export class ServiceB {
  constructor(private serviceA: ServiceA) {}

  doSomething(): string {
    this.serviceA.doSomething();
    return '洗脚'; 
  }
}

provider 的多种类型

provider 如果只是一个值,比如字符串,数字或者对象,共享的方式稍稍有那么一点不同, 那我们先写一个 是字符串的 provider, 这种静态数据用 useValue 声明。

{ 
    provide: 'TOOL', 
    useValue: '洗脚盆'
}

把这个 provider 放在 模块的 providers 种,然后我们开始使用service调用它。service 在调用这个 provider 时候只要注入这个 TOOL 的标识就好

@Injectable()
export class ServiceB {
  constructor( @Inject('TOOL') private tool) {}

  doSomething(): string {
    return '洗脚' + this.tool;
  }
}

上面的是静态数据,如果这个值是动态计算出来的该怎么办呢? 用 useFactory! 它提供了一个工厂函数,这个函数会在运行时被调用,其返回值会被作为依赖注入的值。这让你可以根据运行时的环境或者状态,动态地创建依赖注入的值。工厂函数也可以是异步的,也就是说,你可以在工厂函数中进行一些异步操作,比如

{
  provide: 'CONFIG',
  useFactory: async () => {
    const env = process.env.NODE_ENV;
    let config;

    if (env === 'production') {
      // 在生产环境中,我们可能需要从远程服务获取配置
      config = await fetchRemoteConfig();
    } else {
      // 在其他环境中,我们可能只需要使用本地文件中的配置
      config = require('./config.development.json');
    }

    return config;
  },
}

在这个例子中,useFactory 是一个异步函数,它将在运行时被 Nest.js 调用。根据 NODE_ENV 的值,我们可能会从远程服务获取配置,或者直接使用本地文件中的配置。当 useFactory 返回(或者 Promise resolve)时,其返回值将被作为 CONFIG 提供者的值。

这样,我们就可以在代码中注入并使用 CONFIG 提供者,而不需要关心配置是如何获取的,也不需要关心配置可能会根据环境而变化。

再举一个实际的例子, 比如我们写了一个 数据库连接服务 DatabaseService, 提供了对数据库进行检索的能力。

import * as dbLibrary from 'your-db-library'; 

export class DatabaseService {
  private dbConnection: any;

  constructor() {
    const dbConfig = { /* 你的数据库配置 */ };
    this.dbConnection = dbLibrary.createConnection(dbConfig); // 创建数据库连接
  }
}

如果按照上面这样写代码,就会有个致命问题:如果你将这个 service 提供给别人, 别人怎么传他自己的数据库参数来连接数据库。

虽然我们可以找个中介,比如环境变量,我们和调用者约定好,让他写到环境变量里,然后程序直接读这个环境变量,但其实不够友好。

正常的该怎么做呢?我们可以自定义一个 provider,让用户来填数据库连接参数

{
  provide: 'DATABASE_CONNECTION',
  useFactory: async () => {
    const connection = await typeorm.createConnection({
      host: '10.1.2.5',
      port: 4000,
      username: 'max',
      password: '123@3@',
    });
    return connection;
  },
}

刚才我们用了 useValueuseFactory,还有另一个非常常用的叫做 useClass,它是干嘛的呢?在Nest.js中,useClass和直接使用service注入都是在依赖注入系统中创建对象实例。两者的主要区别在于使用场景和灵活性。

当你直接使用service注入时,你是在告诉Nest.js的依赖注入系统,你需要一个特定类型的实例。Nest.js会自动创建这个类型的实例(如果还没有创建的话),并且将其注入到需要它的地方。这是最简单,也是最常见的使用方式。

@Module({
  providers: [DatabaseService], // 直接使用service注入
})
export class AppModule {}

但是上面代码有局限性: 这个 service 的类型已经被确定了 如果现在来了一个场景: 当你需要创建多个不同配置的同一类型实例,比如创建数据库,你直接使用 service,你只能创建一个 DatabaseService 实例,因此只能创建一个数据库连接。但是,如果你使用 useClass,你可以为每个数据库配置创建一个单独的提供者,每个提供者都会创建一个 DatabaseService 的新实例,并根据相应的数据库配置创建一个数据库连接。

{
  provide: 'DATABASE_CONNECTION_1',
  useClass: DatabaseService,
  inject: ['DATABASE_CONFIG_1'],
},
{
  provide: 'DATABASE_CONNECTION_2',
  useClass: DatabaseService,
  inject: ['DATABASE_CONFIG_2'],
}

在这个示例中,我们为两个数据库配置 DATABASE_CONFIG_1DATABASE_CONFIG_2 分别创建了两个提供者 DATABASE_CONNECTION_1DATABASE_CONNECTION_2。每个提供者都会创建一个 DatabaseService 的新实例,并根据相应的数据库配置创建一个数据库连接。

然后,你可以在你的控制器或服务中注入这两个提供者,并且使用它们来访问两个不同的数据库连接。例如:

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

@Injectable()
export class SomeService {
  constructor(
    @Inject('DATABASE_CONNECTION_1') private dbConnection1: DatabaseService,
    @Inject('DATABASE_CONNECTION_2') private dbConnection2: DatabaseService,
  ) {}

  // 这里你可以通过 `this.dbConnection1` 和 `this.dbConnection2` 来访问两个不同的数据库连接
}

在上述代码中,DatabaseService 实例将被注入到 SomeService 服务中,并且可以在类的任何地方使用 this.dbConnection1this.dbConnection2 来访问两个不同的数据库连接。

需要注意的是,无论是使用useClass还是直接使用service注入,Nest.js都会确保在整个应用中,同一类型的service或者同一提供者令牌的提供者,只会创建一个实例(单例模式)。也就是说,无论在哪里注入这个service或者提供者,你都会得到同一个实例。

exports

在模块中,声明在 providers 里的工具只能自己模块使用,那么如果想给其他模块使用怎么办呢? exports 属性是 @Module() 装饰器选项中的一部分,它定义了一组提供者,这些提供者应该是公开的,可以被其他模块导入和使用。

例如,假设我们有一个UsersService,它在UsersModule中定义,并且我们希望在OrdersModule中使用它。这就需要在UsersModule中导出UsersService

@Module({
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

然后,在OrdersModule中,我们就可以注入并使用UsersService了:

@Module({
  imports: [UsersModule],
  providers: [OrdersService],
})
export class OrdersModule {
  constructor(private usersService: UsersService) {}
}

在这个例子中,exports使我们能够跨模块共享UsersService

总的来说,Nest.js 模块的机制允许我们以一种组织良好和可维护的方式来编写代码。通过模块,我们可以将相关的功能组织在一起,并通过依赖注入来共享服务。

希望这篇文章能帮助你理解和使用 Nest.js 的模块功能,快去设计你应用的“房间”吧。

转载自:https://juejin.cn/post/7330159032991154212
评论
请登录