一个完整的 func 应用通常由许多相互协作的对象组成:Command 接收用户输入,service 处理业务,repository 访问数据,client 连接外部系统。当一个对象需要另一个对象才能完成工作时,它们就构成了依赖关系。随着应用增长,如果每个对象都自行创建和组织依赖,业务逻辑很快就会与具体实现和初始化过程混在一起。
依赖注入(Dependency Injection,简称 DI)将依赖的创建与使用分开。在 func 中,Command 或 service 只需声明自己依赖什么,框架会查找对应的 Provider、创建实例并将它传入;模块则负责登记当前功能拥有的 Commands 和 Providers,并确定它们之间的依赖关系。这样,业务对象可以专注于使用能力,而不必了解这些能力之间的复杂依赖关系。
DI 基础
我们通过一个简化的项目命令了解 DI 如何工作。
1. 定义 Provider
ProjectService 是我们假定的业务逻辑服务,@Injectable() 表示这个 class 可以由 func 管理:
import { Injectable } from 'func'
@Injectable()
export class ProjectService {
list() {
return ['func']
}
}Service 是 Provider 最常见的形式。Repository、client 和其他可复用能力也可以成为 Provider。
2. 声明依赖
随后我们认为,ProjectCommand 需要调用此业务逻辑,随即我们在 ProjectCommand 中声明依赖即可:
import { Command, Handler } from 'func'
import { ProjectService } from '../services/project.service'
@Command('project')
export class ProjectCommand {
constructor(private readonly projects: ProjectService) {}
@Handler()
list() {
console.log(this.projects.list())
}
}ProjectCommand 不需要创建 service。构造函数参数已经说明:运行这个 Command 需要一个 ProjectService。
3. 注册 Provider
最后,在模块中注册 Command 和 Provider:
import { Module } from 'func'
import { ProjectCommand } from './commands/project.command'
import { ProjectService } from './services/project.service'
@Module({
commands: [ProjectCommand],
providers: [ProjectService],
})
export class ProjectModule {}commands 表示这个 Module 提供的 Commands,providers 表示其中可以被注入的依赖。现在 func 已经知道如何创建 ProjectCommand 需要的 ProjectService。
这个过程包含三个关键步骤:
@Injectable()将ProjectService标记为可管理的 class;ProjectCommand通过构造函数请求ProjectService;ProjectModule将ProjectService注册到providers。
创建 ProjectCommand 时,func 会使用 ProjectService 这个 token 查找 Provider,创建实例并传入构造函数。如果 service 还有其他依赖,func 会继续解析它们。这样的自动化依赖分析在拥有复杂依赖关系的大型应用中格外有用。
标准 Provider
最常用的注册方式是直接写入 class:
providers: [ProjectService]这是下面写法的简写:
providers: [
{
provide: ProjectService,
useClass: ProjectService,
},
]provide 是查找依赖时使用的 token,useClass 表示这个 token 应当得到哪个 class 的实例。在标准写法中,class 同时充当 token 和实现。
标准 Provider 已经能满足大多数 service。需要提供配置值、替换实现或动态创建对象时,可以使用自定义 Provider。
自定义 Provider
自定义 Provider 仍然包含 provide,但可以选择不同的提供方式:
useValue直接提供一个值;useClass指定一个 class 实现;useFactory使用函数创建结果。
Value Provider:useValue
useValue 适合配置、常量、已有对象和测试替身:
import { createToken } from 'func'
export const API_URL = createToken<string>('API_URL')
export const apiUrlProvider = {
provide: API_URL,
useValue: 'https://api.example.com',
}把 apiUrlProvider 放入 Module 的 providers 后,请求 API_URL 就会得到 'https://api.example.com':
@Module({
providers: [apiUrlProvider],
})
export class ProjectModule {}useValue 返回注册时给出的原值,不会调用构造函数。若传入对象,同一个对象会在不同调用之间继续使用。
非 class token
Class 可以直接作为 token,但 TypeScript interface、字符串配置等内容在运行时没有可用的 class。func 使用 createToken<T>() 表示这类依赖:
export const API_URL = createToken<string>('API_URL')使用非 class token 时,通过 @Inject() 指定它:
import { Inject, Injectable } from 'func'
import { API_URL } from '../providers/api-url.provider'
@Injectable()
export class ProjectClient {
constructor(@Inject(API_URL) readonly apiUrl: string) {}
}Token 应定义为常量并在注册处与使用处导入。同样的描述文字不会让两个 createToken() 变成同一个 token。
Class Provider:useClass
useClass 可以让一个 token 对应具体的 class 实现:
import { Injectable, Module, createToken } from 'func'
interface ProjectStorage {
findAll(): string[]
}
export const PROJECT_STORAGE =
createToken<ProjectStorage>('PROJECT_STORAGE')
@Injectable()
class FileProjectStorage implements ProjectStorage {
findAll() {
return ['func']
}
}
export const projectStorageProvider = {
provide: PROJECT_STORAGE,
useClass: FileProjectStorage,
}
@Module({
providers: [projectStorageProvider],
})
export class ProjectModule {}业务代码可以依赖 PROJECT_STORAGE,而不必知道当前使用 FileProjectStorage。以后切换到内存或远程实现时,只需要替换 Provider 配置。
useClass 指定的 class 需要使用 @Injectable()。
Factory Provider:useFactory
当依赖需要根据配置或其他 Providers 创建时,可以使用 factory:
import { Module } from 'func'
import { API_URL, apiUrlProvider } from './providers/api-url.provider'
class ProjectClient {
constructor(readonly apiUrl: string) {}
}
export const projectClientProvider = {
provide: ProjectClient,
inject: [API_URL],
useFactory: (apiUrl: string) => new ProjectClient(apiUrl),
}
@Module({
providers: [apiUrlProvider, projectClientProvider],
})
export class ProjectModule {}inject 中的 tokens 会按顺序解析,并传给 useFactory 的参数。Factory 可以直接返回结果,也可以返回 Promise。
Provider 不只用于 service。Factory 可以返回 client、配置对象、函数或任何业务需要的值。
Module 组织依赖
Module 是带有 @Module() 的 class。它将 Commands 和 Providers 组织为一项功能,并决定哪些能力可以被其他 Modules 使用。
| 字段 | 用途 |
|---|---|
commands | 当前 Module 提供的 Commands。 |
providers | 当前 Module 中可以注入的 Providers。 |
imports | 当前 Module 使用的其他 Modules。 |
exports | 允许其他 Modules 使用的 Provider tokens。 |
options | 多个 Commands 共享的字段或动作。 |
Module class 通常保持为空。一个简单 Module 只需要 commands 和 providers,不必声明没有使用的字段。
在 Modules 之间共享 Provider
Provider 默认只在声明它的 Module 中可见。如果另一个功能需要 ProjectService,ProjectModule 必须将它导出:
import { Module } from 'func'
import { ProjectCommand } from './commands/project.command'
import { ProjectService } from './services/project.service'
@Module({
commands: [ProjectCommand],
providers: [ProjectService],
exports: [ProjectService],
})
export class ProjectModule {}然后由使用方导入 ProjectModule:
import { Injectable, Module } from 'func'
import { ReleaseCommand } from './commands/release.command'
import { ProjectModule } from '../projects/project.module'
import { ProjectService } from '../projects/services/project.service'
@Injectable()
class ReleaseService {
constructor(private readonly projects: ProjectService) {}
}
@Module({
imports: [ProjectModule],
commands: [ReleaseCommand],
providers: [ReleaseService],
})
export class ReleaseModule {}现在 ReleaseService 可以注入 ProjectService。
exports 是 Module 对外提供的能力。只在项目功能内部使用的 repository、client 和配置应保持私有。文件中的 export class 只允许 TypeScript import;Module.exports 才允许其他 Modules 通过 DI 使用它。
自定义 Provider 通过 token 导出:
@Module({
providers: [apiUrlProvider],
exports: [API_URL],
})
export class ProjectModule {}组合应用
每个应用至少有一个根模块。根模块通常只负责组合功能:
import { Module } from 'func'
import { HelpOption } from 'func/help'
import { ProjectModule } from './projects/project.module'
import { ReleaseModule } from './releases/release.module'
@Module({
imports: [ProjectModule, ReleaseModule],
options: [HelpOption],
})
export class AppModule {}导入 ProjectModule 和 ReleaseModule 后,它们的 Commands 都会加入应用,根模块不需要重新注册这些 Commands 和 Providers。
部分通用功能需要在导入时接收配置。它们通常提供 register() 方法:
import { Module } from 'func'
import { LogModule } from 'func/log'
import { ReleaseCommand } from './commands/release.command'
import { ReleaseService } from './services/release.service'
@Module({
imports: [LogModule.register()],
commands: [ReleaseCommand],
providers: [ReleaseService],
})
export class ReleaseModule {}LogModule.register() 返回一个配置好的 Module。使用方可以使用它导出的日志能力,不需要关心内部 Providers 如何创建。
按业务划分 Module
Module 应该表示一项完整的业务功能,而不是一种文件类型:
ProjectModule组织项目命令、项目 service 和内部 repository;ReleaseModule组织发布命令与发布流程;StorageModule提供多个业务共同使用的存储能力;AppModule负责组合这些功能。
推荐按功能组织目录:
src/
├─ app.module.ts
├─ index.ts
├─ projects/
│ ├─ project.module.ts
│ ├─ commands/
│ ├─ services/
│ └─ repositories/
├─ releases/
│ ├─ release.module.ts
│ ├─ commands/
│ └─ services/
└─ infrastructure/
└─ storage/
├─ storage.module.ts
└─ storage.service.ts这样修改项目业务时,大部分工作都发生在 projects/ 内,而不需要在全局 commands/、services/ 和 repositories/ 目录之间来回寻找。