<p><i>Автор: Максим Кирпичников, ведущий NestJs разработчик ИТ-компании QTIM</i></p>

Автор: Максим Кирпичников, ведущий NestJs разработчик ИТ-компании QTIM

Dependency injection (или внедрение зависимостей) — это одна из реализаций принципа ООП под названием Inversion of Control, название которой впервые дал Мартин Фаулер. DI предоставляет общий механизм управления зависимостями, поэтому объектам не нужно знать, как и откуда они получат свои зависимости. Как во многих объектно-ориентированных языках (например, Java), внедрение зависимостей можно реализовать и в Typescript. В этой статье мы на примере мини-фреймворка для фронтенда увидим, как легко и быстро можно создать простой DI-контейнер, настроить механизм внедрения зависимостей, а также использовать для этой цели Typescript-декораторы и Reflect API.

Содержание:

  • Подготовка среды
  • DI-контейнер и базовые классы
  • Компоненты, внедрение зависимостей и метаданные
  • Декораторы
  • Заключение

Подготовка среды

Для начала создадим проект и установим необходимые зависимости:

mkdir di-typescript

cd di-typescipt

npm init

npm i -S axios navigo

npm i -D @babel/core @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/plugin-transform-runtime @babel/preset-typescript @babel/runtime @types/node babel-loader babel-plugin-transform-typescript-metadata copy-webpack-plugin css-loader css-modules-typescript-loader dotenv html-webpack-plugin mini-css-extract-plugin reflect-metadata sass sass-loader style-loader typescript webpack webpack-cli webpack-dev-server

Для работы с бэкендом будем использовать axios, а для роутинга воспользуемся библиотекой navigo. Затем создаем файл webpack.config.js, подключаем DefinePlugin и dotenv для использования переменных из .env-файла, устанавливаем правила для файлов .ts, .scss и ассетов. Настраиваем tsconfig.json, обязательно указываем «experimentalDecorators»: true.

В файле .babelrc подключим плагины:

Создадим файл .env, куда положим адрес нашего бэкенда:

На этом основная настройка закончена, можно приступать к реализации DI.

DI-контейнер и базовые классы

Самый простой DI-контейнер будет выглядеть вот так:

Все, что делает класс, — складывает экземпляры других классов в реестр и достает их оттуда, но наши объекты никогда не будут взаимодействовать с ним напрямую, за исключением базовых классов. В качестве ключей для реестра будем использовать строки со специальным префиксом di:

Далее создадим базовые классы нашего приложения. Работать с ними за пределами механизма DI будем строго через интерфейсы, чтобы случайно не затронуть внутреннюю логику, например, инициализацию. Реализуем класс Router:

А также базовый класс Api:

И небольшой класс-builder App:

App будет центральным классом приложения, из которого оно запустится. Через него будут задаваться глобальные префиксы для api и роутера, а также views — классы-компоненты страниц, которые мы реализуем позднее. Сразу пропишем запуск приложения в index.ts (для views на данном этапе можно экспортировать пустой массив):

Обращаю внимание на импорт пакета reflect-metadata, он нам понадобится для работы Reflect API. Далее, мы переходим непосредственно к внедрению зависимостей.

Компоненты, внедрение зависимостей и метаданные

Каждый объект страницы, будь то HTML-элемент или группа элементов, будет представлен в качестве класса-компонента. Этот подход частично напоминает то, как реализована работа с компонентами в фреймворке Angular, но в отличие от последнего, для упрощения понимания работы DI мы не будем делить все на модули. Также, чтобы не тратить время на парсинг html, каждый компонент для отрисовки будет использовать вызов функций через каррирование в специальном методе render. Для начала напишем фабрику для наших компонентов:

Children — это union из string | HTMLElement | (string | HTMLElement)[] | undefined, этот тип пригодится нам позже. Как мы можем видеть в примере кода, метод render подменяется «на ходу», чтобы вернуть корректный html-элемент другому компоненту, в который этот компонент будет внедрен.

Для получения зависимостей из метадаты воспользуемся Reflect API. Мы будем хранить в качестве метадаты компонента конструкторы его зависимостей, а также специальный префикс, который укажет, является ли наш класс именно компонентом, или это другой внедряемый класс (например, сервис). Также, префикс обеспечит уникальность ключа реестра для каждого внедряемого класса. Модифицируем нашу фабрику:

