Упражнение по разработке программного обеспечения, демонстрирующее преимущества веб-компонентов
Веб-компоненты — это набор стандартизированных технологий, представленных Консорциумом World Wide Web (W3C) в 2011 году с целью создания повторно используемых и совместимых компонентов для Интернета.
Именно так ChatGPT определяет веб-компоненты, которые существуют уже более десяти лет, но стали по-настоящему доступными в 2021 году, когда нативная поддержка наконец-то распространилась на Safari как на последний в своем классе. Возможно, по этой причине до сегодняшнего дня они использовались ограниченно. Готов поспорить, что большинство из нас никогда не использовали веб-компоненты в проекте и, вероятно, даже не думали об их использовании.
Тем не менее, я думаю, что они заслуживают внимания, потому что у них есть потенциальные преимущества при создании веб-приложения по сравнению с другими методами.
Посмотрите, например, на список скриптов, включенных в веб-приложение, над которым мне довелось работать:
<script type="text/javascript" src="lib/jquery.min.js"></script> <script type="text/javascript" src="lib/jquery.easyui.min.js"></script> <script type="text/javascript" src="lib/js.cookie.js"></script> <script type="text/javascript" src="lib/w2ui-1.5.rc1-custom.js?v=5"></script> <script type="text/javascript" src="lib/datagrid-filter.js"></script> <script type="text/javascript" src="lib/bootstrap.min.js"></script> <script type="text/javascript" src="lib/handlebars.min.js"></script> <script type="text/javascript" src="lib/jquery.price_format.min.js"></script> <script type="text/javascript" src="lib/alpaca.min.js"></script> <script type="text/javascript" src="lib/moment-with-locales.min.js"></script> <script type="text/javascript" src="lib/moment.min.js"></script> <script type="text/javascript" src="lib/bootstrap-datetimepicker.min.js"></script> <script type="text/javascript" src="lib/bootstrap-multiselect.js"></script> <script type="text/javascript" src="lib/fullcalendar.min.js"></script> <script type="text/javascript" src="lib/jquery.ambiance.js"></script> <script type="text/javascript" src="lib/bloodhound.min.js"></script> <script type="text/javascript" src="lib/typeahead.bundle.min.js"></script> <script type="text/javascript" src="lib/Chart.min.js"></script> <script type="text/javascript" src="lib/chartjs-plugin-labels.js"></script> <script type="text/javascript" src="lib/summernote.min.js"></script> <script type="text/javascript" src="lib/jquery.justifiedGallery.js"></script> <script type="text/javascript" src="lib/jquery.twbsPagination.js"></script>
Эквивалентный список таблиц стилей сопровождал этот. Пользовательский интерфейс, полученный в результате этого потока компонентов, действительно был красивым и удобным для пользователя, но состав программного обеспечения вызывает некоторые опасения.
Программисты выбрали различные компоненты, исходя из функциональных и эргономических соображений. Это виджеты из разных источников, некоторые основаны на CSS, другие больше основаны на преобразованиях JavaScript, с большей или меньшей сложностью в зависимости от случая. Ни один из них не является стандартным веб-компонентом.
Одна из проблем, возникающих при использовании этого метода, заключается в том, что в HTML-коде непросто определить происхождение данного элемента. Например, не сразу становится понятно, что параметры данных в
<div data-options="region:'north'" class="banner">
относится к объекту Layout в библиотеке jeasyui или узнать, что классы в
<div id="menu-user" class="dropdown-menu dropdown-menu-right hide"
взяты из bootstrap.css.
Я задался вопросом, есть ли способ улучшить этот нерегулируемый метод комбинирования элементов, не отказываясь от наших любимых компонентов, даже если они не соответствуют единому стандарту. Поэтому в этой статье я объясню на двух примерах, как мы можем обернуть виджеты старого стиля в стандартные веб-компоненты и какие преимущества мы получим.
Простой выбор даты
На анимации выше показаны два виджета календаря, созданные с помощью библиотеки js-datepicker, которые позволяют выбирать диапазон дат. Ниже приведена соответствующая HTML-страница:
<!DOCTYPE html> <html> <head> <title>js-datepicker example</title> <link rel="stylesheet" href="https://unpkg.com/js-datepicker/dist/datepicker.min.css"> </head> <body> <script src="https://unpkg.com/js-datepicker"></script> <input type="text" id="date-from" class="form-control" placeholder="Select a start date..."> <input type="text" id="date-to" style="margin-left:90px" class="form-control" placeholder="Select an end date..."> <script> datepicker('#date-from', { id: 1, alwaysShow: true, dateSelected: new Date(), noWeekends: true }); datepicker('#date-to', { id: 1, alwaysShow: true, noWeekends: true }); </script> </body> </html>
Страница содержит два обычных тега input с type="text", которые преобразуются в календари с помощью вызова функции datepicker. Страница также импортирует необходимые файлы таблицы стилей и скрипта.
Сейчас мы собираемся превратить виджет в стандартный веб-компонент, чтобы HTML для достижения того же результата стал таким:
<!DOCTYPE html> <html> <head> <title>Transformed js-datepicker example</title> </head> <body> <script src="wc/date-picker.js"></script> <date-picker id="date-from" data-options="{ id: 1, alwaysShow: true, dateSelected: new Date(), noWeekends: true }"></date-picker> <date-picker id="date-to" style="margin-left:90px" data-options="{ id: 1, alwaysShow: true, noWeekends: true }"></date-picker> </body> </html>
Он не сильно отличается от предыдущего, но содержит ряд существенных улучшений:
- HTML-элемент был преобразован в Пользовательский элемент под названием выбор даты. Поэтому он сразу узнаваем как веб-компонент, его имя указывает на тип элемента, и он легко ассоциируется с одноименным сценарием.
- Достаточно импортировать один скрипт, который, как мы увидим, позаботится обо всех зависимостях от других скриптов и таблиц стилей.
- Мы можем передавать параметры конфигурации через HTML атрибуты. Поэтому нет необходимости писать явный код JavaScript для инициализации и настройки.
Все это можно сделать без потери каких-либо функций оригинального виджета. Теперь посмотрим, как это делается.
Веб-компонент: средство выбора даты — базовая структура
Базовая структура нашего веб-компонента выглядит следующим образом:
class DatePicker extends HTMLElement { constructor() { super(); } connectedCallback() { // Creations and configuration of HTML elements } disconnectedCallback() { } attributeChangedCallback(name, oldValue, newValue) { } } customElements.define('date-picker', DatePicker);
Это стандартная структура веб-компонента, основная часть которой находится в методе connectedCallback, вызываемом браузером, как только элемент HTML становится частью модели DOM. В нашем примере нам не нужно писать код в методе disconnectedCallback или attributeChangedCallback.
Метод connectedCallback
В connectedCallback мы собираемся сделать четыре вещи:
- загрузка js-datepicker таблицы стилей и скрипта
- создание тега «input», необходимого для js-datepicker
- создание и настройка средства выбора даты
- распространение собственных свойств средства выбора даты на веб-компонент
1. Загрузка таблицы стилей и скрипта
Мы решили, что наш WC загружает все необходимые таблицы стилей и скрипты, чтобы сделать HTML-код более компактным и обеспечить изолированное внутреннее функционирование веб-компонента. Однако код, который нам нужен, должен распознавать, загружены ли уже одни и те же библиотеки, и избегать их повторной загрузки. В рассматриваемом случае для этой проверки мы проверяем, существует ли функция «datepicker» (она определена в скрипте js-datepicker). Вот соответствующий код:
// Load the js-datepicker once if (typeof datepicker === 'undefined') { // "datepicker" does not exists if (!window.jsDatepicker) { // not already running (no other instance of the same WC) const script = document.createElement('script'); script.src = "https://unpkg.com/js-datepicker"; document.body.appendChild(script); script.addEventListener('load', () => { window.jsDatepicker = 'avail'; // when script is fully loaded, mark it "available" }); const link = document.createElement('link'); link.rel = "stylesheet"; link.href = "https://unpkg.com/js-datepicker/dist/datepicker.min.css"; document.head.appendChild(link); // in main doc, to be able to change styles window.jsDatepicker = 'loading'; } } else if (typeof datepicker !== 'function') { // "datepicker" exists but it is not a function alert("Name conflict: js-datepicker not loadable, the name 'datepicker' is already in use"); return; } else { // "datepicker" exists and it is a function: therefore js-datepicker is available window.jsDatepicker = 'avail'; }
Мы используем глобальную переменную window.jsDatepicker, чтобы гарантировать выполнение процесса загрузки только один раз, даже если один и тот же веб-компонент появляется более одного раза на одной и той же веб-странице. Обратите внимание, что для window.jsDatepicker установлено значение «доступно» в прослушивателе load скрипта, т. е. в конце асинхронной загрузки.
2. Создание тега «input», необходимого для js-datepicker
Создание тега «вход» является простым и не зависит от процесса загрузки, показанного ранее:
const fldId = "input_" + this.id; this.innerHTML = `<input id="${fldId}" type="text">`;
Обратите внимание, что мы не используем Shadow DOM, а загружаем и таблицу стилей, и скрипт непосредственно в основную модель DOM, потому что таким образом мы гарантируем такое же поведение, как исходный виджет (который не был изолированы от контекста страницы).
3. Создание и настройка средства выбора даты
Как указано в документации js-datepicker, параметры конфигурации представляют собой объект JavaScript, передаваемый в качестве второго аргумента функции datepicker. В нашем компоненте data-picker мы решили поместить его в атрибут data-option, откуда мы можем получить его:
var options = {}; var wc = this; if (typeof wc.dataset.options === 'string') { options = wc.getObjectFromString(wc.dataset.options); }
Функция getObjectFromString — это небольшая служебная функция, которая преобразует литерал JS в объект. Вы можете найти его в исходном коде на GitHub (см. ссылки в конце).
Прежде чем мы сможем инициализировать фактическое средство выбора даты, нам нужно убедиться, что сценарий, содержащий функцию даты выбора, завершил загрузку:
var interval = setInterval(() => { if (window.jsDatepicker === 'avail' && typeof datepicker === 'function') { clearInterval(interval); delete window.jsDatepicker; wc.datepicker = datepicker(`#${fldId}`, options); } }, 20);
4. Распространение собственных свойств средства выбора даты
Последним шагом является распространение свойств базового js-datepicker на наш веб-компонент после успешной инициализации. Это позволит нам применять к нашему объекту те же функции, что и базовый виджет.
Мы можем сделать это двумя способами, самый безопасный — создать собственное свойство WC, содержащее весь внутренний объект, как показано в последнем присваивании предыдущего сегмента:
wc.datepicker = datepicker(`#${fldId}`, options);
В противном случае мы могли бы применить все свойства datepicker к WC:
var wc = this; var dp = datepicker(`#${fldId}`, options); Object.getOwnPropertyNames(dp).forEach(function (prop) { if (!wc.hasOwnProperty(prop) && !wc.getAttribute(prop)) { wc[prop] = dp[prop]; } });
Это менее безопасно из-за возможных побочных эффектов такого необработанного «копирования» свойств. С обоими решениями мы сможем, например, вызвать метод getRange, который возвращает объект, содержащий две выбранные даты:
var dp = document.geteElementById('date-from'); // first case, when a datepicker property is created in the Custom Element var range = dp.datepicker.getRange(); // second method, when all properties of datepicker are cloned to the Custom Element var range = dp.getRange();
Полный код выбора даты
Вот полный код нашего веб-компонента data-picker:
В этом примере я намеренно свел изменения исходной функциональности виджета к нулю. Но мы также можем воспользоваться возможностью обогатить оригинал или ограничить его для более удобного использования, как я покажу во втором примере.
Слайдер галереи
Во втором эксперименте я исследую виджет с более структурированной композицией — карусель изображений, полученную с помощью библиотеки Glide.js. Анимация выше является продуктом следующей HTML-страницы:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Example with Glide.js</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@glidejs/[email protected]/dist/css/glide.core.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@glidejs/[email protected]/dist/css/glide.theme.min.css"> <style> .glide { max-width: 1000px; } .glide__slide { height: 600px; display: flex; justify-content: center; align-items: center; background-color: #f0f0f0; } .glide__arrow { font-size: 24px; } </style> </head> <body> <div class="glide"> <div class="glide__track" data-glide-el="track"> <ul class="glide__slides"> <li class="glide__slide"><img src="https://picsum.photos/1000/600"></li> <li class="glide__slide"><img src="https://picsum.photos/1001/600"></li> <li class="glide__slide"><img src="https://picsum.photos/1002/600"></li> <li class="glide__slide"><img src="https://picsum.photos/1003/600"></li> </ul> </div> <div class="glide__arrows" data-glide-el="controls"> <button class="glide__arrow glide__arrow--left" data-glide-dir="<"><</button> <button class="glide__arrow glide__arrow--right" data-glide-dir=">">></button> </div> <div class="glide__bullets" data-glide-el="controls[nav]"> <button class="glide__bullet glide__bullet--active" data-glide-dir="=0"></button> <button class="glide__bullet" data-glide-dir="=1"></button> <button class="glide__bullet" data-glide-dir="=2"></button> <button class="glide__bullet" data-glide-dir="=3"></button> </div> </div> <script src="https://cdn.jsdelivr.net/npm/@glidejs/[email protected]/dist/glide.min.js"></script> <script> document.addEventListener('DOMContentLoaded', function () { const glide = new Glide('.glide', { type: 'carousel', perView: 1, focusAt: 'center' }); glide.mount(); }); </script> </body> </html>
Как видите, это неожиданно богатый код, учитывая простоту желаемого эффекта: есть три таблицы стилей, структура, содержащая различные ‹div›'ы, неупорядоченный список (‹ul›), содержащий изображения, и фрагмент кода JavaScript для инициализации. Также необходимо использовать определенные классы CSS для различных элементов карусели.
Вместо этого с подходящим определением веб-компонента мы хотим получить более чистое и простое решение, например следующее:
<!DOCTYPE html> <html lang="it"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Example WC wrapper for Glide.js</title> <style> image-carousel>div { max-width:1000px; } image-carousel div li { height: 600px; display: flex; justify-content: center; align-items: center; background-color: #f0f0f0; } </style> </head> <body> <script src="wc/image-carousel.js"></script> <image-carousel> <img src="https://picsum.photos/1000/600"> <img src="https://picsum.photos/1001/600"> <img src="https://picsum.photos/1002/600"> <img src="https://picsum.photos/1003/600"> </image-carousel> </body> </html>
В этой форме все сводится к пользовательскому элементу «image-carousel», содержащему изображения карусели, единственный скрипт для импорта и небольшую оставшуюся таблицу стилей для идентичного размера всего исходного кода. . Обратите внимание, насколько лаконичен тег image-carousel по сравнению с тегом div, показанным выше.
Вот код веб-компонента:
Базовая структура та же, что и для средства выбора даты, с двумя интересными вариациями:
- Внутренний HTML-код тега представляет собой сложный элемент ‹div›, структура которого аналогична исходному HTML-коду. Наш пользовательский элемент позаботится обо всех деталях и создаст список изображений и кнопок, используя имена тегов и классов, предоставленные Glide.js (строки с 43 по 68).
- Конфигурация виджета предустановлена с возможностью переопределения через атрибуты data-*** компонента (строки с 70 по 77). Например, чтобы установить autoplay: 2000 (что означает автоматическую смену изображения каждые 2 секунды), нам просто нужно добавить атрибут:
<image-carousel data-autoplay="2000">
В данном случае я стремился не столько сохранить все возможности Glide.js, сколько максимально упростить его использование, предварительно выбрав набор стандартных параметров конфигурации и максимально уменьшив структурную сложность видимый HTML-код.
Однако, если бы мы хотели оставить свойства объекта Glide видимыми для разработчика, достаточно было бы скопировать их в свойство пользовательского элемента, как это уже было видно в первом примере.
Заключительные соображения
Таким образом, мы увидели, как использовать веб-компонент для упаковки виджета JavaScript в стандартный повторно используемый компонент. Такое преобразование может сделать HTML-код короче и выразительнее за счет создания новых тегов (пользовательские элементы), представляющих отличительные элементы пользовательского интерфейса.
Таким образом, преобразование в веб-компоненты повторно используемых UX-компонентов, являющихся частью проекта (внутреннего или стороннего), может быть хорошей практикой разработки программного обеспечения.
Дополнительные материалы на PlainEnglish.io.
Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .
Заинтересованы в масштабировании запуска вашего программного обеспечения? Ознакомьтесь с разделом Схема.