正在加载,请稍候…

Node.js 中的依赖注入与 IoC 容器

在 Node.js 应用中实现依赖注入,通过实际示例对比手动 DI、tsyringe、InversifyJS 和 NestJS IoC 容器。

Node.js 中的依赖注入与 IoC 容器

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);

Node.js 中的依赖注入与 IoC 容器示意图

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);

Node.js 中的依赖注入与 IoC 容器示意图

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 {}

Node.js 中的依赖注入与 IoC 容器示意图

自定义装饰器实现 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 使代码可测试、模块化且灵活——无需修改消费者即可替换组件。