awilix для TypeScript: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Awilix для TypeScript: Полное руководство по DI-контейнеру

Awilix — это мощный и гибкий контейнер внедрения зависимостей (Dependency Injection, DI) для Node.js и браузеров, написанный на TypeScript. Он предоставляет элегантный способ управления зависимостями в вашем приложении, делая код более модульным, тестируемым и поддерживаемым. В этой статье мы подробно разберем, как использовать Awilix в проектах на TypeScript, от установки до продвинутых техник.

1. Что это и зачем нужна

В современной разработке программного обеспечения принцип инверсии управления (IoC) и внедрение зависимостей (DI) стали стандартом де-факто для создания масштабируемых и тестируемых приложений. Awilix — это реализация DI-контейнера, которая берет на себя создание и управление жизненным циклом объектов (сервисов, репозиториев, контроллеров и т.д.). Вместо того чтобы вручную создавать экземпляры классов внутри других классов (что приводит к сильной связанности), вы регистрируете свои зависимости в контейнере, а он автоматически "внедряет" их туда, где они нужны.

Зачем это нужно? Во-первых, это радикально упрощает тестирование. Вы можете легко подменить реальную реализацию (например, базу данных) на mock-объект во время юнит-тестов, просто зарегистрировав другую зависимость в контейнере. Во-вторых, это улучшает архитектуру приложения, заставляя вас следовать принципу единственной ответственности (SRP) и явно объявлять свои зависимости. В-третьих, Awilix поддерживает различные "стили жизни" (lifetime) объектов (синглтон, временный, скоуп), что позволяет гибко управлять памятью и состоянием.

Awilix особенно хорош для TypeScript благодаря своей строгой типизации. Он предоставляет встроенные типы для регистрации и разрешения зависимостей, что позволяет отлавливать ошибки на этапе компиляции, а не в рантайме. Библиотека поддерживает асинхронное разрешение зависимостей, работу с классами, фабричными функциями и уже существующими экземплярами объектов.

2. Установка

Для установки Awilix в ваш проект на TypeScript выполните следующую команду в терминале:

npm install awilix

Awilix имеет встроенную поддержку TypeScript, поэтому вам не нужно устанавливать отдельные пакеты с типами (@types/awilix). Все типы уже включены в основную библиотеку.

3. Быстрый старт — минимальный рабочий пример

Давайте создадим простейшее приложение, которое приветствует пользователя. У нас будет класс Greeter, который зависит от Logger.

import { createContainer, asClass, Lifetime } from 'awilix';

// 1. Определяем зависимости
class Logger {
  log(message: string): void {
    console.log(`[LOG]: ${message}`);
  }
}

class Greeter {
  private logger: Logger;

  // Зависимость будет внедрена через конструктор
  constructor({ logger }: { logger: Logger }) {
    this.logger = logger;
  }

  greet(name: string): void {
    this.logger.log(`Hello, ${name}!`);
  }
}

// 2. Создаем контейнер
const container = createContainer();

// 3. Регистрируем зависимости
container.register({
  logger: asClass(Logger, { lifetime: Lifetime.SINGLETON }),
  greeter: asClass(Greeter, { lifetime: Lifetime.TRANSIENT }),
});

// 4. Разрешаем (получаем) экземпляр Greeter
const greeter = container.resolve<Greeter>('greeter');

// 5. Используем
greeter.greet('World'); // Вывод: [LOG]: Hello, World!

В этом примере мы создали контейнер, зарегистрировали два класса (Logger как синглтон и Greeter как временный объект), а затем разрешили Greeter. Awilix автоматически создал экземпляр Logger и передал его в конструктор Greeter.

4. Основные методы/функции/классы

Рассмотрим ключевые API Awilix. Для каждого метода приведена сигнатура, описание и пример.

4.1. createContainer()

Сигнатура: createContainer(options?: AwilixContainerOptions): AwilixContainer

Что делает: Создает новый экземпляр DI-контейнера. Это главная точка входа в библиотеку.

import { createContainer } from 'awilix';

