Упражнение по разработке программного обеспечения, демонстрирующее преимущества веб-компонентов

Веб-компоненты — это набор стандартизированных технологий, представленных Консорциумом 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>

Он не сильно отличается от предыдущего, но содержит ряд существенных улучшений:

  1. HTML-элемент был преобразован в Пользовательский элемент под названием выбор даты. Поэтому он сразу узнаваем как веб-компонент, его имя указывает на тип элемента, и он легко ассоциируется с одноименным сценарием.
  2. Достаточно импортировать один скрипт, который, как мы увидим, позаботится обо всех зависимостях от других скриптов и таблиц стилей.
  3. Мы можем передавать параметры конфигурации через 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 мы собираемся сделать четыре вещи:

  1. загрузка js-datepicker таблицы стилей и скрипта
  2. создание тега «input», необходимого для js-datepicker
  3. создание и настройка средства выбора даты
  4. распространение собственных свойств средства выбора даты на веб-компонент

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="<">&lt;</button>
      <button class="glide__arrow glide__arrow--right" data-glide-dir=">">&gt;</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, показанным выше.

Вот код веб-компонента:

Базовая структура та же, что и для средства выбора даты, с двумя интересными вариациями:

  1. Внутренний HTML-код тега представляет собой сложный элемент ‹div›, структура которого аналогична исходному HTML-коду. Наш пользовательский элемент позаботится обо всех деталях и создаст список изображений и кнопок, используя имена тегов и классов, предоставленные Glide.js (строки с 43 по 68).
  2. Конфигурация виджета предустановлена ​​с возможностью переопределения через атрибуты data-*** компонента (строки с 70 по 77). Например, чтобы установить autoplay: 2000 (что означает автоматическую смену изображения каждые 2 секунды), нам просто нужно добавить атрибут:
<image-carousel data-autoplay="2000">

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

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

Заключительные соображения

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

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

Дополнительные материалы на PlainEnglish.io.

Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .

Заинтересованы в масштабировании запуска вашего программного обеспечения? Ознакомьтесь с разделом Схема.