Web Components и IoC

Web Components и IoC

Замечательной особенностью веб-компонентов или скорее даже языка разметки HTML является возможность прозрачно оборачивать существующий контент новым. Благодаря этому, вы можете практически неинвазивно дорабатывать существующий legacy код, а так же изящно структурировать новый.

Говоря конкретно, если вы обернете какую-то верстку своим кастомным элементом (гугл. Custom Elements), то конфигурируясь в хуке connectedCallback() ваш элемент может определить какие у него внутри есть подэлементы и настроить свою и их работу соответствующим образом если надо приспособить их к новым требованиям и это будет вполне себе архитектурно грамотным решением. Также он будет получать всплывающие события от своих подэлементов и если вы включили изоляцию теневого дерево, он станет их безальтернативным брокером (а если не включили — прозрачным прокси), т.к. за его пределы они всплывать не будут и вся ответственность ляжет на него. 

В одной из прошлых статей «WebComponents как фреймворки взаимодействие компонентов», которую вы можете прочитать по адресу https://habr.com/ru/post/461153/ мы уже разобрались как организовать взаимодействие между компонентами используя события браузера. 

Но что делать если требуется не прямо обусловленное иерархией дерева объектов взаимодействие ?

Многие решения, из тех, что я встречал в последнее время предлагали использовать статический контекст. Т.е. если вам надо добавить какую-то зависимость всем вашим кнопкам вы просто присваиваете его в соответствующее свойство декларации класса. Но опыт разработки для разных языков и платформ подсказывает, что использование статического контекста как и прочие глобалы любого сорта не является особенно удачным решением, особенно, может быть, для динамических языков типа джаваскрипта. Т.к. такое присваивание зависимости может произойти где и когда угодно, также как и обращение к зависимости, в том числе вероятны ситуации, когда обращение произойдет до присваивания/инициализации, когда произойдет перезапись другим значением и обращение к нему по старой взаимосвязи т.е. вероятное нарушение контракта и целостности, кроме того этот статически-динамический конфигурирующий код может оказаться сложным отыскать в совокупной базе кода. 

В некоторых других зарекомендовавших себя как довольно таки доброкачественные технологиях для решения всех этих проблем применяются так называемые IoC-контейнеры, т.е. реестры зависимостей с прилагающейся абстрактной процедурой связывания компонентов. Широко такие контейнеры распространены в технологиях Java вплоть до того, что их считают сугубо энтепрайзной или языковой фичей. Мол в питоне управлять зависимостями не обязательно т.к. и так все просто и легко и всегда работает (если не сломаны табы;). Кроме автоматизации инициализационных процессов, реестры решают и проблемы переиспользования параметров конфигураций, помогая устранять не только дублирование кода, но и используемых в конфигурационных декларациях, которые также могут разделяться между компонентами выступая своего рода переиспользуемыми зависимостями. Например, у вас есть некоторый параметр HOST, который вы можете везде в 20 местах захардкодить, да еще и везде немного по-своему, а можете использовать заданное однажды значение в виде переменной, которая будет автомагически присвоена всем нуждающимся, чтобы у них был сразу какой надо HOST когда вы его поменяете, а не в зависимости от успешности Search & Replace. 

Есть у подобных решений и недостатки, такие как недостаточная детерминированность результата процедуры сканирования зависимостей и их связывания, которая может производиться при старте приложения, что влечет уход многих потенциальных ошибок в рантайм что плохо. Например, когда на роль зависимости неожиданно оказывается более одного кандидата, или наоборот ни одного, или кандидат есть как положено, но его API уже или еще не соответствует ожиданиям клиента из-за разницы версий (самая “любимая” ошибка в Spring Boot).  

Знакомы нам и решения где эта проблема, можно сказать, преодолена, это конфигурации модулей в фреймворках Angular и возможно Composite API в Vue. JSX в React и его Context API также можно считать контейнером и системой управления зависимостями “для бедных”, т.к. там оно запаяно в иерархию вложенности компонентов и вынуждает дублировать параметры или проявлять экзерсисы структурирования в силу некоторой ограниченности структуры дерева. Если использовать IoC контейнер и его конкретный инструмент “инжектор” зависимостей, даже в реакте код выглядит гораздо благонадежнее и понятнее.

Вот как раз такой открученный от Angular инжектор я и использовал вместе с веб-компонентами, чтобы получить возможность красиво и централизованно  структурировать иерархии зависимостей и так же подставлять в них параметры конфигурации. Библиотека называется injection-js и хороша всем кроме некоторой древней многословности если вы не используете аннотаций typescript. Иначе говоря, чтобы добавить зависимость на понятном ей языке надо написать фабрику, сеттер, декларацию и всякое такое. Поэтому я написал свой контейнер-инжектор, который к заодно умеет регистрировать веб-компоненты и выходит это у него вовремя. 

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

Чтобы понять все эти выкладки по-настоящему нужна практика. Давайте уже кодировать ! Создадим новый проект выполнив к консоли что-нибудь вот такое:

mkdir testioc
cd testioc
npm init

установим сразу библиотеку виджетов на веб-компонентах

npm i skinny-widgets —save

создадим файл index.html

в нем в body добавим базовую декларацию инжектора

<sk-config
theme=»antd»
base-path=»/node_modules/skinny-widgets/src»
></sk-config>

<script src=»/node_modules/skinny-widgets/dist/sk-compat-bundle.js»></script>
<script type=»module»>
import { Registry } from «/node_modules/skinny-widgets/complets/registry.js»;

window.registry = window.registry || new Registry();

registry.wire({
SkConfig: { def: SkConfig, is: ‘sk-config’},
SkButton: { def: SkButton, is: ‘sk-button’},
});
</script>