const container = createContainer();
// Можно передать опции, например, для кастомного резолвера имен
const containerWithOptions = createContainer({
  injectionMode: 'CLASSIC' // или 'PROXY'
});

4.2. container.register()

Сигнатура: register(name: string, registration: Registration): AwilixContainer (или register(registrations: Record<string, Registration>): AwilixContainer)

Что делает: Регистрирует одну или несколько зависимостей в контейнере. Принимает либо пару "имя-регистрация", либо объект с множеством регистраций.

import { createContainer, asClass, asFunction, asValue } from 'awilix';

const container = createContainer();

// Регистрация одного класса
container.register('myService', asClass(MyService));

// Регистрация нескольких зависимостей сразу
container.register({
  config: asValue({ apiUrl: 'https://api.example.com' }),
  createUser: asFunction(({ config }) => new UserService(config)),
});

4.3. container.resolve()

Сигнатура: resolve<T>(name: string): T

Что делает: Разрешает (создает или возвращает) экземпляр зарегистрированной зависимости по её имени. Это основной способ получения объектов из контейнера.

import { createContainer, asClass } from 'awilix';

class Database {
  connect() { console.log('Connected!'); }
}

const container = createContainer();
container.register('db', asClass(Database));

const dbInstance = container.resolve<Database>('db');
dbInstance.connect(); // 'Connected!'

4.4. asClass()

Сигнатура: asClass(Constructor: Class<T>, opts?: RegistrationOptions): Registration

Что делает: Создает регистрацию на основе класса. Awilix будет создавать новые экземпляры класса, автоматически внедряя зависимости в конструктор. Опции позволяют задать время жизни (lifetime), режим инъекции и другие параметры.

import { createContainer, asClass, Lifetime } from 'awilix';

class EmailService {
  send(email: string) { /* ... */ }
}

const container = createContainer();
container.register('emailService', asClass(EmailService, {
  lifetime: Lifetime.SINGLETON, // Один экземпляр на всё приложение
  injectionMode: 'CLASSIC' // Внедрение через параметры конструктора
}));

4.5. asFunction()

Сигнатура: asFunction(fn: Function, opts?: RegistrationOptions): Registration

Что делает: Регистрирует фабричную функцию. Каждый раз при разрешении зависимости будет вызываться эта функция. Это полезно для создания объектов, требующих сложной логики инициализации или для интеграции с кодом, не основанным на классах.

import { createContainer, asFunction } from 'awilix';

const container = createContainer();

container.register('getCurrentTime', asFunction(() => {
  return new Date().toISOString();
}));

const time = container.resolve<string>('getCurrentTime');
console.log(time); // 2024-01-15T10:30:00.000Z

4.6. asValue()

Сигнатура: asValue(value: any): Registration

Что делает: Регистрирует уже существующее значение (объект, строку, число и т.д.) как зависимость. Это идеально подходит для конфигураций, констант или внешних экземпляров (например, подключение к базе данных).

import { createContainer, asValue } from 'awilix';

const container = createContainer();
const appConfig = { port: 3000, env: 'development' };

container.register('config', asValue(appConfig));

const config = container.resolve<typeof appConfig>('config');
console.log(config.port); // 3000

4.7. container.createScope()

Сигнатура: createScope(): AwilixContainer

Что делает: Создает дочерний контейнер (scope). Дочерний контейнер наследует все регистрации от родителя, но может переопределять их. Зависимости с Lifetime.SCOPED будут создаваться один раз на каждый scope. Это полезно для обработки HTTP-запросов (один scope = один запрос).

import { createContainer, asClass, Lifetime } from 'awilix';

class RequestContext {
  userId: number = 0;
}

const container = createContainer();
container.register('requestContext', asClass(RequestContext, {
  lifetime: Lifetime.SCOPED
}));

// Для каждого HTTP-запроса создаем новый scope
function handleRequest(userId: number) {
  const requestScope = container.createScope();
  const ctx = requestScope.resolve<RequestContext>('requestContext');
  ctx.userId = userId;
  // ... обработка запроса
}

