С годами мы, разработчики пользовательского интерфейса, стали настолько привычными использовать уже доступные UI Framework и библиотеки, такие как React, Angular, Vue и т. Д., Из-за растущего спроса на быструю доставку. И главное преимущество этих фреймворков заключается в том, что они не только эффективно решают эту проблему, но и предоставляют нам сложный интерфейс и API, скрывая при этом детали нижнего уровня. Детали настолько скрыты, что с годами мы как бы успокаивались, принимая «волшебство», происходящее за кулисами, рендеринг нашего Angular или React-компонента, который мы только что создали, в машинописном тексте.

Повестка дня

Цель этой статьи не в изучении того, как работают внутренние механизмы этих фреймворков. Скорее, я бы обсудил сценарий, в котором предположим, что этих фреймворков на рынке нет, и если бы вы сегодня писали компоненты, используя простой VanillaJS и веб-стандарты, как бы вы это сделали, а также некоторую сложность внедрения управляемые динамической конфигурацией и реактивные компоненты. На протяжении всей статьи мы рассмотрим, сколько мы платим за неиспользование сложной структуры и как вы должны думать об обходных путях, чтобы справиться с работой самостоятельно. Вот ссылка репо https://github.com/paramsinghvc/dynamic-form-web-components. Не стесняйтесь форкнуть и расширить его функциональность.

Веб-компоненты

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

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

Спецификация веб-компонентов основана на следующих трех столпах:

  1. Пользовательские элементы: теперь мы можем создавать наши собственные HTML-теги, такие как <my-element>, с набором API, предоставленным для них. Точно так же, как мы создаем компонент React или Angular, чтобы изолировать часть функциональности и выполнить желаемое поведение при каком-либо событии, мы можем использовать API пользовательских элементов, чтобы сделать то же самое, не используя какой-либо сторонний фреймворк.
  2. Shadow DOM: Shadow DOM - отличный способ инкапсулировать DOM и CSSOM нашего веб-компонента, чтобы он не мешал и не мешал окружающим или предшествующим элементам. Другими словами, он не допускает утечки стилей из родительского дерева в дочерние деревья поддомов и наоборот. Вариант использования лучше всего подходит, когда вы хотите создать подключаемый компонент, например окно поддержки чата, внутри приложения, чтобы его тематика и стили не зависели от основного приложения (помните, как использовать iframe для того же?)

И знаете ли вы, что уже существующие элементы html также имеют встроенный теневой дом, вы можете включить их, чтобы увидеть внутри devtools, перейдя в настройки → элементы → показать теневой объект пользовательского агента

3. Тег шаблона: теги <template> и <slot> могут использоваться для создания разметки, которая может быть загружена в существующую модель DOM по запросу, почти так же, как мы создаем ручки управления, но, к сожалению, <template> не поддерживает интерполяцию ({{title}} ) динамических переменных.

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

Свойство content тега шаблона возвращает фрагмент документа того, что находится внутри тега. Фрагмент документа - это набор узлов dom, в которые внесены какие-либо изменения, такие как добавление или удаление, не вызовет перекомпоновки отрисовки или макета, если они не добавлены в dom. Следовательно, мы можем определить разметку нашего настраиваемого элемента, используя его, чтобы он повторно использовался во всех наших экземплярах компонентов.

Разработка через конфигурацию (CDD)

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

  • Во-первых, он предоставляет общий интерфейс для разработки вещей, которые помогают хорошо масштабировать ваш проект.
  • Это экономит время и силы на разработку. Вместо обязательной разработки новых страниц или модулей путем написания кода для каждого модуля самостоятельно, вы можете просто настроить модуль или страницу.
  • Конфигурацию можно отделить от базы кода внешнего интерфейса, и, следовательно, любые изменения, которые потребуются в будущем, не потребуют развертывания в пользовательском интерфейсе.
  • Поскольку конфигурация поддерживается в бэкэнде как JSON, группы разработчиков продукта могут легко экспериментировать, просто изменив конфигурацию, но да, включение нового элемента или виджета, конечно же, потребует написания кода для него в пользовательском интерфейсе.
  • Централизованный код для получения и рендеринга конфигурации будет более надежным, поскольку любые ошибки будут централизованы только для этого кода. В отличие от сценария, в котором у нас были разные кодовые базы для разных модулей.

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

