Dependency Injection в компонентах

Dependency Injection (DI) — это подход к управлению зависимостями компонентов, который позволяет отделять создание объектов от их использования. В контексте Lit этот подход обеспечивает гибкость, тестируемость и упрощает повторное использование компонентов. В Lit DI реализуется через свойства компонентов, сервисы и провайдеры контекста.


Внедрение зависимостей через свойства

В Lit компоненты наследуются от LitElement. Одним из способов внедрения зависимостей является использование свойств (@property) для передачи необходимых объектов:

import { LitElement, html, css } from 'lit';
import { property } from 'lit/decorators.js';

class UserProfile extends LitElement {
  @property({ type: Object }) userService;

  render() {
    if (!this.userService) {
      return html`<p>Сервис не подключен</p>`;
    }
    const user = this.userService.getCurrentUser();
    return html`<p>Имя пользователя: ${user.name}</p>`;
  }
}

customElements.define('user-profile', UserProfile);

В этом примере userService передаётся как зависимость извне, что позволяет тестировать компонент с разными реализациями сервиса без изменения кода компонента.


Провайдеры контекста

Lit поддерживает контекст через экспериментальные API @lit-labs/context, которые упрощают DI для глубоких деревьев компонентов. Контекст позволяет передавать зависимости вниз по иерархии без явного пробрасывания через свойства каждого компонента.

Пример создания контекста:

import { createContext, provide, consume } from '@lit-labs/context';

const UserServiceContext = createContext('user-service');

class AppRoot extends LitElement {
  userService = new UserService();

  render() {
    return html`
      ${provide(this, UserServiceContext, this.userService)}
      <user-profile></user-profile>
    `;
  }
}

class UserProfile extends LitElement {
  @consume({ context: UserServiceContext, subscribe: true })
  userService;

  render() {
    return html`<p>Имя пользователя: ${this.userService.getCurrentUser().name}</p>`;
  }
}

Ключевые моменты:

  • createContext создаёт уникальный контекст для сервиса.
  • provide передаёт сервис вниз по дереву компонентов.
  • @consume автоматически подписывается на изменения контекста и обновляет компонент при смене значения.

Сервисные классы и фабрики

Для сложных зависимостей удобно использовать сервисные классы и фабрики. Сервисный класс инкапсулирует логику, а фабрика создаёт экземпляры на основе конфигурации.

class ApiService {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
  }

  async fetchData(endpoint) {
    const response = await fetch(`${this.baseUrl}/${endpoint}`);
    return response.json();
  }
}

// Фабрика
function createApiService() {
  return new ApiService('https://api.example.com');
}

// Использование в компоненте
class DataViewer extends LitElement {
  @property({ type: Object }) apiService = createApiService();

  async firstUpdated() {
    this.data = await this.apiService.fetchData('users');
  }

  render() {
    return html`<pre>${JSON.stringify(this.data, null, 2)}</pre>`;
  }
}

Такой подход позволяет легко заменять сервисы на mock-объекты для тестирования.


Инъекция через конструктор

Иногда удобно передавать зависимости через конструктор:

class Logger {
  log(message) {
    console.log(message);
  }
}

class LogViewer extends LitElement {
  constructor(logger) {
    super();
    this.logger = logger;
  }

  connectedCallback() {
    super.connectedCallback();
    this.logger.log('Компонент подключен');
  }
}

Метод полезен для компонентов, которые создаются программно, а не через HTML-разметку.


Обновление зависимостей

В Lit можно реализовать реактивное обновление зависимостей через свойства с наблюдением:

@property({ type: Object }) userService;

updated(changedProperties) {
  if (changedProperties.has('userService')) {
    this.user = this.userService.getCurrentUser();
  }
}

Это позволяет компоненту динамически реагировать на смену сервисов без полного пересоздания.


Практические рекомендации

  • Использовать свойства для простых зависимостей и контекст для сложных иерархий компонентов.
  • Сервисы должны быть максимально независимыми и легко заменяемыми.
  • Внедрение через конструктор подходит для компонентов, создаваемых динамически, а не через HTML.
  • Реактивные свойства обеспечивают автоматическое обновление интерфейса при смене зависимостей.

DI в Lit упрощает масштабирование приложений, делает компоненты тестируемыми и гибкими. Сочетание свойств, контекста и сервисных классов позволяет организовать чистую архитектуру без жёсткой связности между компонентами и их зависимостями.