控制反转:Typescript 中的服务定位器

2025-06-07

控制反转: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;
}
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 { 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;
}
}
view raw UserService.ts hosted with ❤ by GitHub
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;
}
}
view raw UserService.ts hosted with ❤ by GitHub
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;
}
}
view raw UserService.ts hosted with ❤ by GitHub

最后,无论何时您想要实际实例化单例服务,您都可以这样做:

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);
view raw main.ts hosted with ❤ by GitHub
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);
view raw main.ts hosted with ❤ by GitHub
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);
view raw main.ts hosted with ❤ by GitHub

实际的好处是,无论何时您想要测试使用此实例的任何其他组件,您都可以轻松地在方法上实例化应用程序的模拟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
})
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 实现者提供的现有解决方案,例如tsyringedecoration-ioc(VSCode 中 DI 实现的提取)。我喜欢在探索新想法时撰写文章,原因之一是可以立即获得反馈并学习新知识,从而改进这些解决方案,所以非常感谢!

文章来源:https://dev.to/this-is-angular/inversion-of-control-dependency-injection-in-typescript-1dj9
PREV
10 个对 Web 开发者有用的 Chrome 扩展程序
NEXT
我改变主意了。Angular 需要一个响应式原语