mini-di-cocos
v1.0.0
Published
Lightweight DI container for Cocos Creator 3.x - inspired by VContainer and Zenject, optimized for Playable Ads
Maintainers
Readme
MiniDI
Lightweight Dependency Injection container for Cocos Creator 3.x, inspired by VContainer and Zenject.
Optimized for Playable Ads with minimal bundle size impact.
Features
- 🚀 No reflect-metadata — uses static properties, works with any Cocos build
- 🎮 Cocos Creator native — seamless integration with Components and Prefabs
- 📦 Minimal footprint — optimized for playable ads (2-15MB limits)
- 🔄 Lifecycle hooks — IInitializable, IStartable, ITickable, IDisposable
- 🏭 VContainer-style instantiate —
container.instantiate(prefab)with auto-injection - 🔍 Zenject-style scene injection — automatic injection across entire scene
Installation
npm install mini-di-cocos --legacy-peer-depsOr add to your .npmrc:
legacy-peer-deps=trueQuick Start
1. Create Services
import { Injectable, IInitializable, IDisposable } from 'mini-di-cocos';
// Simple service
@Injectable([])
class ConfigService {
readonly maxScore = 1000;
readonly soundEnabled = true;
}
// Service with dependencies
@Injectable([ConfigService])
class AudioService implements IInitializable, IDisposable {
constructor(private config: ConfigService) {}
initialize(): void {
console.log('AudioService initialized');
}
playSound(name: string): void {
if (this.config.soundEnabled) {
// play sound
}
}
dispose(): void {
console.log('AudioService disposed');
}
}
@Injectable([AudioService, ConfigService])
class GameService {
constructor(
private audio: AudioService,
private config: ConfigService
) {}
onScore(): void {
this.audio.playSound('score');
}
}2. Create LifetimeScope
import { _decorator } from 'cc';
import { LifetimeScope, ContainerBuilder } from 'mini-di-cocos';
const { ccclass } = _decorator;
@ccclass('GameScope')
export class GameScope extends LifetimeScope {
protected configure(builder: ContainerBuilder): void {
builder.register(ConfigService);
builder.register(AudioService);
builder.register(GameService);
}
}3. Inject into Cocos Components
import { _decorator, Component } from 'cc';
import { NeedInject, InjectProperty } from 'mini-di-cocos';
@NeedInject()
@ccclass('PlayerController')
export class PlayerController extends Component {
@InjectProperty(GameService)
private gameService!: GameService;
@InjectProperty(AudioService)
private audio!: AudioService;
onCollectCoin(): void {
this.gameService.onScore();
this.audio.playSound('coin');
}
}4. Scene Setup
Scene Hierarchy:
├── GameScope (LifetimeScope) ← Add your scope here
├── Player
│ └── PlayerController ← @NeedInject components
├── UI
│ └── ScoreView ← @NeedInject components
└── ...API Reference
Decorators
@Injectable(dependencies: Token[])
Marks a class for constructor injection.
@Injectable([DepA, DepB])
class MyService {
constructor(private depA: DepA, private depB: DepB) {}
}@NeedInject()
Marks a Cocos Component for property injection.
@NeedInject()
@ccclass('MyComponent')
class MyComponent extends Component {}@InjectProperty(token: Token)
Injects a dependency into a property.
@InjectProperty(AudioService)
private audio!: AudioService;Container
// Registration
container.register(Token, options?)
container.registerSingleton(Token, useClass?)
container.registerTransient(Token, useClass?)
container.registerInstance(Token, instance)
container.registerFactory(Token, factory, lifetime?)
// Resolution
container.resolve<T>(Token): T
container.tryResolve<T>(Token): T | null
container.isRegistered(Token): boolean
// Cocos Integration
container.injectComponent(component)
container.injectNode(node)
container.instantiate(prefab, parent?): Node
container.instantiateAndGet<T>(prefab, type, parent?): T
// Lifecycle
container.initializeAll()
container.startAll()
container.disposeAll()
container.getAllSingletons(): any[]ContainerBuilder
const builder = new ContainerBuilder();
builder
.register(ServiceA)
.register(ServiceB, { useClass: ServiceBImpl })
.registerSingleton(ServiceC)
.registerTransient(ServiceD)
.registerInstance(Config, configInstance)
.registerFactory(ServiceE, (c) => new ServiceE(c.resolve(ServiceA)));
const container = builder.build();LifetimeScope
| Property | Default | Description |
|----------|---------|-------------|
| autoInjectScene | true | Auto-inject all @NeedInject components in scene |
| autoInitialize | true | Auto-call IInitializable.initialize() |
| autoStart | true | Auto-call IStartable.start() |
| enableTickManager | false | Enable ITickable support |
@ccclass('GameScope')
class GameScope extends LifetimeScope {
protected configure(builder: ContainerBuilder): void {
// Register your services here
}
}
// Static access
const scope = LifetimeScope.findInScene();
const service = scope?.resolve(MyService);InjectionToken
For interface-based injection (TypeScript interfaces are erased at runtime):
interface IAudioService {
playSound(name: string): void;
}
const IAudioService = new InjectionToken<IAudioService>('IAudioService');
// Registration
builder.register(IAudioService, {
useFactory: (c) => new AudioServiceImpl(c.resolve(ConfigService))
});
// Injection
@InjectProperty(IAudioService)
private audio!: IAudioService;Lifecycle Interfaces
interface IInitializable {
initialize(): void | Promise<void>;
}
interface IStartable {
start(): void;
}
interface ITickable {
tick(deltaTime: number): void;
}
interface IFixedTickable {
fixedTick(fixedDeltaTime: number): void;
}
interface ILateTickable {
lateTick(deltaTime: number): void;
}
interface IDisposable {
dispose(): void;
}Execution Order:
onLoad()
└── Container.build()
└── IInitializable.initialize() (registration order)
└── Scene injection
start()
└── IStartable.start() (registration order)
update(dt)
└── ITickable.tick(dt)
└── IFixedTickable.fixedTick(fixedDt) (fixed timestep)
lateUpdate(dt)
└── ILateTickable.lateTick(dt)
onDestroy()
└── IDisposable.dispose() (reverse order)Dynamic Instantiation
@Injectable([Container])
class EnemySpawner {
@property(Prefab)
private enemyPrefab!: Prefab;
constructor(private container: Container) {}
spawn(parent: Node): Enemy {
// Instantiate with auto-injection
return this.container.instantiateAndGet(
this.enemyPrefab,
Enemy,
parent
);
}
}Lifetime
| Type | Description |
|------|-------------|
| Lifetime.Singleton | One instance per container (default) |
| Lifetime.Transient | New instance on each resolve |
Comparison
| Feature | MiniDI | VContainer | Zenject | |---------|--------|------------|---------| | Platform | Cocos Creator | Unity | Unity | | reflect-metadata | ❌ Not required | ✅ Required | ✅ Required | | Bundle size | ~5KB | ~50KB | ~200KB | | Constructor injection | ✅ | ✅ | ✅ | | Property injection | ✅ | ✅ | ✅ | | Scene injection | ✅ | ❌ | ✅ | | Prefab instantiate | ✅ | ✅ | ✅ | | Lifecycle hooks | ✅ | ✅ | ✅ | | Scoped containers | ❌ | ✅ | ✅ |
Best Practices
1. Service vs Component
// ✅ Services — constructor injection
@Injectable([ConfigService])
class AudioService {
constructor(private config: ConfigService) {}
}
// ✅ Cocos Components — property injection
@NeedInject()
@ccclass('PlayerController')
class PlayerController extends Component {
@InjectProperty(AudioService)
private audio!: AudioService;
}2. Interface Segregation
// Define interfaces
interface IAudioService {
playSound(name: string): void;
}
// Create tokens
const IAudioService = new InjectionToken<IAudioService>('IAudioService');
// Register implementation
builder.register(IAudioService, { useClass: AudioServiceImpl });3. Factory Registration
builder.registerFactory(ComplexService, (container) => {
const config = container.resolve(ConfigService);
const audio = container.resolve(AudioService);
return new ComplexService(config, audio, {
debug: true,
maxRetries: 3
});
});Troubleshooting
"X is not registered"
Ensure the dependency is registered before resolving:
protected configure(builder: ContainerBuilder): void {
builder.register(DependencyFirst); // Register dependencies first
builder.register(ServiceThatNeedsIt);
}"Circular dependency detected"
Break the cycle with factory or lazy resolution:
@Injectable([])
class ServiceA {
private _serviceB: ServiceB | null = null;
setServiceB(b: ServiceB): void {
this._serviceB = b;
}
}
// In configure:
builder.registerFactory(ServiceA, (c) => {
const a = new ServiceA();
const b = c.resolve(ServiceB);
a.setServiceB(b);
return a;
});Component not injected
- Check
@NeedInject()decorator is present - Check component is in scene when LifetimeScope loads
- For dynamic objects, use
container.instantiate()
License
MIT
