InversifyJS重构3D项目

从混乱到秩序的重构之路。

概述

Pacific Vis 2025 是一个基于 Three.js 的复杂数据可视化项目,用于展示全球女性投资者相关的社会经济数据。随着项目复杂度的增长,我们面临着严重的架构问题:循环依赖、手动依赖管理、模块间耦合度过高等。本文将分享我们引入 InversifyJS 进行重构的真实经验,包括具体的代码对比、遇到的问题以及解决方案。

项目背景与痛点

项目复杂度分析

我们的项目包含以下核心组件:

  • 6 种主要的 3D 对象类型:Coil(法律线圈)、GDPLayer(GDP 光柱)、IndexLayer(指标人物)、StairLayer(阶梯)、Floor(地板)、VolumetricSpotLight(体积聚光灯)
  • 复杂的依赖关系:Stage → Controller → Element → Object3D 的强依赖链
  • 静态模型共享机制:如 Coil.model 等静态方法管理模型复用
  • 多层架构:包含渲染层、业务逻辑层、数据层、交互层等

重构前的主要问题

1. 严重的循环依赖问题

1
2
3
4
5
6
7
8
9
10
11
// 重构前:Element 和 Stage 相互依赖
class Element {
constructor(private stage: Stage) {} // 直接依赖 Stage
}

class Stage {
addElement(element: Element) {
this.scene.add(element.object3D);
}
// 在其他地方又需要创建 Element
}

2. 手动依赖管理的混乱

1
2
3
4
5
6
7
8
9
10
11
// 重构前: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. 大模块问题

1
2
3
4
5
6
7
8
9
10
// 重构前: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 简单易实现 隐藏依赖关系,测试困难 ⭐⭐
工厂模式 解决对象创建问题 依赖管理仍然复杂 ⭐⭐⭐

核心收益预期

  1. 解决循环依赖:通过接口抽象和容器管理
  2. 提升模块化:清晰的依赖关系和职责分离
  3. 增强可测试性:依赖注入便于 Mock 测试
  4. 改善可维护性:自动化的依赖管理

重构实施过程

阶段 1:基础设施建设

1.1 安装和配置

1
npm install inversify reflect-metadata

TypeScript 配置调整:

1
2
3
4
5
6
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}

1.2 创建核心容器系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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 应用容器管理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 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 类重构

重构前的问题代码

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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;
// 手动管理依赖
}
}

重构后的依赖注入版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 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 接口抽象系统

1
2
3
4
5
6
7
8
9
10
11
12
// 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 类重构

重构前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 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();
// 手动创建所有依赖
}
}

重构后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 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 模块配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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 工厂模式实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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. 依赖管理对比

重构前:手动依赖传递

1
2
3
4
5
6
7
8
9
10
11
12
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);
}
}

重构后:自动依赖注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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. 循环依赖解决对比

重构前:循环依赖问题

1
2
3
4
5
6
7
8
9
10
11
// Element 需要 Stage,Stage 管理 Element
class Element {
constructor(private stage: Stage) {}
}

class Stage {
addElement(element: Element) {
this.scene.add(element.object3D);
}
// 其他方法中需要创建 Element
}

重构后:接口抽象解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 使用接口抽象
@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. 对象创建对比

重构前:分散的对象创建

1
2
3
4
5
6
7
8
9
// 在不同地方手动创建对象
const modelLoader = new ModelLoaderHelper(modelOptions);
const postProcessor = new PostProcessingHelper(stage);
const labelHelper = new LabelHelper(stage, dataset, labels, size);

// 需要手动管理生命周期
modelLoader.prepareModels().then(() => {
// 使用加载的模型
});

重构后:统一的容器管理

1
2
3
4
5
6
7
// 从容器获取对象,自动管理依赖
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%

解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 问题:过于复杂的泛型约束
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. 性能优化问题

问题:依赖注入容器的反射机制带来性能开销。

解决方案

1
2
3
4
5
6
7
8
9
10
11
// 使用合适的生命周期管理
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. 向后兼容性

问题:确保现有代码在新架构下继续工作。

解决方案

1
2
3
4
5
6
7
8
9
10
11
// 保持向后兼容的 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. 开发效率提升

对象创建简化

1
2
3
4
5
6
7
// 重构前:需要手动管理 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);

测试便利性

1
2
3
4
5
6
7
8
9
// 重构前:难以测试
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 阶段1:新功能优先使用 DI
@injectable()
class NewFeature {
constructor(@inject(TYPES.Service) service: Service) {}
}

// 阶段2:迁移独立的 Helper 类
@injectable()
class HelperBase {
// 重构现有 Helper 类
}

// 阶段3:迁移核心类
@injectable()
class CoreController {
// 最后重构核心业务逻辑
}

团队培训重点

  1. IoC/DI 概念:控制反转和依赖注入的核心思想
  2. 装饰器语法:TypeScript 装饰器的使用方法
  3. 容器配置:依赖绑定和生命周期管理
  4. 最佳实践:避免常见陷阱和性能优化

质量保证措施

  1. 编译检查:确保 TypeScript 类型安全
  2. 单元测试:验证依赖注入的正确性
  3. 性能监控:监控内存使用和执行效率
  4. 代码审查:确保架构一致性

总结

InversifyJS 重构为我们的 Pacific Vis 2025 项目带来了显著的架构改善:

核心收益

  1. 解决了循环依赖问题:通过接口抽象和容器管理
  2. 提升了代码质量:模块化程度提高,职责分离清晰
  3. 增强了可测试性:依赖注入便于 Mock 和单元测试
  4. 改善了可维护性:自动化的依赖管理,减少手动错误

经验教训

  1. 渐进式重构:分阶段实施,降低风险
  2. 团队培训:重视概念学习和技能提升
  3. 质量保证:充分的测试和性能监控
  4. 向后兼容:确保平滑过渡

决策建议

InversifyJS 特别适合复杂度较高的 TypeScript 项目,尤其是存在循环依赖、需要高可测试性和长期维护的项目。但对于小型项目或性能敏感的场景,需要谨慎评估投入产出比。

通过这次重构实践,我们不仅改善了技术架构,更重要的是建立了可持续发展的技术体系,为未来的功能扩展和团队协作奠定了坚实的基础。