Вот как выглядит конфигурация

Конфигурационный парсер

Приведенную выше конфигурацию необходимо прочитать и перевести в веб-компоненты, чтобы они отображались как

Переданная конфигурация зацикливается, и для каждой записи в конфигурации мы определяем, какой компонент будет отображаться, на основе переданного типа. Есть COMPONENTS_MAPPING, который нам нужно поддерживать, чтобы сопоставить тип компонента с фактическим именем тега веб-компонента. Как только мы получим, что создаем этот элемент в JS, используя document.createElement и т.д., который скрыт за вспомогательным методом createNode(nodeName, options) в приведенном выше фрагменте.

Помимо этого, необходимо сохранить formValidations карту, которая представляет собой не что иное, как сопоставление elementId с текущим состоянием проверки формы. Например: элемент формы возраста в любой точке может содержать проверки требуемых, выходящих за пределы диапазона, не числа и т. Д. Все это можно сохранить внутри этой карты.

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

Реактивные компоненты

Теперь о том, как мы хотим, чтобы наши компоненты были динамичными и достаточно реактивными, чтобы реагировать на любые изменения в так называемых «свойствах», которые мы им передаем.

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

То же самое и с Angular, где мы указываем декоратор @Input() для всех входных свойств данных компонентов. Делая это, все фреймворки поддерживают отдельное пространство в памяти для внутреннего обслуживания, такого как ведение реестра свойств данного компонента и проверка предыдущих свойств и следующих свойств, а затем реагирование на любые изменения, если они обнаружены, путем повторного рендеринга части. для рендеринга. Следовательно, они всегда следят за тем, чтобы вид компонента всегда синхронизировался с состоянием или моделью компонента. Это главный аргумент в пользу этих сложных фреймворков. Потому что изначально в vanilla JS не было возможности определять изменения в ответ на свойства компонента.

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

  • Использование API Mutation Observer
  • Api веб-компонентов attributeChangedCallback(attrName, oldValue, newValue)

Создание пользовательских компонентов

Давайте расширим HTML нашими собственными элементами формы.

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

Поскольку мы уже много лет используем такие библиотеки, как React и Angular, мы ожидаем, что наши веб-компоненты будут вести себя так же, как и те, что созданы с помощью React. Но здесь мы не будем тратить время на создание элегантного уровня нашей собственной компиляции, привязки данных, согласования (обнаружения изменений). Скорее, моя цель здесь - использовать существующие веб-интерфейсы API из коробки.

Итак, для обнаружения изменений на основе реквизита, attributeChangedCallback кажется хорошим (и только: P) подходящим. Следовательно, мы производим все динамические свойства компонентов как атрибуты, и нам нужно сообщить веб-компоненту о том, какие свойства он должен соблюдать, определяя для него свойство статического получателя как

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

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

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

Здесь представление - это наш компонент, состояние - это входной атрибут, доступ к которому осуществляется через геттер this.value, а действие - это событие изменения, которое мы запускаем, когда значение изменяется посредством ввода пользователем в текстовое поле. Следовательно, в этом примере мы пока просто ретранслируем ввод и вывод в и из нашего поля ввода внутри него.

Валидации

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

Это выглядит так в виде карты.

Обратите внимание, что здесь лучше использовать карту ES6, чтобы избежать ненужной загрузки на нее прототипа объекта. Но я не использовал из-за стрингификации JSON.

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

Теперь, когда мы обновили карту formValidations, нам нужно передать ее как атрибут нашему пользовательскому компоненту, но подождите секунду, пока вы не увидите это

