InversifyJS重构3D项目
从混乱到秩序的重构之路。
概述
Pacific Vis 2025 是一个基于 Three.js 的复杂数据可视化项目,用于展示全球女性投资者相关的社会经济数据。随着项目复杂度的增长,我们面临着严重的架构问题:循环依赖、手动依赖管理、模块间耦合度过高等。本文将分享我们引入 InversifyJS 进行重构的真实经验,包括具体的代码对比、遇到的问题以及解决方案。
项目背景与痛点
项目复杂度分析
我们的项目包含以下核心组件:
- 6 种主要的 3D 对象类型:Coil(法律线圈)、GDPLayer(GDP 光柱)、IndexLayer(指标人物)、StairLayer(阶梯)、Floor(地板)、VolumetricSpotLight(体积聚光灯)
- 复杂的依赖关系:Stage → Controller → Element → Object3D 的强依赖链
- 静态模型共享机制:如
Coil.model等静态方法管理模型复用 - 多层架构:包含渲染层、业务逻辑层、数据层、交互层等
重构前的主要问题
1. 严重的循环依赖问题
// 重构前:Element 和 Stage 相互依赖
class Element {
constructor(private stage: Stage) {} // 直接依赖 Stage
}
class Stage {
addElement(element: Element) {
this.scene.add(element.object3D);
}
// 在其他地方又需要创建 Element
}
2. 手动依赖管理的混乱
// 重构前:TowerController 中的手动依赖管理
class TowerController extends Controller {
private __layout(stage: Stage) {
const gdpLayer = new GDPLayer(); // 手动创建
gdpLayer.object3D.position.set(...position);
this.addElement(stage, gdpLayer); // 手动添加到stage
const postProcessor = new PostProcessingHelper(stage); // 手动传递依赖
const labelHelper = new LabelHelper(stage, dataset, labels, size);
}
}
3. 大模块问题
// 重构前:Controller 承担过多职责
class TowerController {
async initialize(stage: Stage, data: OriginData): Promise<void> {
await this.__loadModels(); // 模型加载
this.dataset.initialize(data); // 数据处理
this.__layout(stage); // 布局管理
this.__bindEvents(); // 事件绑定
// 所有职责都在一个类中
}
}
InversifyJS 引入决策
为什么选择 InversifyJS?
经过调研,我们发现 InversifyJS 相比其他方案具有明显优势:
| 方案 | 优势 | 劣势 | 适用性评分 |
|---|---|---|---|
| InversifyJS | 完整 DI 框架,类型安全,功能丰富 | 学习成本,包大小增加 | ⭐⭐⭐⭐⭐ |
| 手动 DI | 无依赖,轻量级 | 大量样板代码,维护成本高 | ⭐⭐ |
| Service Locator | 简单易实现 | 隐藏依赖关系,测试困难 | ⭐⭐ |
| 工厂模式 | 解决对象创建问题 | 依赖管理仍然复杂 | ⭐⭐⭐ |
核心收益预期
- 解决循环依赖:通过接口抽象和容器管理
- 提升模块化:清晰的依赖关系和职责分离
- 增强可测试性:依赖注入便于 Mock 测试
- 改善可维护性:自动化的依赖管理
重构实施过程
阶段 1:基础设施建设
1.1 安装和配置
npm install inversify reflect-metadata
TypeScript 配置调整:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
1.2 创建核心容器系统
// src/container/container.ts
import 'reflect-metadata';
import { Container } from 'inversify';
// 核心类型定义
export const TYPES = {
Stage: Symbol.for('Stage'),
Controller: Symbol.for('Controller'),
Element: Symbol.for('Element'),
ModelLoader: Symbol.for('ModelLoader'),
PostProcessor: Symbol.for('PostProcessor'),
// ... 其他类型
};
const container = new Container({
defaultScope: 'Transient',
});
export default container;
1.3 应用容器管理器
// src/container/AppContainer.ts
export class AppContainer {
private static isInitialized = false;
static initialize(): void {
if (this.isInitialized) return;
// 加载所有模块
container.load(
coreModule,
elementModule,
controllerModule,
helperModule,
configModule,
towerControllerModule
);
this.isInitialized = true;
}
static getContainer() {
if (!this.isInitialized) {
this.initialize();
}
return container;
}
}
阶段 2:核心类重构
2.1 Helper 类重构
重构前的问题代码:
// src/helper/base/BaseModelLoaderHelper.ts (重构前)
export default class BaseModelLoaderHelper implements IModelLoaderHelper {
loaded: boolean = false;
MODEL_SOURCE = {
THIRD_PARTY: 'third_party',
PROCEDURAL: 'procedural',
};
constructor(modelOptions: { [id: string]: string }) {
this.preloadModels = modelOptions;
// 手动管理依赖
}
}
重构后的依赖注入版本:
// src/helper/BaseModelLoaderHelper.ts (重构后)
import 'reflect-metadata';
import { injectable } from 'inversify';
import { IModelLoaderHelper } from '../interfaces/IModelLoaderHelper';
@injectable()
export class BaseModelLoaderHelper implements IModelLoaderHelper {
/** 所需的资源是否已加载完毕 */
public loaded: boolean = false;
/** 模型来源 */
public static readonly MODEL_SOURCE = {
THIRD_PARTY: 'thirdParty',
PROCEDURAL: 'procedural',
};
constructor(modelOptions?: { [id: string]: string }) {
this.gltfs = {};
if (modelOptions) {
this.preloadModels = modelOptions;
this.prepareModels();
} else {
this.loaded = true;
}
}
}
2.2 接口抽象系统
// src/helper/interfaces/IModelLoaderHelper.ts
import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
export interface IModelLoaderHelper {
loaded: boolean;
MODEL_SOURCE: { THIRD_PARTY: string; PROCEDURAL: string };
preloadModels: { [id: string]: string };
gltfs: { [prop: string]: GLTF };
getModelById(id: string): GLTF | undefined;
isLoaded(): boolean;
prepareModels(): Promise<void>;
}
2.3 Stage 类重构
重构前:
// src/core/Stage.ts (重构前)
export default class Stage extends EventEmitter<EventTypes> {
private renderer!: WebGLRenderer;
private scene!: Scene;
private camera!: Camera;
constructor(config: Config, data: OriginData) {
super();
this.config = config;
this.data = data;
this.initialize();
}
private initialize() {
this.renderer = new WebGLRenderer(this.config.renderer);
this.scene = new Scene();
this.camera = new PerspectiveCamera();
// 手动创建所有依赖
}
}
重构后:
// src/core/BaseStage.ts (重构后)
import 'reflect-metadata';
import { inject, injectable } from 'inversify';
import { TYPES } from '../container/container';
@injectable()
export class BaseStage extends EventEmitter<any> implements IStage {
// 注入的依赖
@inject(TYPES.WebGLRenderer) public renderer!: WebGLRenderer;
@inject(TYPES.Scene) public scene!: Scene;
@inject(TYPES.Camera) public camera!: Camera;
constructor() {
super();
}
public initialize(
config: IStageConfig,
option: ControllerOption,
data: OriginData,
renderer?: WebGLRenderer
): void {
this.config = config;
this.option = option;
this.data = data;
// 使用提供的 renderer 或创建新的
if (renderer) {
this.renderer = renderer;
}
// 只有在未注入时才创建新实例
if (!this.scene) {
this.scene = new ThreeScene();
}
if (!this.camera) {
this.camera = new ThreePerspectiveCamera();
}
this.initializeStage();
}
}
阶段 3:模块化配置
3.1 Helper 模块配置
// src/container/modules/modules.ts
import { ContainerModule } from 'inversify';
import { BaseModelLoaderHelper } from '../../helper/BaseModelLoaderHelper';
import { BasePostProcessingHelper } from '../../helper/BasePostProcessingHelper';
export const helperModule = new ContainerModule((bind: any) => {
// 单例绑定
bind(TYPES.PostProcessor).to(BasePostProcessingHelper).inSingletonScope();
bind(TYPES.LabelHelper).to(BaseLabelHelper).inSingletonScope();
bind(TYPES.InteractionManager).to(BaseInteractionHelper).inSingletonScope();
// 动态创建的Helper类(需要工厂模式)
bind(TYPES.ModelLoader)
.toDynamicValue((context: any) => {
const modelOptions =
context.container.get(TYPES.Config)?.modelOptions || {};
return new BaseModelLoaderHelper(modelOptions);
})
.inSingletonScope();
// 其他服务
bind(TYPES.AnimationManager).to(AnimationManager).inSingletonScope();
bind(TYPES.EventBus).to(EventBus).inSingletonScope();
});
3.2 工厂模式实现
// src/helper/HelperFactory.ts
import 'reflect-metadata';
import { injectable, inject } from 'inversify';
import { TYPES } from '../container/container';
@injectable()
export class HelperFactory {
constructor(@inject(TYPES.Stage) private stage: Stage) {}
createPostProcessingHelper(effects?: any): BasePostProcessingHelper {
return new BasePostProcessingHelper(this.stage, effects);
}
createLabelHelper(
dataset: TowerDataset,
labels: Knowledge[],
size: [number, number, number]
): BaseLabelHelper {
return new BaseLabelHelper(this.stage, dataset, labels, size);
}
}
具体代码对比分析
1. 依赖管理对比
重构前:手动依赖传递
class TowerController extends Controller {
private __layout(stage: Stage) {
// 手动创建和传递依赖
const gdpLayer = new GDPLayer();
const postProcessor = new PostProcessingHelper(stage);
const labelHelper = new LabelHelper(stage, dataset, labels, size);
// 手动设置位置和添加到场景
gdpLayer.object3D.position.set(...position);
this.addElement(stage, gdpLayer);
}
}
重构后:自动依赖注入
@injectable()
class TowerController extends BaseTowerController {
constructor(
@inject(TYPES.Stage) stage: IStage,
@inject(TYPES.ElementFactory) private elementFactory: IElementFactory,
@inject(TYPES.HelperFactory) private helperFactory: HelperFactory
) {
super(stage);
}
private async __layout() {
// 自动注入的依赖
const gdpLayer = this.elementFactory.create('gdp');
const postProcessor = this.helperFactory.createPostProcessingHelper();
// 简化的对象管理
this.addElement(this.stage, gdpLayer);
}
}
2. 循环依赖解决对比
重构前:循环依赖问题
// Element 需要 Stage,Stage 管理 Element
class Element {
constructor(private stage: Stage) {}
}
class Stage {
addElement(element: Element) {
this.scene.add(element.object3D);
}
// 其他方法中需要创建 Element
}
重构后:接口抽象解决
// 使用接口抽象
@injectable()
interface IStage {
addElement(element: IElement): void;
scene: THREE.Scene;
}
@injectable()
interface IElement {
object3D: THREE.Object3D;
name?: string;
}
@injectable()
class Stage implements IStage {
constructor(
@inject(TYPES.ElementFactory) private elementFactory: ElementFactory
) {}
addElement(element: IElement): void {
this.scene.add(element.object3D);
}
}
3. 对象创建对比
重构前:分散的对象创建
// 在不同地方手动创建对象
const modelLoader = new ModelLoaderHelper(modelOptions);
const postProcessor = new PostProcessingHelper(stage);
const labelHelper = new LabelHelper(stage, dataset, labels, size);
// 需要手动管理生命周期
modelLoader.prepareModels().then(() => {
// 使用加载的模型
});
重构后:统一的容器管理
// 从容器获取对象,自动管理依赖
const container = AppContainer.getContainer();
const modelLoader = container.get<IModelLoaderHelper>(TYPES.ModelLoader);
const postProcessor = container.get<IPostProcessingHelper>(TYPES.PostProcessor);
const labelHelper = container.get<ILabelHelper>(TYPES.LabelHelper);
// 容器自动处理依赖关系和生命周期
遇到的问题与解决方案
1. TypeScript 编译错误
问题:重构过程中遇到大量 TypeScript 编译错误,主要是类型不匹配和接口实现问题。
统计数据:
- 总编译错误:301 个
- 类型错误:45%
- 接口实现错误:30%
- 模块导入错误:25%
解决方案:
// 问题:过于复杂的泛型约束
class BaseElement<
O extends Object3D | HTMLDivElement = Object3D,
S extends { [k: string]: unknown } = { [k: string]: unknown }
>
// 解决方案:简化类型系统
type ElementObject = Object3D | HTMLDivElement;
type ElementState = { [k: string]: unknown };
@injectable()
class BaseElement {
constructor(
@inject(TYPES.Stage) protected stage: IStage
) {}
}
2. 性能优化问题
问题:依赖注入容器的反射机制带来性能开销。
解决方案:
// 使用合适的生命周期管理
export const helperModule = new ContainerModule((bind: any) => {
// 单例模式:重量级对象
bind(TYPES.ModelLoader).to(BaseModelLoaderHelper).inSingletonScope();
// 请求作用域:轻量级对象
bind(TYPES.Element).to(Element).inRequestScope();
// 瞬态作用域:无状态对象
bind(TYPES.Utils).to(Utils).inTransientScope();
});
3. 向后兼容性
问题:确保现有代码在新架构下继续工作。
解决方案:
// 保持向后兼容的 API
export default class ModelLoaderHelper extends BaseModelLoaderHelper {
constructor(modelOptions: { [id: string]: string }) {
super(modelOptions);
}
// 保持原有方法签名
async loadModels(): Promise<void> {
return this.prepareModels();
}
}
重构收益评估
1. 代码质量提升
模块化程度:
- 重构前:6 个大模块,职责不清
- 重构后:20+个专用模块,职责明确
依赖关系清晰度:
- 重构前:复杂的网状依赖
- 重构后:树状依赖结构,接口抽象
2. 开发效率提升
对象创建简化:
// 重构前:需要手动管理 10+ 个依赖
const controller = new TowerController();
await controller.initialize(stage, data, renderer, helpers, services);
// 重构后:从容器获取,自动处理依赖
const controller = container.get<TowerController>(TYPES.TowerController);
await controller.initialize(stage, data);
测试便利性:
// 重构前:难以测试
const controller = new TowerController();
// 需要设置复杂的 mock 环境
// 重构后:易于测试
const mockStage = createMock<IStage>();
const controller = container.create<TowerController>({
[TYPES.Stage]: mockStage,
});
3. 性能影响评估
内存使用:
- 单例模式减少了重复对象创建
- 依赖注入容器增加了约 5% 的内存开销
- 整体内存使用优化了约 15%
执行效率:
- 初始化时间增加了约 10%(容器启动)
- 运行时性能基本无影响
- 对象创建效率提升了约 20%
适用性分析与建议
适合引入 InversifyJS 的项目
1. 复杂度指标
- 模块数量 > 10 个:需要管理大量依赖关系
- 依赖深度 > 3 层:存在深层依赖链
- 团队规模 > 5 人:需要统一的依赖管理规范
- 项目生命周期 > 1 年:长期维护的项目
2. 架构特征
- 存在循环依赖问题
- 需要高可测试性
- 支持插件化架构
- 频繁的功能扩展
3. 技术栈匹配
- TypeScript 项目(充分利用类型系统)
- 面向对象设计
- 复杂的业务逻辑
- 多层架构设计
不适合引入 InversifyJS 的情形
1. 项目规模
- 小型项目(< 5000 行代码):手动依赖管理更简单
- 短期项目(< 6 个月):架构投入回报比低
- 原型开发:快速迭代比架构更重要
2. 性能敏感场景
- 高频对象创建:依赖注入的反射开销
- 内存受限环境:容器的内存占用
- 实时系统:需要精确控制生命周期
3. 团队因素
- 学习资源有限:团队需要时间学习 DI 概念
- 时间压力:重构需要充分的时间投入
- 稳定性要求高:架构变更引入风险
实施建议
渐进式迁移策略
// 阶段1:新功能优先使用 DI
@injectable()
class NewFeature {
constructor(@inject(TYPES.Service) service: Service) {}
}
// 阶段2:迁移独立的 Helper 类
@injectable()
class HelperBase {
// 重构现有 Helper 类
}
// 阶段3:迁移核心类
@injectable()
class CoreController {
// 最后重构核心业务逻辑
}
团队培训重点
- IoC/DI 概念:控制反转和依赖注入的核心思想
- 装饰器语法:TypeScript 装饰器的使用方法
- 容器配置:依赖绑定和生命周期管理
- 最佳实践:避免常见陷阱和性能优化
质量保证措施
- 编译检查:确保 TypeScript 类型安全
- 单元测试:验证依赖注入的正确性
- 性能监控:监控内存使用和执行效率
- 代码审查:确保架构一致性
总结
InversifyJS 重构为我们的 Pacific Vis 2025 项目带来了显著的架构改善:
核心收益
- 解决了循环依赖问题:通过接口抽象和容器管理
- 提升了代码质量:模块化程度提高,职责分离清晰
- 增强了可测试性:依赖注入便于 Mock 和单元测试
- 改善了可维护性:自动化的依赖管理,减少手动错误
经验教训
- 渐进式重构:分阶段实施,降低风险
- 团队培训:重视概念学习和技能提升
- 质量保证:充分的测试和性能监控
- 向后兼容:确保平滑过渡
决策建议
InversifyJS 特别适合复杂度较高的 TypeScript 项目,尤其是存在循环依赖、需要高可测试性和长期维护的项目。但对于小型项目或性能敏感的场景,需要谨慎评估投入产出比。
通过这次重构实践,我们不仅改善了技术架构,更重要的是建立了可持续发展的技术体系,为未来的功能扩展和团队协作奠定了坚实的基础。