前言
开发过程中我们的 Module 文件包含着我们模块的信息,其用来配置我们这个模块需要用到的东西和对外导出的功能,如果单纯的开发,可能我们就导入一个数据库就行了,如果功能复杂的模块则可能模块之间有交互呢,那么我们怎么配置和使用,下面简单讲解一下(已扩展了更详细的信息
)
ps
:这是后续补充的,除了简单使用操作,后续还加入了 provider 更详细的介绍,以及module模块的详细操作
module 使用简介
案例
下面就给出一个案例,就是常用的一些导入了,我们简单介绍回顾下
@Module({
//导入其他模块
imports: [
TypeOrmModule.forFeature([User]),
TypeOrmModule.forFeature([File]),
],
//导入该模块需要的控制器,包括其他模块的控制器
controllers: [UserController],
//导入该模块需要的service
providers: [UserService, FileService, MinioService],
//该模块有外部需要用到的服务,对外导出,否则外部不能正常使用
exports: [TemplateService],
})
imports简介
imports
用于导入用到的其他模块,常见的就是数据库、jwt 等模块等,也可以导入我们自己的模块,这样就不用导入该模块的引用的 service
了(当然建议 service
一个一个引入,能看出来该模块与那些具体服务耦合)
imports: [
TypeOrmModule.forFeature([User]),
TypeOrmModule.forFeature([Auth]),
JwtModule.register({
global: true, //设置为全局
secret: envConfig.secret,
signOptions: {
expiresIn: '7d', //失效时长设置为7天
},
}),
//引用某个模块
ArticleModule,
],
controllers简介
引入 controller
,controller
为我们的路由模块,主要用于分发路由给外部返回数据,一个模块可能功能很多,因此可能根据该模块功能不同分出来很多 controller
,需要将我们用到的 controller
全部引入,不然该 controller
会不生效
例如
:我们的 user 模块有基础功能增删改查(UserController),后面还增加了,同类用户不同用户之间的各种业务关系操作(UserRelationController)
controllers: [UserController, UserRelationController],
providers简介
providers
就是我们使用的 servie
,每个 controller
都会对应一个至多个 service
,其主要用来编写我们的业务服务,如果我们用到其他模块的 service
, 需要在这里加入其 service
(其他模块要 exports导出),以便于调用其他模块的方法
并且里面还支持我们配置校验 Guard
的格式,后面后详细介绍
providers: [
UserService,
AuthService,
ArticleService,
{
provide: APP_GUARD,
useClass: UserGuard,
},
],
exports简介
看名字就知道对外导出,外部模块用到本模块的 service 时,会在其模块的 providers 中加入本模块 servier,如果本模块没有对外导出 exports 的话,那么就会调用失败(最简单的报错),因此我们的 service 如果有对外使用的模块,需要将该 service 模块导出
exports: [
FileService,
FileExService,
MinioService,
RedisService,
]
service之间调用
写后端时,也会碰到多个服务之间的交互前面也介绍了,本模块要用到其他 service,首先其他 service 需要 exports 其 service,然后本模块引用其 service,然后调用其写好的方法即可
此外,就需要讨论他们之间的互相调用的代码形式怎么写了,返回成功失败数据时怎么返回呢,一个就当一段正常执行的内部代码,还有一个调用外部吧别的模块当做一个微服务,像访问接口一样访问
//ArticleService
//假设外部需要调用我们的 ArticleService,调用我们的 find 方法查找该文章是否存在
//如果我们文章存在假删除、是否允许查看,那么我们外部调用时就可能需要同时where对比这些参数
//这样外部使用时基础功能时,会引入一些不方便更改的代码(假设我们又加入、删除一个过滤参数,外部还得一个一个找)
//如果这类代码写到本 service 至少维护会好很多,因此可能会需要用到对外导入
//当然也会有些创建等操作也可以
//假设外部需要调用查找
find(id: string) {
return this.ArticleRepository.findOneBy({
id,
is_allow: 1,
usable: 1,
})
}
//假设外部需要调用创建
create() {
const article = new Article()
...
return this.ArticleRepository.save(article)
}
认读度比较高的交互方案
对于简单查询
就要一个结果的,直接就返回混合类型,成功返回查询对象,失败返回null,一般不需要返回错误字样,需要返回错误,就是谁用谁写
对于比较复杂的操作
,那就成功直接返回,失败抛出异常,并在异常中返回自己的错误字样(可以自定义错误),外部使用的时候成功就往后走,失败就在捕获异常中直接return结束即可
对于后端返回错误message
的,有些没有特殊要求,直接根据自己页面功能返回就行,有些觉得需要统一,返回字段可以统一声明到一块,外部统一使用对应字段
也有嫌麻烦的,直接以微服务的方式包裹整个功能,就跟掉接口一样获取成功或者失败的数据,只要团队认可,那么规则定下来,大家一起遵守,那么都不是问题😂
依赖注入基础
依赖注入是一种控制反转(IOC
)技术,将依赖项的实例化委托给IOC
容器
那和传统的依赖注入有什么区别呢,传统依赖,谁使用对象谁去创建一个对象,而使用了 IOC
之后,对象的创建、获取都是通过 IOC
,根据设置依赖,IOC容器
会自动创建对象,获取时,会从 IOC 容器查找获取,就这么简单,实际上也没那么高大上,就是多了个管理员
罢了
当然这个模式是默认的,也就是注入到一个容器中的可以全局使用,如果想改作用域的,可以参考这里,一般不需要
举个例子:
使用 Injectable 标记为提供者 provider
@Injectable()
export class UserService {
}
注入 provider 到我们的控制器汇总
@Controller('user')
export class UserController {
constructor(private userService: UserService) {}
}
将 provider 注册到我们的容器当中,当初始化我们的控制器时,会检查所有依赖,会按照标记和注入创建 provider 并缓存,别的地方需要时,仍然可以从缓存中获取已有的实例,这也是一个简化版流程
@Module({
controllers: [UserController],
providers: [UserService],
})
provider介绍
useClass
provider 提供者,也就是我们常见的service,我们写起来很简单,实际上,他还有一个全称,如下所示
//平时这么写
providers: [UserService];
//实际上是两个属性, provide 令牌、useClass 都是 provider 本身
providers: [
{
provide: UserService,
useClass: UserService,
},
];
//甚至可能会出现根据不同环境使用不同配置的情况
providers: [
{
provide: ConfigService,
useClass:
process.env.NODE_ENV === 'development'
? DevelopmentConfigService
: ProductionConfigService,
}
useValue
除了上面的写法,我们可能还会碰到自定义 provider,我们可能需要接收保存一些外部常量等信息,因此会使用到 useValue
,(有人可能会觉得根本用不到呀,有些场景就是可能会用到,了解就好,像上面的常量参数就可以使用这种方式)
const CustomConfig = {
host: ''
}
export type CustomServiceType = typeof CustomService;
providers: [
{
provide: 'file_custom',
useValue: CustomConfig,
},
],
constructor(
@Inject('file_custom') customServce: CustomServiceType,
) {
console.log(customServce);
}
useFactory
除了上面的,有时候我们还会看到别人使用的 useFactory
,这个也类似,我们可以通过 useFactory
初始化返回用到的数据,可以通过 inject
获取到内容
//这里我们设置标识,方便其他地方使用
export const redis_provide_identifier = 'REDIS_CLIENT'
//更加方便外部使用
export const InjectMyRedis = () => Inject(redis_provide_identifier);
//设置provider
export const RedisProvider = {
provide: redis_provide_identifier,
async useFactory() {
const client = createClient({
socket: {
host: envConfig.REDIS_HOST,
port: Number(envConfig.REDIS_PORT),
},
});
await client.connect();
return client;
},
};
@InjectMyRedis() private readonly redisClient: RedisClientType,
注入案例
看到上面案例,和声明的带令牌的 provider简介,可能会有点想法了,通过 Inject + 标识
就可以获取到我们 useClass、useValue、useFactory 中的内容,只不过 useClass 的一般我们直接使用类名,直接就可以拿到,其他两个需要使用 Inject + 标识
获取
我们注册三种类型,例如:
//module
providers: [
UserService,
{
provide: 'file_custom',
useValue: CustomConfig,
},
{
provide: 'factory',
async useFactory() {
const client = createClient({
socket: {
host: envConfig.REDIS_HOST,
port: Number(envConfig.REDIS_PORT),
},
});
await client.connect();
return client;
},
}
],
//service,注入三种
constructor(
private minioService: UserService,
@Inject('file_custom') customServce: CustomConfigType,
@Inject('factory') redis: RedisClientType,
//@InjectMyRedis() private readonly redis: RedisClientType, //这个是简单封装了一下,实际和和上面是一样,避免使用标识了
) {}
异步提供者
有时,应用程序的启动应该被延迟,直到完成一个或多个异步任务。例如,在与数据库建立连接之前,您可能不希望开始接受请求,这种情况下就可以使用 useFactory 的 异步功能,如下所示(当然这里和三方模块的看着不一样,后面会介绍模块)
{
provide: 'factory',
useFactory: async () => {
const connection = await createConnection(options);
return connection;
},
}
动态模块module
正常的模块使用我们已经了解了,我们可能会看到一些三方库的模块导入不太一样,register、forRoot、forFeature 等,下面我们会介绍一些
如下所示,我们给我们自己的模块设置一个 register方法,可以方便我们的模块动态从外部获取参数,如下所示,这样动态注册该模块时,能够称心使用了
//.module.ts
export type ConfigType = {
key: string;
secret: string;
};
@Module({})
export class ConfigModule {
//这种配置我们一般在不同模块配置,然后就可以直接在导入的模块使用了
static register(config: ConfigType) {
//也可以根据参数,返回不同的 provider等信息
return {
global: true, //全局
module: ConfigModule,
controllers: [ConfigController],
providers: [
{
provide: 'CONFIG_OPTIONS',
useValue: config,
},
ConfigService,
],
exports: [ConfigService],
};
}
}
//.service.ts
constructor(@Inject('CONFIG_OPTIONS') private config: ConfigType) {
console.log(this.config);
}
//导入模块,像使用jwt一样注册使用
ConfigModule.register({
key: 'config-key',
secret: 'config-secret',
}),
按照上面的做,会发现,如果直接导入该模块
使用,那么将会使用同一组对象
,毕竟他们是同一组容器,如果在另一个模块重新初始化
(使用 register 注册),发现该模块可以使用当前模块注册
的数据,他们互相分离,相当于是另外一个容器
模块引用
上面的基本上就够使用了,可是我们是不会满足的
我们会看到一些三方,我们在 app 中注册了一次
,里面的一些服务,甚至不用直接导入我们的 module、service,可以直接注入使用
,这对于我们的一些通用模块来说,那真是再好不过了,这就是模块引用
接下来我们我们介绍下怎么实现和使用的,我们在自己的 provider 中声明我们用到的服务
,初始化 moduleRef
,然后实现 OnModuleInit
接口,最后通过 moduleRef.get
获取我们所需的 provider
,这样就获取到了(有人可能会觉得,我直接导入,不比这个舒服吗,确实是没错,但是可能会导致更多的模块依赖
,甚至会出现循环依赖
)
@Injectable()
export class ConfigService implements OnModuleInit {
redis: RedisService;
constructor(
private moduleRef: ModuleRef,
) {}
onModuleInit() {
this.redis = this.moduleRef.get(RedisService, { strict: false });
}
}
ps
:需要注意的是,如果用到的那个服务,在自己模块都没有注册,那么是获得不到的话(正常写到自己的 providers 中即可)
模块全局导入
有时候也会觉得上面的模块引用
不太好用,也不太通用,很不舒服,也可以使用模块全局导入
的方式,注册完模块,直接注入使用即可
不管是我们默认的静态模块、动态模块,如果我们想不在其他模块导入,就想直接 inject
注入,然后获取到对应的 provider,只需要将该模块设置成 Global
即可,如下所示
//加上该参数即可,某些三方就是这样,里面的参数用着很方便
@Global()
@Module({})
export class ConfigModule {
//这种配置我们一般在不同模块配置,然后就可以直接在导入的模块使用了
static register(config: ConfigType): DynamicModule {
return {
module: ConfigModule,
providers: [
{
provide: 'CONFIG_OPTIONS',
useValue: config,
},
ConfigService,
// RedisService,
],
exports: [ConfigService],
};
}
}
导入的话根据注册的服务来即可
private configService: ConfigService,
@Inject("identifier") private service: Service,
循环依赖
前面讲到了不同 service 中的引用,也就是一个 service 依赖于另一个 service,通过 export + provider
的方式引入,这种方式对于单向依赖
很好用(例如: A依赖B,C依赖B, Y依赖X,Y依赖Z),但是对于相互依赖
就不行了(A依赖B,B依赖A),会出现循环依赖,报错,虽然我们可以尽可能绕过
模块之间的相互依赖,但也不可能全部避免,甚至有人会因此引出第三个模块作为中间模块,那就很麻烦,实际上,nestjs 早就为我们准备好了方案,即通过 @Inject() + forwardRef()
方式解决
一个模块内的 service 出现了相互依赖,例如 UserService 和 Service 相互依赖
//auth.service.ts
@Inject(forwardRef(() => UserService)) private userService: UserService,
//user.service.ts
@Inject(forwardRef(() => AuthService)) private authService: AuthService,
如果出现两个模块之间的 service 相互引用,即:A模块 引用了 B模块 的某个service,B模块 引用了 A模块的某个service。
即便是两个模块的service不是和互相和对方的service相互引用,但由于模块间相互导入,也会报错,此时将模块导入也加上 forwardRef()
,于此同时,我们的 Providers 不要导入另一个模块的 Service
了
//user模块
//user.module.ts
forwardRef(() => OrderModule),
//user.service.ts
@Inject(forwardRef(() => OrderService)) private orderService: OrderService,
//order模块
//order.module.ts
forwardRef(() => UserModule),
//order.service.ts
@Inject(forwardRef(() => UserService)) private userService: UserService,
这样就解决相互依赖的问题了
最后
之前看过文章的可能觉得没啥,看的比较晚的,由于后续还加了东西,会增加了不少知识基础,就那一句,收藏加关注,学习不迷路
哈
思考是进步的源泉,思维的碰撞可以帮助我们更优雅的解决更多问题😂