Ой! Атрибуты HTML становятся строковыми, поэтому мы не можем передавать в них объекты или массивы. Следовательно, здесь необходимо выполнить дополнительный шаг по упаковке и разворачиванию в JSON и обратно. Это то, что меня немного разочаровало при работе с веб-компонентами, потому что мы так привыкли передавать непримитивные структуры данных в наши компоненты и из них, что мы также сочли это инстинктивным.

Подумаешь! Мы можем сделать это, определив, имеет ли передаваемое значение атрибута непримитивный тип. (Учтите, что массив typeof также является объектом). Хотя это не очень надежный способ идентификации, поскольку создание строки с использованием new String('') даст typeof как true.

Теперь следующий шаг - задействовать ловушку attributeChangedCallback, предусмотренную в API жизненного цикла веб-компонентов, и прослушать изменения, происходящие для данного атрибута, и соответствующим образом отреагировать путем повторного рендеринга желаемой части dom. Здесь, в случае проверок, мы хотели бы отобразить <ul>, который мы создали как родственного брата нашему входному компоненту. Нам нужно проанализировать строковые значения атрибутов json и неглубоко сравнить их, чтобы затем инициировать изменения, иначе будет много повторных отрисовок.

Мы реагируем на атрибут проверки, проверяя, является ли он пустым объектом. Если он пуст, это означает, что ошибок проверки нет, и нам нужно удалить ранее отображенные ошибки ui или повторно визуализировать div с ошибками и соответственно применить стиль к элементу ввода, например, красную границу.

Функция renderErrors выглядит так. Я использую documentFragment, чтобы сначала добавить все узлы dom, а затем сразу добавить их к исходному dom в конце.

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

Уроки выучены

Ниже приведены некоторые уроки, извлеченные из моего желаемого стремления полностью использовать ванильный JS для создания динамических и реактивных приложений, которые мы создаем сегодня:

  • Трудно передавать данные в компоненты из-за дополнительного уровня стрингификации и де-стрингификации json.
  • Вы должны полагаться на setAttribute и getAttribute для «свойств» компонента, поскольку attributeChangedCallback - единственный способ обнаружить изменения.
  • Если я вспомню о привязках данных angular или vue и о том, как они работают на самом деле. У этих фреймворков есть компиляторы или анализаторы dom, которые ищут эти привязки и обновляют пользовательский интерфейс в ответ на любое изменение связанных свойств (модели). Но здесь мы должны явно написать логику обновления для обработки каждого изменения свойства / атрибута, точно так же, как мы сделали для validations ключа, каждый раз вызывая renderErrors, что намного превосходит оптимизированный подход по сравнению с ключами React или Vue или trackBy angular.

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

Заключение

Мы изучили извращенный способ создания веб-компонентов, поклявшись не использовать какие-либо сторонние устройства, и мы столкнулись с некоторыми препятствиями и обсудили, как их решить, сохраняя при этом нетривиальную формулировку проблемы. Полагаться на веб-компоненты для создания приложения, готового к производству, было бы сильным заявлением, потому что я все еще считаю, что оно находится в незрелом состоянии. Я просто задавался вопросом о грядущих днях, когда веб-API будут достаточно зрелыми, чтобы позволить нам создавать то, что мы делаем, используя сторонние фреймворки сегодня. Утопия для Интернета, которую я могу себе представить, будет иметь богатые возможности создания компонентов и управления ими, встроенные как часть спецификации и браузеров, реализующих их. Следовательно, нам не нужно включать какие-либо тяжелые сторонние библиотеки, которые сократят время загрузки для пользователей за счет огромной экономии затрат на загрузку.

Тем не менее, вот повод подбодрить. Веб-компоненты не зависят от платформы, и если вы думаете о создании компонентов, не зависящих от платформы, со всеми удобствами и простотой API, наравне с имеющимися у нас сегодня фреймворками, мы можем использовать для этого Polymer или Stencil.

использованная литература