nestjs-module模块

268 阅读12分钟

前言

开发过程中我们的 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简介

引入 controllercontroller为我们的路由模块,主要用于分发路由给外部返回数据,一个模块可能功能很多,因此可能根据该模块功能不同分出来很多 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介绍

nestjs/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

nestjs/modules

正常的模块使用我们已经了解了,我们可能会看到一些三方库的模块导入不太一样,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,

这样就解决相互依赖的问题了

最后

之前看过文章的可能觉得没啥,看的比较晚的,由于后续还加了东西,会增加了不少知识基础,就那一句,收藏加关注,学习不迷路

思考是进步的源泉,思维的碰撞可以帮助我们更优雅的解决更多问题😂