模块的概念就是家里面的不同房间。每个房间都有特定的用途和功能,比如厨房用于烹饪,卧室用于休息,书房用于工作或学习。这些房间包含了完成特定任务所需的所有工具和设施,同时也各自独立,互不干扰。
Nest.js 的模块就是一个个的房间,至少是一居吧, 所以Nest.js 也至少有一个模块就是 AppModule。每个模块它们各自封装了完成特定功能所需的控制器、服务和提供者。通过这样的组织方式,让代码更好维护和复用。
怎么建一个空房间呢,需要用 @Module({})
标记一个普通的 class,那么这个 class 就可以被称为一个模块了。
js
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 是采耳
js
import { ServiceA } from './a.service';
import { ServiceB } from './b.service';
@Module({
providers: [ServiceA, ServiceB]
})
export class MyModule {}
这是 A 和 B 分别的实现
js
// 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来洗脚就好了
js
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
声明。
js
{
provide: 'TOOL',
useValue: '洗脚盆'
}
把这个 provider 放在 模块的 providers 种,然后我们开始使用service调用它。service 在调用这个 provider 时候只要注入这个 TOOL
的标识就好
js
@Injectable()
export class ServiceB {
constructor( @Inject('TOOL') private tool) {}
doSomething(): string {
return '洗脚' + this.tool;
}
}
上面的是静态数据,如果这个值是动态计算出来的该怎么办呢? 用 useFactory
! 它提供了一个工厂函数,这个函数会在运行时被调用,其返回值会被作为依赖注入的值。这让你可以根据运行时的环境或者状态,动态地创建依赖注入的值。工厂函数也可以是异步的,也就是说,你可以在工厂函数中进行一些异步操作,比如
js
{
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
, 提供了对数据库进行检索的能力。
js
import * as dbLibrary from 'your-db-library';
export class DatabaseService {
private dbConnection: any;
constructor() {
const dbConfig = { /* 你的数据库配置 */ };
this.dbConnection = dbLibrary.createConnection(dbConfig); // 创建数据库连接
}
}
如果按照上面这样写代码,就会有个致命问题:如果你将这个 service 提供给别人, 别人怎么传他自己的数据库参数来连接数据库。
虽然我们可以找个中介,比如环境变量,我们和调用者约定好,让他写到环境变量里,然后程序直接读这个环境变量,但其实不够友好。
正常的该怎么做呢?我们可以自定义一个 provider,让用户来填数据库连接参数
js
{
provide: 'DATABASE_CONNECTION',
useFactory: async () => {
const connection = await typeorm.createConnection({
host: '10.1.2.5',
port: 4000,
username: 'max',
password: '123@3@',
});
return connection;
},
}
刚才我们用了 useValue
和 useFactory
,还有另一个非常常用的叫做 useClass
,它是干嘛的呢?在Nest.js中,useClass和直接使用service注入都是在依赖注入系统中创建对象实例。两者的主要区别在于使用场景和灵活性。
当你直接使用service注入时,你是在告诉Nest.js的依赖注入系统,你需要一个特定类型的实例。Nest.js会自动创建这个类型的实例(如果还没有创建的话),并且将其注入到需要它的地方。这是最简单,也是最常见的使用方式。
js
@Module({
providers: [DatabaseService], // 直接使用service注入
})
export class AppModule {}
但是上面代码有局限性: 这个 service 的类型已经被确定了 如果现在来了一个场景: 当你需要创建多个不同配置的同一类型实例,比如创建数据库,你直接使用 service,你只能创建一个 DatabaseService
实例,因此只能创建一个数据库连接。但是,如果你使用 useClass
,你可以为每个数据库配置创建一个单独的提供者,每个提供者都会创建一个 DatabaseService
的新实例,并根据相应的数据库配置创建一个数据库连接。
js
{
provide: 'DATABASE_CONNECTION_1',
useClass: DatabaseService,
inject: ['DATABASE_CONFIG_1'],
},
{
provide: 'DATABASE_CONNECTION_2',
useClass: DatabaseService,
inject: ['DATABASE_CONFIG_2'],
}
在这个示例中,我们为两个数据库配置 DATABASE_CONFIG_1
和 DATABASE_CONFIG_2
分别创建了两个提供者 DATABASE_CONNECTION_1
和 DATABASE_CONNECTION_2
。每个提供者都会创建一个 DatabaseService
的新实例,并根据相应的数据库配置创建一个数据库连接。
然后,你可以在你的控制器或服务中注入这两个提供者,并且使用它们来访问两个不同的数据库连接。例如:
js
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.dbConnection1
和 this.dbConnection2
来访问两个不同的数据库连接。
需要注意的是,无论是使用useClass还是直接使用service注入,Nest.js都会确保在整个应用中,同一类型的service或者同一提供者令牌的提供者,只会创建一个实例(单例模式)。也就是说,无论在哪里注入这个service或者提供者,你都会得到同一个实例。
exports
在模块中,声明在 providers 里的工具只能自己模块使用,那么如果想给其他模块使用怎么办呢? exports 属性是 @Module() 装饰器选项中的一部分,它定义了一组提供者,这些提供者应该是公开的,可以被其他模块导入和使用。
例如,假设我们有一个UsersService
,它在UsersModule
中定义,并且我们希望在OrdersModule
中使用它。这就需要在UsersModule
中导出UsersService
:
kotlin
@Module({
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
然后,在OrdersModule
中,我们就可以注入并使用UsersService
了:
kotlin
@Module({
imports: [UsersModule],
providers: [OrdersService],
})
export class OrdersModule {
constructor(private usersService: UsersService) {}
}
在这个例子中,exports
使我们能够跨模块共享UsersService
。
总的来说,Nest.js 模块的机制允许我们以一种组织良好和可维护的方式来编写代码。通过模块,我们可以将相关的功能组织在一起,并通过依赖注入来共享服务。
希望这篇文章能帮助你理解和使用 Nest.js 的模块功能,快去设计你应用的"房间"吧。