控制反转:Typescript 中的服务定位器
我已经使用 Angular 有一段时间了(超过 2 年),自从我转到另一个工作地点后,我(主要)转向了 Stencil。
Stencil 是一款非常优秀的 Typescript 编译器,可以生成 Web 组件。如果要我给它定位,它可以说是 Angular 和 React 的混合体。它像 Angular 一样使用装饰器(编译器可以收集元数据并据此生成输出代码),并且像 React 一样使用具有生命周期的jsx/tsx 。它是一款非常强大的设计系统生成工具。
在我一直在研究的这个设计系统中,存在一个如何存储全局状态的问题。我希望有一个类似于控制反转(因为我非常喜欢Angular中的控制反转)。使用控制反转系统最大的优势在于,你可以在测试中轻松地模拟服务,而无需在 Jest 中覆盖实际的导入(覆盖虽然没问题,但在复杂的设置中,如果你希望在迭代之间使用不同的配置,那么这会变得很混乱,因为如果在模拟之前存在对模块的任何引用,模拟将无法工作)。
因此我想出了一个简单的解决方案,用于全局可注入服务,而不关心实际的实现。
第一步是思考一下你想要编写的服务到底要做什么。基于此,你可以声明一个接口,包含服务的所有公共方法。我们来考虑一个设置和获取用户信息的简单服务。
export interface IUserData { | |
//... userdata | |
} | |
export interface IUserServiceModel { | |
public set user(data: IUserData): void; | |
public get user(): IUserData; | |
} |
export interface IUserData { | |
//... userdata | |
} | |
export interface IUserServiceModel { | |
public set user(data: IUserData): void; | |
public get user(): IUserData; | |
} |
然后我们需要声明一个UserServiceInstanceResolver
静态类,它将接收该接口的类实现的实例,并将其存储以便能够访问它。
import { IUserServiceModel } from './UserServiceModel'; | |
export class UserServiceInstanceResolver { | |
private static _userServiceInstance: IUserServiceModel; | |
public static Instantiate(userService: IUserServiceModel) { | |
if (!this._userServiceInstance) { | |
this._userServiceInstance = userService; | |
} | |
return this._userServiceInstance; | |
} | |
public static get Instance() { | |
return this._userServiceInstance; | |
} | |
} |
import { IUserServiceModel } from './UserServiceModel'; | |
export class UserServiceInstanceResolver { | |
private static _userServiceInstance: IUserServiceModel; | |
public static Instantiate(userService: IUserServiceModel) { | |
if (!this._userServiceInstance) { | |
this._userServiceInstance = userService; | |
} | |
return this._userServiceInstance; | |
} | |
public static get Instance() { | |
return this._userServiceInstance; | |
} | |
} |
最后但同样重要的是,我们可以实现实际的服务
import { IUserData, IUserServiceModel } from './UserServiceModel.ts'; | |
export class UserService implements IUserServiceModel { | |
private _userData: IUserData; | |
constructor() { | |
//... | |
} | |
set userData(data: IUserData) { | |
this._userData = data; | |
} | |
get userData() { | |
return this._userData; | |
} | |
} |
import { IUserData, IUserServiceModel } from './UserServiceModel.ts'; | |
export class UserService implements IUserServiceModel { | |
private _userData: IUserData; | |
constructor() { | |
//... | |
} | |
set userData(data: IUserData) { | |
this._userData = data; | |
} | |
get userData() { | |
return this._userData; | |
} | |
} |
最后,无论何时您想要实际实例化单例服务,您都可以这样做:
import { UserServiceInstanceResolver } from './UserServiceInstanceResolver.ts' | |
import { UserService } from './UserService.ts'; | |
//... | |
const userService = UserServiceInstanceResolver.Instantiate(new UserService()); | |
userService.setUser({ /* ...userData */ }) | |
// And later in code (probably in another file, you can get the instance by): | |
const userService = UserServiceInstanceResolver.Instance; | |
console.log(userService.user); |
import { UserServiceInstanceResolver } from './UserServiceInstanceResolver.ts' | |
import { UserService } from './UserService.ts'; | |
//... | |
const userService = UserServiceInstanceResolver.Instantiate(new UserService()); | |
userService.setUser({ /* ...userData */ }) | |
// And later in code (probably in another file, you can get the instance by): | |
const userService = UserServiceInstanceResolver.Instance; | |
console.log(userService.user); |
实际的好处是,无论何时您想要测试使用此实例的任何其他组件,您都可以轻松地在方法上实例化应用程序的模拟UserServiceInstanceResolver.Instantiate
。
import { UserServiceInstanceResolver } from './UserServiceInstanceResolver'; | |
import { IUserServiceModel, IUserData } from './UserServiceModel'; | |
// This should be imported from another file but I didn't want to write another gist. | |
class UserServiceMock implements IUserServiceModel { | |
set user(userData: IUserData): { | |
//...whatever you want the mock to do | |
}; | |
get user() { | |
return { | |
//...mocked user | |
} | |
}; | |
} | |
describe('Component: SomeComponent', () => { | |
beforeAll(() => { | |
UserServiceInstanceResolver.Instantiate(new UserServiceMock()); | |
//...logic for setting up the component | |
}) | |
// write tests and have mocked user instance used in the tested component | |
}) |
import { UserServiceInstanceResolver } from './UserServiceInstanceResolver'; | |
import { IUserServiceModel, IUserData } from './UserServiceModel'; | |
// This should be imported from another file but I didn't want to write another gist. | |
class UserServiceMock implements IUserServiceModel { | |
set user(userData: IUserData): { | |
//...whatever you want the mock to do | |
}; | |
get user() { | |
return { | |
//...mocked user | |
} | |
}; | |
} | |
describe('Component: SomeComponent', () => { | |
beforeAll(() => { | |
UserServiceInstanceResolver.Instantiate(new UserServiceMock()); | |
//...logic for setting up the component | |
}) | |
// write tests and have mocked user instance used in the tested component | |
}) |
这实际上就是我所需要的全部内容(仅需一个根级全局实例)。但这段代码可以扩展,以支持每个组件实例(或层级实例),并获得更接近 Angular DI 的体验。
感谢您阅读我的文章。如果您喜欢,请点个爱心,让更多人看到。
LE:
看了部分评论后,我同意这并非最佳方案,或许你应该参考 TypeScript 实现者提供的现有解决方案,例如tsyringe或decoration-ioc(VSCode 中 DI 实现的提取)。我喜欢在探索新想法时撰写文章,原因之一是可以立即获得反馈并学习新知识,从而改进这些解决方案,所以非常感谢!