Node.js 中的依赖注入与 IoC 容器
依赖注入通过从外部提供依赖来解耦组件。
手动依赖注入
// 定义接口(抽象)
interface Logger {
log(message: string): void;
error(message: string, error?: Error): void;
}
interface UserRepository {
findById(id: string): Promise<User | null>;
save(user: User): Promise<void>;
}
// 具体实现
class ConsoleLogger implements Logger {
log(message: string) { console.log(`[INFO] ${message}`); }
error(message: string, error?: Error) { console.error(`[ERROR] ${message}`, error); }
}
class PostgresUserRepository implements UserRepository {
constructor(private db: Database) {}
async findById(id: string) { return this.db.users.findOne(id); }
async save(user: User) { await this.db.users.upsert(user); }
}
// 服务依赖于抽象
class UserService {
constructor(
private userRepo: UserRepository,
private logger: Logger
) {}
async getUser(id: string): Promise<User> {
this.logger.log(`Getting user ${id}`);
const user = await this.userRepo.findById(id);
if (!user) throw new UserNotFoundError(id);
return user;
}
}
// 组合根 - 将所有组件连接起来
const logger = new ConsoleLogger();
const db = new Database(config.databaseUrl);
const userRepo = new PostgresUserRepository(db);
const userService = new UserService(userRepo, logger);

tsyringe(轻量级容器)
import 'reflect-metadata';
import { container, injectable, inject, singleton } from 'tsyringe';
@singleton()
class ConsoleLogger implements Logger {
log(message: string) { console.log(message); }
error(message: string) { console.error(message); }
}
@injectable()
class UserRepository {
constructor(@inject('Database') private db: Database) {}
async findById(id: string) { return this.db.users.findOne(id); }
}
@injectable()
class UserService {
constructor(
private userRepo: UserRepository,
private logger: ConsoleLogger
) {}
async getUser(id: string) {
this.logger.log(`Getting user ${id}`);
return this.userRepo.findById(id);
}
}
// 注册和解析
container.register('Database', { useValue: new Database(config.url) });
const service = container.resolve(UserService);

NestJS 内置 IoC
import { Injectable, Module } from '@nestjs/common';
@Injectable()
class UserRepository {
async findById(id: string): Promise<User> { /* ... */ }
}
@Injectable()
class UserService {
constructor(private userRepository: UserRepository) {}
async getUser(id: string) {
return this.userRepository.findById(id);
}
}
@Module({
providers: [UserService, UserRepository],
exports: [UserService],
})
class UserModule {}

自定义装饰器实现 DI
// 简单的基于装饰器的 DI
const METADATA_KEY = Symbol('injectable');
const container = new Map<string, any>();
function Injectable(token: string) {
return (target: any) => {
Reflect.defineMetadata(METADATA_KEY, token, target);
};
}
function register(token: string, implementation: any) {
container.set(token, new implementation());
}
function resolve<T>(token: string): T {
const instance = container.get(token);
if (!instance) throw new Error(`No binding for ${token}`);
return instance;
}
使用 DI 进行测试
// 在测试中轻松替换实现
describe('UserService', () => {
let service: UserService;
let mockRepo: jest.Mocked<UserRepository>;
beforeEach(() => {
mockRepo = {
findById: jest.fn(),
save: jest.fn(),
};
service = new UserService(mockRepo, new ConsoleLogger());
});
test('throws when user not found', async () => {
mockRepo.findById.mockResolvedValue(null);
await expect(service.getUser('unknown')).rejects.toThrow(UserNotFoundError);
});
});
DI 使代码可测试、模块化且灵活——无需修改消费者即可替换组件。