тег sk-config здесь выступает конфигом для библиотеки виджетов, это необходимость, что-бы не повторять аттрибуты у каждого элемента. Далее подключается статический бандл библиотеки, но без специальных атрибутов она свои компоненты не зарегистрирует (хотя в глобале появялся декларации их классо) и мы делаем это с помощью объявления реестра компонентов в котором указан элемент конфига и кнопки, а атрибут is указывает под каким тегнеймом эти элементы регистрировать в реестре customElements. Вместо бандла можно также подключать все модульно импортами, я хотел показать, что это все можно смешивать при необходимости.

Конструкция window.registry = window.registry || new Registry(); делает наш реестр модульным, если он уже был определен ранее, то будет расширен новой декларацией, если нет — проинициализирован “с нуля”.

Чтобы убедиться что все работает добавим саму кнопку

<sk-button>Open</sk-button>

Сделаем сервер для нашего кода выполнив к каталоге с проектом следующую команду

npx http-server

откроем http://127.0.0.1:8080 в браузере и убедимся что кнопка отрендерилась

Web Components и IoCРаботающая кнопка

Но пока что это как из пушки по воробьям, давайте наш пример немного усложним такой постановкой: кнопка должна открывать диалог.

Добавим сначала его.

<script type=»module»>
import { Registry } from «/node_modules/skinny-widgets/complets/registry.js»;
import { MyDialog } from «./my-dialog.js»;

window.registry = window.registry || new Registry();

registry.wire({
SkConfig: { def: SkConfig, is: ‘sk-config’},
SkButton: { def: SkButton, is: ‘sk-button’},
myDialog: { def: MyDialog, is: ‘my-dialog’ },
});
</script>

<sk-button>Open</sk-button>
<my-dialog>This is my dialog</my-dialog>

Диалог будет нашим собственным классом унаследованным от класса SkDialog из библиотеки виджетов, чтобы мы могли в нем все переопределять как нам надо. Создадим файл my-dialog.js с пустым пока классом.

export class MyDialog extends SkDialog {

}

Теперь нам надо связать кнопку и диалог так чтобы при нажатии кнопки он открывался. Конечно, это можно было бы сделать “проще” выбрав кнопку по айди или другому селектору и добавив ей обработчик события click в котором бы вызывался метод open() у диалога. Но допустим нам хочется чтобы в HTML не было никакой вообще бизнес-логики, а были только конфигурации как максимум, чтобы код был более поддерживаемым.

Для реализации столь амбициозной задачи нам потребуется создать класс-представление MyView на основе тех же customElements. В этом классе мы все и свяжем. Обернем кнопку и диалог новым элементом my-view.

 Добавим файл my-view.js и пусть в нем сразу будет связывающий код:

export class MyView extends HTMLElement {

bindDialog() {
if (this.dialogCfg) {
let button = this.querySelector(this.dialogCfg.buttonSl);
let dialog = this.querySelector(this.dialogCfg.dialogSl);
if (button) {
button.addEventListener(‘click’, (event) => {
dialog.open();
});
}
}
}

render() {
this.bindDialog();
}

connectedCallback() {
this.render();
}
}

Много кода.. давайте разбираться.

Тут мы можем заметить, что для инициализации всего используется хук веб-компонентов connectedCallback(), который срабатывает когда элемент распознается браузером как кастомный и зарегистрированный.

В нем мы вызываем метод render(), в котором в свою очередь bindDialog() — все чтобы было понятно что происходит и не перемешивалось в вермишель если логика усложнится.

В bindDialog() селекторами выбираются кнопка и диалог и вешается тот самый обработчик события click в котором открывается диалог. Но обратите внимание, что значения селекторов берутся из неведомого нам пока еще свойства 

this.dialogCfg

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

<script type=»module»>
import { Registry } from «/node_modules/skinny-widgets/complets/registry.js»;
import { MyDialog } from «./my-dialog.js»;
import { MyView } from «./my-view.js»;

window.registry = window.registry || new Registry();

registry.wire({
SkConfig: { def: SkConfig, is: ‘sk-config’},
SkButton: { def: SkButton, is: ‘sk-button’},
dialogCfg: { buttonSl: ‘#dialogButton’, dialogSl: ‘#dialog’ },
myDialog: { def: MyDialog, is: ‘my-dialog’ },
myView: { def: MyView, deps: { dialogCfg: ‘dialogCfg’ }, is: ‘my-view’ },
});
</script>

<my-view>
<sk-button id=»dialogButton»>Open</sk-button>
<my-dialog id=»dialog»>This is my dialog</my-dialog>
</my-view>

В хешмапе будут указаны айди элементов, которые мы им добавим. Собственно говоря, этого достаточно, чтобы кнопка начала открывать диалог. Проверим в браузере:

Web Components и IoCРезультат

Теперь если мы будем развивать функционал и усложнять логику, можно не опасаться, что все это добро станет километровой простыней говнокода в HTML или будет  использовать статическую инициализацию или идентификаторы, которые потом будет сложно отрефакторить. С другой стороны все наши конфигурации остались в HTML, а не захардкожены в javascript коде и мы можем легко и централизованно менять их вместе с версткой. Кроме того, нам совсем не потребовалось компилировать код транспиляторами и он при этом не выглядит как страшное легаси.

Готовый код примера можно взять на репозитории в битбакете: https://bitbucket.org/techminded/testioc.git

Другие мои статьи про веб-компоненты:

Роутинг для веб-компонентов — https://habr.com/ru/post/496924/ 

WebComponents как фреймворки, взаимодействие компонентов — https://habr.com/ru/post/461153/

Material как WebComponents — https://habr.com/ru/post/462695/

Быстрый старт с WebComponents — https://habr.com/ru/post/460397/

Рубрики: Новости
×
Обсудить проект
Заполните форму и мы с Вами свяжемся
×