异步 SSH 模块注册与测试
上一节实现了通过 name 参数注册多个同步 SSH 客户端。但在真实项目中,SSH 连接配置通常存储在 .env 文件或配置中心中,需要在运行时动态读取。本节将重构 forRoot 的代码结构,并实现 forRootAsync 异步注册方法,使 SSH 模块能通过 ConfigService 获取配置信息。
重构 forRoot 的 Provider 结构
首先优化 forRoot 方法中的重复代码,将 Provider 收集逻辑提取为数组,使代码更加简洁:
// ssh.module.ts
static forRoot(options: SshModuleOptions, name?: string): DynamicModule {
const sshClientProviders: Provider[] = [];
if (name) {
// Named provider with unique token
const providerName = `${name}:SSH`;
sshClientProviders.push({
provide: providerName,
useFactory: async () => {
const ssh = new SshService(options);
await ssh.connect();
return ssh;
},
});
} else {
// Default provider (no name)
sshClientProviders.push(SshService);
}
return {
module: SshModule,
providers: [
{
provide: name ? `${name}:SSH_OPTIONS` : 'SSH_OPTIONS',
useValue: options,
},
...sshClientProviders,
],
exports: [...sshClientProviders],
};
}
typescript
优化要点:
- 使用
sshClientProviders数组统一管理所有 Provider,通过扩展运算符展开到providers和exports中 name相关的 Provider Token 加上:SSH后缀,避免与其他模块的 Provider 命名冲突- 代码结构清晰,
if/else分支分别处理有名称和无名称的场景
使用 Symbol 替代字符串 Token
直接使用字符串作为 Provider Token 存在命名冲突的风险。更安全的做法是使用 Symbol 作为注入标识:
// ssh.constants.ts
export const SSH_OPTIONS = Symbol('SSH_OPTIONS');
typescript
在 Module 和 Service 中统一引用该 Symbol:
// ssh.module.ts
import { SSH_OPTIONS } from './ssh.constants';
providers: [
{
provide: SSH_OPTIONS,
useValue: options,
},
]
// ssh.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { SSH_OPTIONS } from './ssh.constants';
@Injectable()
export class SshService {
constructor(
@Inject(SSH_OPTIONS) private options: SshModuleOptions,
) {}
}
typescript
将 Symbol 定义在独立的 ssh.constants.ts 文件中,可以避免跨模块导入时的循环依赖问题。
实现 forRootAsync 异步注册
forRootAsync 方法支持通过工厂函数异步获取 SSH 配置,通常结合 ConfigService 从环境变量中读取:
// ssh.module.ts
interface SshModuleAsyncOptions {
useFactory: (...args: any[]) => Promise<SshModuleOptions> | SshModuleOptions;
inject?: any[];
name?: string;
}
static forRootAsync(asyncOptions: SshModuleAsyncOptions): DynamicModule {
const { useFactory, inject = [], name } = asyncOptions;
const asyncProvider: Provider = {
provide: name ? `${name}:SSH_OPTIONS` : SSH_OPTIONS,
useFactory,
inject,
};
const sshClientProviders: Provider[] = [];
if (name) {
sshClientProviders.push({
provide: `${name}:SSH`,
useFactory: async (options: SshModuleOptions) => {
const ssh = new SshService(options);
await ssh.connect();
return ssh;
},
inject: [name ? `${name}:SSH_OPTIONS` : SSH_OPTIONS],
});
} else {
sshClientProviders.push({
provide: SshService,
useFactory: async (options: SshModuleOptions) => {
const ssh = new SshService(options);
await ssh.connect();
return ssh;
},
inject: [SSH_OPTIONS],
});
}
return {
module: SshModule,
providers: [asyncProvider, ...sshClientProviders],
exports: [...sshClientProviders],
};
}
typescript
在业务模块中使用异步注册
结合 ConfigService,从 .env 文件中动态读取 SSH 连接配置:
// app.module.ts
import { ConfigService } from '@nestjs/config';
@Module({
imports: [
SshModule.forRootAsync({
useFactory: (configService: ConfigService) => ({
host: configService.get('SSH_HOST'),
port: configService.get('SSH_PORT'),
username: configService.get('SSH_USER'),
password: configService.get('SSH_PASSWORD'),
}),
inject: [ConfigService],
name: 'ubuntu',
}),
],
})
export class AppModule {}
typescript
在 Controller 中注入时,直接使用 SshService 类型(无需 @Inject):
// app.controller.ts
@Controller()
export class AppController {
constructor(private sshService: SshService) {}
@Get('test')
async test() {
return await this.sshService.exec('ls -la /tmp');
}
}
typescript
注入 ConfigService 获取更多配置
异步注册的优势在于可以在工厂函数中注入任意依赖。例如同时注入 ConfigService 获取 cron 开关、SSH 连接信息等:
SshModule.forRootAsync({
useFactory: (configService: ConfigService) => ({
host: configService.get('SSH_HOST'),
port: configService.get<number>('SSH_PORT'),
username: configService.get('SSH_USER'),
password: configService.get('SSH_PASSWORD'),
}),
inject: [ConfigService],
name: 'ubuntu',
}),
typescript
启动调试后,可以在控制台日志中看到 ConfigService 读取到的配置信息,验证了从 .env 文件中动态获取 SSH 配置的完整流程。
带名称的异步注册测试
当同时传递 name 参数时,Controller 需要使用 @Inject 注入指定名称的 Service:
@Controller()
export class AppController {
constructor(
@Inject('ubuntu:SSH') private sshService: SshService,
) {}
@Get('test')
async test() {
return await this.sshService.exec('ls -la /tmp');
}
}
typescript
测试流程:
- 启动调试进程,确认无编译错误
- 向
GET /test发起请求,验证返回/tmp目录的文件列表 - 多次请求不同名称的客户端,确认各自连接的独立性
本节总结
- 代码重构:将 Provider 收集逻辑统一为数组,使用扩展运算符简化
providers和exports的配置 - Symbol Token:使用
Symbol替代字符串作为注入标识,避免跨模块命名冲突 - 独立常量文件:将
SSH_OPTIONSSymbol 定义在ssh.constants.ts中,解决循环导入问题 - 异步注册:
forRootAsync方法支持通过useFactory和inject配合ConfigService动态获取配置 - 至此,SSH 模块的同步注册和异步注册两种方式均已实现并通过测试
↑