Теперь она наследуется от абстрактной фабрики InjectableFactory, где есть метод getArgs, предоставляющий готовые зависимости для каждой из подобных фабрик:

Как видно из примера, мы берем массив фабрик зависимостей из метадаты, а если их нет, то берем оригинальные классы из списка аргументов конструктора при помощи специального ключа design:paramtypes, а затем берем их фабрики из метадаты. Это позволит внедрять зависимости и при помощи указания класса в качестве типа аргумента, и при помощи декораторов. Перейдём теперь к реализации декораторов.

Больше интересного про кейсы QTIM — в нашем Телеграм-канале

Декораторы

Typescript декораторы — мощный инструмент, позволяющий добавлять классам и их составляющим метаданные и на ходу изменять их поведение. Мы используем их для внедрения зависимостей. Для начала реализуем декоратор Component:

Затем реализуем декоратор Inject, который позволит внедрять один компонент в другой:

Добавим новый метод в ComponentFactory:

Метод создает дубликат экземпляра класса и трансформирует его в curried-функцию, первый вызов которой записывает props в свойства экземпляра класса, а второй возвращает результат метода render. Обращаю внимание на то, что в качестве props записываются только те ключи, которые были заранее сохранены в метадате. Создадим для этих целей декоратор Prop:

Создадим компонент-кнопку:

Как мы видим из метода render, кнопка отображает либо то, что передали ей в свойстве text, либо дочерние компоненты. Тип Children может быть как обычной строкой, так и результатом вызова методов render других компонентов — это позволит без труда вкладывать один компонент в другой, как мы это делали бы, например, работая с JSX.

Напишем интерфейс для curried-функции:

Добавим компонент формы, который будет использовать наши кнопки:

За счет того, что при первом вызове каррированной функции создается копия компонента, мы увидим две разные кнопки: в одной будет текст «‎Нажми меня», а в другой будет «Переданный в props текст». Осталось сделать view-компонент, который мы передадим в роутер и с которого начнется первичный рендер страницы. Декоратор View ничем принципиально не будет отличаться от декоратора Component, он даже будет использовать ту же фабрику, но префикс для контейнера будет иметь другой, будет создавать div по умолчанию, а также будет записывать переданный роут страницы в метадату. Вот так будет выглядеть наш view-компонент:

Осталось реализовать недостающий метод setViews для роутера, который теперь будет принимать на вход массив view-компонентов:

После запуска приложения мы увидим наши кнопки:

Осталось добавить бизнес-логику нашему приложению, для этого будем использовать классы-сервисы. Добавим для них новую фабрику:

Декоратор будет принимать path для апи роута, а в остальном будет делать все то же, что и Component. Представим, что наш бэкенд имеет роут GET /some/, поэтому сделаем SomeService:

И внедрим этот сервис в компонент формы:

Теперь, когда мы снова запустим приложение и заглянем в devtools, мы увидим в консоли надпись «‎done!», а в network будет запрос на GET localhost:3000/some.

Заключение

Выше мы самостоятельно написали реализацию Dependency Injection. Конечно же, наш неоптимизированный код нельзя использовать в боевых проектах: это лишь демонстрация возможностей Typescript, а для боевых проектов есть множество готовых решений по внедрению зависимостей. Тем не менее, есть еще много идей, как использовать механизм DI, декораторы и Reflect API. Например: настроить внедрение анонимных компонентов по HTML-тегу; добавить декоратор Reactive, использующий геттеры и сеттеры для перерендера компонента при изменении декорируемого свойства; добавить декораторы событий и жизненного цикла; добавить глобальный стейт-менеджер в виде внедряемого класса; и многое другое. Возможно что-то из этого когда-нибудь пригодится, например, для понимания внутренней работы DI и декораторов.

А пока же предлагаю ознакомиться с репозиторием полной версии мини-приложения на гитхабе, который можно склонировать и поэкспериментировать. Репозиторий доступен по ссылке: https://github.com/superclaw/di-typescript.

77
Начать дискуссию