4.8. container.hasRegistration()

Сигнатура: hasRegistration(name: string): boolean

Что делает: Проверяет, зарегистрирована ли зависимость с указанным именем в контейнере (или в его родительских контейнерах). Полезно для условной логики или валидации.

import { createContainer, asValue } from 'awilix';

const container = createContainer();
container.register('db', asValue({}));

if (container.hasRegistration('db')) {
  console.log('Database is registered!');
}

if (!container.hasRegistration('cache')) {
  console.log('Cache is not registered, registering default...');
  container.register('cache', asValue(new Map()));
}

4.9. container.dispose()

Сигнатура: dispose(): Promise<void>

Что делает: Асинхронно освобождает все ресурсы, связанные с контейнером. Вызывает методы dispose() на всех зарегистрированных объектах, которые его реализуют. Важно вызывать при завершении работы приложения для предотвращения утечек памяти.

import { createContainer, asClass, Lifetime } from 'awilix';

class DatabaseConnection {
  async connect() { /* ... */ }
  async dispose() {
    console.log('Closing database connection...');
    // Закрываем соединение
  }
}

async function main() {
  const container = createContainer();
  container.register('db', asClass(DatabaseConnection, {
    lifetime: Lifetime.SINGLETON
  }));

  const db = container.resolve<DatabaseConnection>('db');
  await db.connect();

  // ... работа приложения

  await container.dispose(); // 'Closing database connection...'
}

main();

5. Реальный практический пример (100-200 строк кода)

Давайте создадим мини-приложение для управления пользователями. Оно будет состоять из контроллера, сервиса и репозитория, работающего с "базой данных" (в памяти).

import {
  createContainer,
  asClass,
  asValue,
  Lifetime,
  AwilixContainer
} from 'awilix';

// --- Модели ---
interface User {
  id: number;
  name: string;
  email: string;
}

// --- Репозиторий (работа с данными) ---
class UserRepository {
  private users: User[] = [];
  private nextId = 1;

  async findAll(): Promise<User[]> {
    return this.users;
  }

  async findById(id: number): Promise<User | undefined> {
    return this.users.find(u => u.id === id);
  }

  async create(userData: Omit<User, 'id'>): Promise<User> {
    const newUser: User = { id: this.nextId++, ...userData };
    this.users.push(newUser);
    return newUser;
  }
}

// --- Сервис (бизнес-логика) ---
class UserService {
  private userRepository: UserRepository;

  constructor({ userRepository }: { userRepository: UserRepository }) {
    this.userRepository = userRepository;
  }

  async getAllUsers(): Promise<User[]> {
    return this.userRepository.findAll();
  }

  async getUserById(id: number): Promise<User | undefined> {
    return this.userRepository.findById(id);
  }

  async createUser(name: string, email: string): Promise<User> {
    // Простейшая валидация
    if (!name || !email) {
      throw new Error('Name and email are required');
    }
    return this.userRepository.create({ name, email });
  }
}

// --- Контроллер (обработка запросов) ---
class UserController {
  private userService: UserService;

  constructor({ userService }: { userService: UserService }) {
    this.userService = userService;
  }

  async handleListUsers(): Promise<User[]> {
    console.log('Handling list users request...');
    return this.userService.getAllUsers();
  }

  async handleCreateUser(name: string, email: string): Promise<User> {
    console.log(`Handling create user request for ${name}...`);
    return this.userService.createUser(name, email);
  }
}

// --- Настройка контейнера и запуск ---
async function bootstrap() {
  // 1. Создаем контейнер
  const container: AwilixContainer = createContainer();

  // 2. Регистрируем все зависимости
  container.register({
    // Репозиторий - синглтон, чтобы данные сохранялись
    userRepository: asClass(UserRepository, {
      lifetime: Lifetime.SINGLETON
    }),
    // Сервис - временный (можно и синглтон, но для примера)
    userService: asClass(UserService, {
      lifetime: Lifetime.TRANSIENT
    }),
    // Контроллер - временный
    userController                    

Рекомендаций