Русский
Русский
English
Статистика
Реклама

Компоненты

Что делать, если брать фронтенд-фреймворк это излишество

07.07.2020 08:21:43 | Автор: admin

Пример @@include


Современные фронтенд-фреймворки дают удивительные возможности. React, Vue, Angular и другие созданы делать то, что раньше было невозможно, веб-приложения. В 2020 скачивать и устанавливать приложения уже необязательно. Зачем, если всё можно сделать на сайте?


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


В этом вопросе я поддерживаю "консерваторов". Не нужно писать лендинги и многостранички на Create-React-App, для этого пойдет и обычная статика.


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


Что делать? Писать простыню HTML-разметки в одном файле? Хранить данные во view? Это не сделать шаг назад, это сорваться и упасть в пропасть. Это не просто неудобно, это идет вразрез с современной парадигмой фронтенд-разработки.


Во-первых, data-driven. Данные во главе угла, а внешний вид лишь их отображение. Пользователь делает действие, данные меняются, вслед меняется отображение. Так работают все современные фреймворки.


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


До этого мы уже обсуждали, как реализовать data-driven минимальными усилиями. Мой выбор Alpine.js. Что же делать с компонентностью? Для простых статических сайтов я предлагаю самый простой вариант gulp-file-include.


Столько говорил о современности и парадигмах, а закончилось всё библиотекой, которой уже лет 100? Ну, на самом деле, она не такая уж и старая. 4 года с версии 1.0.0, столько же, сколько первой стабильной версии React (15). Да и зачем заново изобретать велосипед, если уже всего готово.


У библиотеки всего шестьсот звезд на Github и 6,5 тысяч скачиваний в неделю на npm, но она дает нам всё, что нужно, чтобы быстро разделить простыню HTML на небольшие удобные компоненты. И при этом не добавив ни байта дополнительного кода в конечный результат.


Единственная преграда знакомство с Gulp. Ну, на самом деле, писать даже простой лендинг без подобного инструмента сегодня не очень правильно. Поэтому, если вы не знаете Gulp, узнайте. Вот для этого статья на Хабре, вот ссылка на перевод документации. Мы на этом останавливаться не будем.


Если вы хотите не прерываться, а хотите продолжить со мной, вот ссылка на мой пресет на Github, с которым мы будем работать, там же вы можете найти мой gulpfile.


Итак, что будем делать? Вот это


Что мы будем делать


Выглядит просто. Посмотрим, как это выглядит в HTML.


Как это выглядит в HTML
<section class="text-gray-700 body-font">  <div class="container px-5 py-24 mx-auto">    <h1 class="mb-20 text-2xl font-medium text-center text-gray-900 sm:text-3xl title-font">      Проблемы, которые я решаю    </h1>    <div class="flex flex-wrap -mx-4 -mt-4 -mb-10 sm:-m-4 md:mb-10">      <div        class="flex flex-col items-center p-4 mb-6 sm:flex-row lg:w-1/3 md:mb-0 sm:items-stretch"      >        <div          class="inline-flex items-center justify-center flex-shrink-0 w-12 h-12 mb-4 text-indigo-500 bg-indigo-100 rounded-full"        >          <svg            class="w-6 h-6"            fill="none"            stroke="currentColor"            stroke-linecap="round"            stroke-linejoin="round"            stroke-width="2"            viewBox="0 0 24 24"          >            <path d="M22 12h-4l-3 9L9 3l-3 9H2"></path>          </svg>        </div>        <div class="flex-grow pl-6">          <h2 class="mb-2 text-xl font-medium text-gray-900 title-font">Оптимизация скорости</h2>          <p class="text-lg leading-relaxed">            Увеличим быстродействие системы при загрузке, уменьшим нагрузку на процессор и оперативную память, исключим из автозагрузки требовательные к ресурсам устройства программы.          </p>        </div>      </div>      <div        class="flex flex-col items-center p-4 mb-6 lg:w-1/3 md:mb-0 sm:flex-row sm:items-stretch"      >        <div          class="inline-flex items-center justify-center flex-shrink-0 w-12 h-12 mb-4 text-indigo-500 bg-indigo-100 rounded-full"        >          <svg            class="w-6 h-6"            fill="none"            stroke="currentColor"            stroke-linecap="round"            stroke-linejoin="round"            stroke-width="2"            viewBox="0 0 24 24"          >            <circle cx="6" cy="6" r="3"></circle>            <circle cx="6" cy="18" r="3"></circle>            <path d="M20 4L8.12 15.88M14.47 14.48L20 20M8.12 8.12L12 12"></path>          </svg>        </div>        <div class="flex-grow pl-6">          <h2 class="mb-2 text-xl font-medium text-gray-900 title-font">            Восстановление системных файлов          </h2>          <p class="text-lg leading-relaxed">            В случае некорректной работы системы и устройств, проведём анализ системных файлов и восстановим их, если они повреждены.          </p>        </div>      </div>      <div        class="flex flex-col items-center p-4 mb-6 lg:w-1/3 md:mb-0 sm:flex-row sm:items-stretch"      >        <div          class="inline-flex items-center justify-center flex-shrink-0 w-12 h-12 mb-4 text-indigo-500 bg-indigo-100 rounded-full"        >          <svg            class="w-6 h-6"            fill="none"            stroke="currentColor"            stroke-linecap="round"            stroke-linejoin="round"            stroke-width="2"            viewBox="0 0 24 24"          >            <path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"></path>            <circle cx="12" cy="7" r="4"></circle>          </svg>        </div>        <div class="flex-grow pl-6">          <h2 class="mb-2 text-xl font-medium text-gray-900 title-font">            Установка и обновление драйверов устройств          </h2>          <p class="text-lg leading-relaxed">            При неработоспособности какого-либо из устройств или проблемах, связанных с их некорректной работой, произведём установку, обновление или откат драйверов.          </p>        </div>      </div>    </div>  </div></section>

Воу! Не так уж и просто, как хотелось бы. Безусловно, TailwindCSS, который здесь используется, тоже даёт о себе знать. Если бы мне пришлось разбираться в этом коде, я бы первым дизлайкнул статью, заявляющую, что TailwindCSS это новый шаг эволюции. Но нет, я её написал. А всё потому, что этот код можно красиво поделить на компоненты, где эта гора классов превратится в осмысленную картину, которая не только ухудшит, а даже улучшить наш developer-experience.


Но вернемся к плагину. Начнем с небольшой теории. Чтобы с помощью gulp-file-include вставить один HTML в другой, нам нужно написать команду @@include(<путь до файла>, <объект с переменными>).


При этом в gulpfile можно многое настроить под себя. У меня настроено примерно так:


function html() {  return src('src/*.html')    .pipe(fileinclude({ basepath: './src/partials' }))    .pipe(dest('dist'));}

Берем все HTML-файлы из src, прогоняем через наш плагин и кладем в папку dist. При этом в настройки плагина можно передать ряд опций. Быстро пройдемся по ним.


  • prefix можно изменить префикс с @@ на любой другой.
  • suffix можно добавить суффикс.
  • basepath можно настроить, как просчитываются пути в директивах. По умолчанию '@file' от HTML-файла. Есть еще '@root' от корня, или любой другой путь. В нашем случае, я создал специальную папку в src partials, где и будут лежать все наши компоненты. Важно правильно настроить Gulp, чтобы эти компоненты не попали в окончальную сборку. В примере выше, Gulp берет только файлы из корня src, не заглядывая в папки. Это то, что нам нужно.
  • filters позволяет задавать функции, которые потом можно будет запускать из разметки. Смотрите примеры в документации.
  • context "глобальные" переменные для условий @@if.

Также есть ряд директив:


  • @@include вставка HTML-файла в другой HTML.
  • @@if условия; переменные берутся из "глобального" context и/или из объекта переменных использованного @@include.
  • @@for обычный цикл по массиву из context/переменных @@include.
  • @@loop проходимся по массиву объектов, используя данные из них как переменные, и вставляем для каждого компонент. Может использовать JSON.

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


В нашем случае нас больше всего интересует @@loop. Мы перенесем все данные из карточек в JSON-файл, создадим компонент и передадим ему его данные, как пропсы.


Что у нас входит в данные? Правильный ответ: заголовок карточки, текст и SVG. В переменные мы можем передавать как просто текст, так и HTML как строку. Он просто подставит её, куда мы скажем.


Вот как выглядит JSON (data.json).


[  {    "title": "Оптимизация скорости",    "text": "Увеличим быстродействие системы при загрузке, уменьшим нагрузку на процессор и оперативную память, исключим из автозагрузки требовательные к ресурсам устройства программы.",    "svg": "<path d=\"M22 12h-4l-3 9L9 3l-3 9H2\"></path>"  },  {    "title": "Восстановление системных файлов",    "text": "В случае некорректной работы системы и устройств, проведём анализ системных файлов и восстановим их, если они повреждены.",    "svg": "<circle cx=\"6\" cy=\"6\" r=\"3\"></circle><circle cx=\"6\" cy=\"18\" r=\"3\"></circle><path d=\"M20 4L8.12 15.88M14.47 14.48L20 20M8.12 8.12L12 12\"></path>"  },  {    "title": "Установка и обновление драйверов устройств",    "text": "При неработоспособности какого-либо из устройств или проблемах, связанных с их некорректной работой, произведём установку, обновление или откат драйверов.",    "svg": "<path d=\"M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2\"></path><circle cx=\"12\" cy=\"7\" r=\"4\"></circle>"  }]

Теперь создадим компонент одной карточки (card.html). Переменные вставляем как @@<имя переменной>.


<div  class="flex flex-col items-center p-4 mb-6 sm:flex-row lg:w-1/3 md:mb-10 sm:items-stretch">  <div    class="inline-flex items-center justify-center flex-shrink-0 w-12 h-12 mb-4 text-indigo-500 bg-indigo-100 rounded-full"  >    <svg      class="w-6 h-6"      fill="none"      stroke="currentColor"      stroke-linecap="round"      stroke-linejoin="round"      stroke-width="2"      viewBox="0 0 24 24"    >      @@svg    </svg>  </div>  <div class="flex-grow pl-6">    <h2 class="mb-2 text-xl font-medium text-gray-900 title-font">@@title</h2>    <p class="text-lg leading-relaxed">@@text</p>  </div></div>

Осталось только создать файл секции (index.html).


<section class="text-gray-700 body-font">  <div class="container px-5 py-24 mx-auto">    <h1 class="mb-20 text-2xl font-medium text-center text-gray-900 sm:text-3xl title-font">      Проблемы, которые я решаю    </h1>    <div class="flex flex-wrap -mx-4 -mt-4 -mb-10 sm:-m-4">      @@loop('problems/card.html', 'partials/problems/data.json')    </div>  </div></section>

Первым параметром в @@loop передаем путь до компонента (от настроенного ранее basepath), вторым путь до JSON-файла (от src).


Структура файлов выглядит вот так:


src   index.html   main.csspartials      problems          index.html          card.html          data.json...

Теперь сам index.html я могу вставить с помощью @@include в файл основной страницы.


<!DOCTYPE html><html lang="ru">  <head>    ...  </head>  <body>    ...    @@include('problems/index.html')    ...  </body></html>

Минимальными усилиями мы получили полноценное компонентное деление. При этом, это никак не отразится на результате, будет такой же HTML, как и раньше. Конечно, не могу не заметить, что это подкрепляет также тезис про TailwindCSS, он уже не кажется таким неудобным, как прежде, не так ли? Дальше дело за малым повторить всё выше перечисленное для всех секций сайта.


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


Напоследок прошу ответить на вопрос:

Подробнее..

Перевод Веб-компоненты руководство для начинающих

21.10.2020 14:12:03 | Автор: admin

Узнайте о преимуществах использования веб-компонентов, о том, как они работают, а также о том, как начать их использовать

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

После этого мы приступим к созданию компонентов, сначала с помощью шаблонов HTML (HTML templates) и интерфейса теневого DOM (shadow DOM), затем немного углубимся в тему и посмотрим как создать кастомизированный встроенный элемент (customized build-in element).

Что такое компоненты?


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

XML Binding Language от Mozilla и спецификации HTML Component от Microsoft для Internet Explorer 5 появились около 20 лет назад. К сожалению, обе реализации были очень сложными и не смогли заинтересовать производителей других браузеров, а потому вскоре были забыты. Несмотря на это, именно они заложили основы того, что мы имеем в этой сфере сегодня.

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

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

4 столпа компонентов


Компоненты состоит из трех интерфейсов (API) кастомные элементы (custom elements), шаблоны HTML (HTML templates) и теневой DOM (shadow DOM), а также из лежащих в их основе модулей JavaScript (ES6 modules). С помощью инструментов, предоставляемых этими интерфейсами, можно создавать кастомные HTML-элементы, которые ведут себя подобно нативным аналогам.

Компоненты используются также, как и обычные элементы HTML. Их можно настраивать с помощью атрибутов, получать с помощью JavaScript, стилизовать с помощью CSS. Главное уведомить браузер о том, что они существуют.

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

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

Рассмотрим каждую спецификацию по-отдельности.



1. Кастомные элементы


Ключевые особенности:

  • Определение поведения элемента
  • Реагирование на изменения атрибутов
  • Расширение существующих элементов

Часто при разговоре о компонентах, люди имеют ввиду интерфейс кастомных элементов.

Этот API позволяет расширять элементы, определяя их поведение при добавлении, обновлении и удалении.

class ExampleElement extends HTMLElement {  static get observedAttributes() {      return [...]  }  attributeChangedCallback(name, oldValue, newValue) {}  connectedCallback() {}}customElements.define('example-element', ExampleElement)

Каждый кастомный элемент имеет похожую структуру. Он расширяет функционал существующего класса HTMLElements.

Внутри кастомного элемента содержится несколько методов, которые называются реакциями (reactions), отвечающих за обработку того или иного изменения элемента. Например, connectedCallback вызывается при добавлении элемента на страницу. Это похоже на стадии жизненного цикла, используемые в фреймворках (componentDidMount в React, mounted в Vue).

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

Элемент должен быть определен перед тем, как браузер сможет его использовать. Метод define принимает два аргумента название тега и его класс. Все теги должны содержать символ "-" во избежание конфликтов с существующими и будущими нативными элементами.

<example-element>Content</example-element>

Элемент может использовать как обычный тег HTML. При обнаружении такого элемента, браузер связывает его поведение с указанным классом. Данный процесс называется обновлением (upgrading).

Существует два типа элементов автономные (autonomous) и кастомизированные встроенные (customized build-in). До сих пор мы рассматривали автономные элементы. Это такие элементы, которые не связаны с существующими HTML-элементами. Подобно тегам div и span, которые не имеют определенного семантического значения.

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

class CustomInput extends HTMLInputElement {}customElements.define('custom-input', CustomInput, { extends: 'input' })

Класс кастомизированного встроенного элемента расширяет класс кастомизируемого элемента. При определении (define) встроенного элемента в качестве третьего аргумента передается расширяемый элемент.

<input is="custom-input" />

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

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

2. Шаблоны HTML


  • Создание готовых структур
  • Не отображаются на странице до вызова
  • Содержат HTML, CSS и JS

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

<template id="tweet">  <div class="tweet">    <span class="message"></span>      Written by @    <span class="username"></span>  </div></template>

Сам по себе он никак не влияет на страницу, т.е. он не парсится движком, запросы на получение ресурсов (аудио, видео) не отправляются. JavaScript не может получить к нему доступ, а для браузеров это пустой элемент.

const template = document.getElementById('tweet')const node = document.importNode(template.content, true)document.body.append(node)

Сначала мы получаем элемент template. Метод importNode создает копию его содержимого, второй аргумент (true) означает глубокое копирование. Наконец, мы добавляем его на страницу, как любой другой элемент.

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

Этим шаблоны не ограничены. Они предстают во всей красе при использовании с другими частями компонентов, в частности, с теневым DOM.

3. Теневой DOM


  • Позволяет избежать конфликта стилей
  • Придумывать названия (классов, например) становится проще
  • Инкапсуляция логики реализации

Объектная модель документа (Document Object Model, DOM) это то, как браузер интерпретирует структуру страницы. Читая разметку, браузер определяет какие элементы какой контент содержат и на основе этого принимает решение о том, что следует отображать на странице. При использовании document.getElemetById(), например, браузер обращается к DOM в поисках нужного элемента.

Для макета (layout) страницы такой подход является приемлемым, но что насчет деталей, скрытых внутри элемента? Например, страницу не должно беспокоить то, какой интерфейс содержится внутри элемента video. Вот где теневой DOM приходит на помощь.

<div id="shadow-root"></div><script>  const host = document.getElementById('shadow-root')  const shadow = host.attachShadow({ mode: 'open' })</script>

Теневой DOM создается в момент применения к элементу. В теневой DOM можно добавлять любой контент, как и в обычный (светлый, light) DOM. На теневой DOM не влияет то, что происходит снаружи, т.е. за его пределами. Обычный DOM также не может получить доступ к теневому напрямую. Это означает, что в теневом DOM мы можем использовать любые названия классов, стили и скрипты и не переживать о возможных конфликтах.

Наилучший результат дает использование теневого DOM вкупе с кастомными элементами. Благодаря теневому DOM при повторном использовании компонента его стили и структура никак не влияют на другие элементы на странице.

ES и HTML модули

  • Добавление при необходимости
  • Предварительная генерация не требуется
  • Все хранится в одном месте

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

Спецификация импортов HTML (HTML Imports) определяет способ экспорта и импорта HTML документов, а также CSS и JavaScript. Это позволило бы кастомным элементам вместе с шаблонами и теневым DOM находится в другом месте и использоваться по необходимости.

Однако, Firefox отказался от реализации данной спецификации в своем браузере и предложил иной способ на основе JavaScript-модулей.

export class ExampleElement external HTMLElement {}import { ExampleElement } from 'ExampleElement.js'

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

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

import { ExampleElement } from 'ExampleElement.html'

Microsoft выдвинула предложение о расширении спецификации JavaScript-модулей экспортом/импортом HTML. Это позволит создавать компоненты с помощью декларативного и семантического HTML. Данная возможность скоро появится в Chrome и Edge.

Создание собственного компонента


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


Компоненты позволяют отображать комментарии пользователей с помощью интерфейсов шаблонов HTML и теневого DOM.

Создадим компонент для отображения комментариев пользователей с помощью шаблонов HTML и теневой DOM.

1. Создание шаблона

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

Добавляем элемент template на страницу. Любые стили, определенные в этом элементе, будут влиять только на него.

<template id="user-comment-template">  <style>      ...  </style></template>

2. Добавление разметки

Кроме стилей, компонент может содержать макет (структуру). Для этих целей используется элемент div.

Динамический контент передается через слоты (slots). Добавим слоты для аватара, имени и сообщения пользователя с соответствующими атрибутами name:

<div class="container">  <div class="avatar-container">    <slot name="avatar"></slot>  </div>  <div class="comment">    <slot name="username"></slot>    <slot name="comment"></slot>  </div></div>

Содержимое слота по умолчанию


Контент по умолчанию будет отображаться при отсутствии переданной слоту информации

Данные, переданные слоту, перезаписывают данные в шаблоне. Если слоту не передается информации, отображается контент по умолчанию.

В данном случае, если имя пользователя не было передано, на его месте отображается сообщение No name:

<slot name="username">  <span class="unknown">No name</span></slot>

3. Создание класса

Создание кастомного элемента начинается с расширения класса HTMLElement. Частью процесса настройки является создание теневого корневого узла (shadow root) для рендеринга контента элемента. Открываем его для получения доступа на следующем этапе.

Наконец, сообщаем браузеру о новом классе UserComment.

class UserComment extends HTMLElement {  constructor() {      super()      this.attachShadow({ mode: 'open' })  }}customElements.define('user-comment', UserComment)

4. Применение теневого контента

Когда браузер встречает элемент user-comment, он обращается к теневому корневому узлу для получения его содержимого. Второй аргумент указывает браузеру копировать весь контент, а не только первый слой (элементы верхнего уровня).

Добавляем разметку в теневой корневой узел, что приводит к немедленному обновлению внешнего вида компонента.

connectedCallback() {  const template = document.getElementById('user-comment-template')  const node = document.importNode(template.content, true)  this.shadowRoot.append(node)}

5. Использование компонента

Теперь компонент готов к использованию. Добавляем тег user-comment и передаем ему необходимую информацию.

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

<user-comment>  <img alt="" slot="avatar" src="avatar.png" />  <span slot="username">Matt Crouch</span>  <div slot="comment">This is an example of a comment</div></user-comment>

Рсширенный код примера:
<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <title>Web Components Example</title>    <style>      body {        display: grid;        place-items: center;      }      img {        width: 80px;        border-radius: 4px;      }    </style>  </head>  <body>    <template id="user-comment-template">      <div class="container">        <div class="avatar-container">          <slot name="avatar">            <slot class="unknown"></slot>          </slot>        </div>        <div class="comment">          <slot name="username">No name</slot>          <slot name="comment"></slot>        </div>      </div>      <style>        .container {          width: 320px;          clear: both;          margin-bottom: 1rem;        }        .avatar-container {          float: left;          margin-right: 1rem;        }        .comment {          height: 80px;          display: flex;          flex-direction: column;          justify-content: center;        }        .unknown {          display: block;          width: 80px;          height: 80px;          border-radius: 4px;          background: #ccc;        }      </style>    </template>    <user-comment>      <img alt="" slot="avatar" src="avatar1.jpg" />      <span slot="username">Matt Crouch</span>      <div slot="comment">Fisrt comment</div>    </user-comment>    <user-comment>      <img alt="" slot="avatar" src="avatar2.jpg" />      <!-- no username -->      <div slot="comment">Second comment</div>    </user-comment>    <user-comment>      <!-- no avatar -->      <span slot="username">John Smith</span>      <div slot="comment">Second comment</div>    </user-comment>    <script>      class UserComment extends HTMLElement {        constructor() {          super();          this.attachShadow({ mode: "open" });        }        connectedCallback() {          const template = document.getElementById("user-comment-template");          const node = document.importNode(template.content, true);          this.shadowRoot.append(node);        }      }      customElements.define("user-comment", UserComment);    </script>  </body></html>





Создание кастомизированного встроенного элемента


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

1. Создание класса

Встроенные элементы, как и автономные, появляются в момент расширения класса, но вместо общего класса HTMLElement, они расширяют конкретный класс.

В нашем случае таким классом является HTMLTimeElement класс, используемый элементами time. Он включает поведение, связанное с атрибутом datetime, включая формат данных.

class RelativeTime extends HTMLTimeElement {}

2. Определение элемента

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

Наш объект будет содержать один ключ со значением кастомизируемого элемента. Он принимает название тега. В случае отсутствия такого ключа будет выброшено исключение.

customElements.define('relative-time', RelativeTime, { extends: 'time' })

3. Установка времени

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

Наконец, мы устанавливаем атрибут title, позволяющий пользователю при наведении курсора увидеть установленное значение.

setTime() {  this.innerHTML = timeago().format(this.getAttribute('datetime'))  this.setAttribute('title', this.getAttribute('datetime'))}

4. Обновление соединения

Компонент может использовать метод сразу после отображения на странице. Поскольку встроенные компоненты не имеют теневого DOM, они не нуждаются в конструкторе.

connectedCAllback() {  this.setTime()}

5. Слежение за изменением атрибутов

Если программно обновить время, компонент не отреагирует. Он не знает о том, что должен следить за изменениями атрибута datetime.

После определения наблюдаемых атрибутов (observed attributes), attributeChangedCallback будет вызываться при каждом их изменении.

static get observedAttributes() {  return ['datetime']}attributeChangedCallback() {  this.setTime()}

6. Добавление на страницу

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

<time is="relative-time" datetime="2020-09-20T12:00:00+0000">  20 сентября 2020 г. 12:00</time>

Расширенный код примера:
<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <title>Web Components Another Example</title>    <!-- timeago.js -->    <script      src="http://personeltest.ru/aways/cdnjs.cloudflare.com/ajax/libs/timeago.js/4.0.2/timeago.min.js"      integrity="sha512-SVDh1zH5N9ChofSlNAK43lcNS7lWze6DTVx1JCXH1Tmno+0/1jMpdbR8YDgDUfcUrPp1xyE53G42GFrcM0CMVg=="      crossorigin="anonymous"    ></script>    <style>      body {        display: flex;        flex-direction: column;        align-items: center;      }      input,      button {        margin-bottom: 0.5rem;      }      time {        font-size: 2rem;      }    </style>  </head>  <body>    <input type="text" placeholder="2020-10-20" value="2020-08-19" />    <button>Set Time</button>    <time is="relative-time" datetime="2020-09-19">      19 сентября 2020 г.    </time>    <script>      class RelativeTime extends HTMLTimeElement {        setTime() {          this.innerHTML = timeago.format(this.getAttribute("datetime"));          this.setAttribute("title", this.getAttribute("datetime"));        }        connectedCallback() {          this.setTime();        }        static get observedAttributes() {          return ["datetime"];        }        attributeChangedCallback() {          this.setTime();        }      }      customElements.define("relative-time", RelativeTime, { extends: "time" });      const button = document.querySelector("button");      const input = document.querySelector("input");      const time = document.querySelector("time");      button.onclick = () => {        const { value } = input;        time.setAttribute("datetime", value);      };    </script>  </body></html>





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

Переиспользуемый компонент Svelte чтобы никому не было больно

10.02.2021 16:11:31 | Автор: admin

Переиспользуемый компонент Svelte: чтобы никому не было больно


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


За последние года полтора для фреймворка Svelte уже создано множество различных компонентов, которые можно найти на NPM, GitHub или официальном списке. К сожалению, не все из них правильно "приготовлены" и порой их использование раздует размер бандла приложения сильнее, чем должно быть. А бывает, что такие пакеты просто невозможно использовать, потому что его автор не силён в подготовке пакетов и упустил какие-то важные моменты.


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


Создаем компонент


Для примера решим, что мы хотим представить миру компонент, который выводит часы с анимацией смены цифр. Посмотрим на его прототип в работе тут. Обратим внимание, что весь наш компонент состоит из трех файлов. Также у него имеется внешняя зависимость в виде библиотеки 'dayjs'.


Animated clock


Для начала понадобится пустая папка. Открыв её в терминале, инициализируем создание нового npm-пакета:


  npm init

Будет предложен ряд вопросов, первый из которых захочет узнать название нашего будущего пакета. По негласному правилу пакеты, содержащие Svelte-компоненты, принято называть с префиксом svelte-, так что назовем наш пакет, например, svelte-clock-demo. Это правило совершенно необязательно, но сильно упростит поиск компонента другими людьми. Также этому поспособствует и хороший набор ключевых слов, которые нужно перечислить через запятую в одном из следующих вопросов. Оставшиеся вопросы заполняйте на свое усмотрение, можно оставить значения по умолчанию нажатием клавиши Enter.


Теперь наша папка уже не пуста, в ней появился файл package.json к которому мы вернемся чуть позже, а пока создадим новую папку components рядом с этим файлом, куда поместим все файлы нашего компонента 'App.svelte','Sign.svelte' и 'flip.js'.


Список файлов


Также не забудем и про внешнюю зависимость, добавим её в пакет командой:


  npm install dayjs

Необходимо указать сборщикам Svelte-проектов, где в папке находится файл компонента, чтобы они могли импортировать его. Для этого откроем файл package.json и заменим строку "main":"index.js", на:


 ... "svelte":"components/App.svelte", ...

В принципе, этого уже достаточно, можно опубликовать пакет в таком виде и большинство разработчиков смогут использовать его в своем проекте. Но большинство это ещё не все...


Нужны модули


На текущий момент нашим пакетом смогут пользоваться только разработчики, которые делают свой Svelte-проект c настроенным сборщиком, который умеет работать с полем "svelte" в package.json. Но кроме этого, было бы неплохо если бы наш компонент могли использовать разработчики, которые делают проект на другом фреймворке или вообще без каких-либо фреймворков. Для них мы должны упаковать компонент в модули с которыми они смогут работать не настраивая плагинов для компиляции Svelte файлов в своих проектах.


ES6 модуль обычно используется при работе со сборщиками вроде Webpack или Rollup. Он должен содержать скомпилированный в JavaScript код нашего Svelte-компонента, а так же все CSS-стили, которые относятся к нашему компоненту. Однако, в модуль не должны быть включены внешние зависимости и различные вспомогательные функции из пакета svelte, например, переходы из svelte/transition или хранилища из svelte/store, а так же функции из svelte/internal. В противном случае, если пользователю понадобится использовать несколько различных пакетов, то в каждом из них будет своя копия фреймворка Svelte, что скажется на размере итогового бандла самым негативным образом.


IIFE модуль нужен для непосредственного использования в браузере в теге <script src="...">. Файлы таких модулей обычно кладут рядом с html-файлом, либо подключают с какого-либо CDN, вроде jsdelivr.com или unpkg.vom. Поскольку для таких модулей не существует никаких общепринятых механизмов управления зависимостями, то они должны быть самодостаточные и включать в себя всё, что необходимо для работы, включая все импорты из пакета svelte, а так же внешние зависимости такие как, dayjs из нашего примера.


Для сборки компонента в модули нам понадобится бандлер. Их существует великое множество Webpack, Rollup, Parcel и другие. Но лично я, в последнее время использую в своих проектах сборщик esbuild, он написан на Go, что позволяет ему собирать проекты невероятно быстро, также он достаточно прост в настройке и умеет оптимизировать бандл tree-шейкингом и минификацией. К нему в компанию нам понадобятся плагин esbuild-svelte и сам svelte. Установим все эти пакеты в dev-зависимости:


 npm install --save-dev esbuild esbuild-svelte svelte

Пакет svelte мы установили в dev-зависимости, поскольку он нам нужен для компиляции кода компонента в Javascript-код. В проектах, которые захотят использовать наш пакет вероятнее всего уже будет установлен svelte и все нужные импорты будут браться из него. Однако, возможна ситуация, когда этот пакет не будет установлен, а разработчик, используя наш ES6 модуль, получит сообщение об отсутствии зависимости. Чтобы избежать этой неприятной ситуации, добавим в package.json секцию с peer-зависимостью.


...,"peerDependencies": {  "svelte": "*"},...

Звездочка * говорит, что мы не требуем какой-то определенной версии Svelte, но при необходимости можно указать нужную версию.


Теперь при установке нашего пакета, если у пользователя не найдётся svelte в node_modules, будет предложено его установить. А NPM начиная с 7й версии сделает это автоматически.


Укажем бандлерам и CDN-сервисам где искать нужные им модули внутри нашего пакета. Для этого сразу под под полем "svelte":..., в package.json добавим ещё пару полей:


    ...    "module":"dist/clock.mjs",    "browser":"dist/clock.min.js",    ...

Конфигурация esbuild


В папке проекта создадим файл esbuild.js с таким содержанием:


const {build} = require(`esbuild`);const sveltePlugin = require(`esbuild-svelte`);// Берем содержимое package.json в виде объекта pkgconst pkg = require(`./package.json`);// Настраиваем плагин компиляции Svelte файловconst svelte = sveltePlugin({  compileOptions:{     // Все стили будут упакованы вместе с компонентом    css: true  }});// Собираем IIFE-модульbuild({  // Откуда и куда собирать модули узнаем в package.json  entryPoints: [pkg.svelte],  outfile: pkg.browser,  format: 'iife',  bundle: true,  minify: true,  sourcemap: true,  plugins: [svelte],  // Задаём имя глобальной переменной для доступа к модулю  globalName: 'svelteClock',})// Собираем ES-модульbuild({  entryPoints: [pkg.svelte],  outfile: pkg.module,  format: 'esm',  bundle: true,  minify: true,  sourcemap: true,  plugins: [svelte],  // Просим не включать в модуль зависимости из разделов  // dependencies и peerDependencies в файле package.json  external: [    ...Object.keys(pkg.dependencies),    ...Object.keys(pkg.peerDependencies),  ]})

Информацию про параметры конфигурации esbuild можно узнать в документации.

Добавим в package.json в раздел "scripts" скрипт для запуска esbuild:


  ...  "scripts": {    ...    "build":"node esbuild",    ...  }

Теперь можно запустить сборку модулей:


  npm run build

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


Текущая структура файлов


Readme.md


Добавьте файл README.md в папку проекта, из него пользователи смогут узнать, как работать с вашим компонентом. Многие согласятся, что написать документацию, описывающую все нюансы работы с компонентом, задача, возможно, ещё более трудная, чем придумать сам этот компонент и правильно его собрать. Могу только порекомендовать указать назначение компонента, объяснить зачем он нужен людям. Показать примеры использования в других svelte-проектах, иных проектах и браузере. Если компонент конфигурируется при помощи экспортируемых свойств или отправляет какие-либо события, обязательно опишите как с этим работать. Писать документацию стоит на английском языке, поскольку вашим компонентом будут пользоваться люди со всего мира.


Публикация пакета


Вся необходимая подготовка проведена и мы готовы собрать и отправить наш пакет с компонентом на NPM. По умолчанию в пакет попадают все файлы и папки, которые содержатся в папке проекта, кроме директории node_modules и некоторых других специфичных файлов. Также не нужно включать в пакет файл esbuild.js, поскольку он нужен только на этапе сборки модулей. Для исключения определенных файлов папок из пакета воспользуемся файлом .npmignore, внутри которого на каждой строчке нужно указать какие файлы не должны попадать в пакет. В нашем случае это всего один файл esbuild.js. В более сложных проектах там стоит перечислить папки содержащие тесты и исходники, которые не нужны в собранном пакете.


Содержимое .npmignore


Каждый раз при публикации новой версии пакета мы должны не забыть выполнить скрипт npm run build, который поместит в папку dist актуальные версии модулей. Чтобы автоматизировать этот процесс добавьте в секцию "scripts" в package.json ещё один скрипт:


  ...  "scripts": {    ...    "prepublish":"npm run build",    ...  }

Если вы все сделали верно, то целиком файл pacakge.json должен выглядеть примерно так:


{  "name": "svelte-clock-demo",  "version": "1.0.0",  "description": "Animated clock component for Svelte",  "svelte": "components/App.svelte",  "module":"dist/clock.mjs",  "browser":"dist/clock.min.js",  "scripts": {    "build":"node esbuild",    "prepublish":"npm run build",    "test": "echo \"Error: no test specified\" && exit 1"  },  "author": "Vasya Pupkin",  "license": "ISC",  "dependencies": {    "dayjs": "^1.10.4"  },  "devDependencies": {    "esbuild": "^0.8.43",    "esbuild-svelte": "^0.4.1",    "svelte": "^3.32.2"  },  "peerDependencies": {    "svelte": "*"  }}

Наконец, всё готово и можно авторизоваться своей учетной записью в NPM и опубликовать пакет:


 npm login npm publish

Пакет будет доставлен в NPM и станет доступным для загрузки всеми желающими. А вам останется наблюдать за количеством установок вашего пакета и своевременно исправлять обнаруженные пользователями баги.


Используем переиспользуемый компонент


Итак компонент уже опубликован. Теперь можно его установить в папке какого-либо своего проекта:


  npm install --save-dev svelte-clock-demo

Если это проект Svelte-приложения, то импортируйте и используйте как обычный компонент:


<script>    import Clock from 'svelte-clock-demo';</script><Clock background="white" color="black" />

Если же это проект, в котором нет настроенного компилятора Svelte, то будет импортирован ES6 модуль, и компонент инициализируется следующим образом:


import Clock from 'svelte-clock-demo';new Clock({  // Указываем элемент DOM, куда будет отрисован компонент  target: document.getElementById('divForClock'),  // Передаём свойства компоненту  props:{    background: 'white',    color: 'black'  }})

Практически таким же образом можно использовать модуль прямо с CDN. Обычно после публикации пакета на NPM, через пару минут его версия появляется и в различных CDN сервисах, например jsdelivr.com.


<html>  <head>    <title>Страница с часами</title>    <!-- Подключаем компонент с CDN -->    <script src='https://cdn.jsdelivr.net/npm/svelte-clock-demo'></script>  </head>  <body>    <div id="divForClock"></div>  </body>  <script>    // Имя глобальной переменной было задано в конфигурации esbuild    new svelteClock.default({      // Указываем элемент DOM, куда будет отрисован компонент      target: document.getElementById('divForClock'),      // Передаём свойства компоненту      props:{        background: 'white',        color: 'black'      }    })  </script></html>

Библиотека компонентов


Иногда в один пакет нужно поместить больше, чем один-единственный компонент. Яркий пример, различные библиотеки UI элементов, состоящие из компонентов вроде Input,Button, Checkbox и т.п.


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


Для создания библиотеки компонентов, добавим в папку components файл index.js внутри которого сделаем ре-экспорт всех компонентов, которые мы хотим сделать доступными в нашем пакете.


export {default as Clock} from './App.svelte';export {default as Sign} from './Sign.svelte';

Затем нужно поменять значение поля "svelte" в package.json, указав там путь до файла index.js.


  ...  "svelte":"components/index.js",  ...

После публикации пакета, можно импортировать только нужные компоненты:


import {Sign} from 'svelte-clock-demo';

Заключение


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


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

Подробнее..

Тупые и умные компоненты

21.09.2020 00:19:49 | Автор: admin

Меня зовут Илона, я Senior Experience Designer в EPAM. Работа для меня удачно совпадает с хобби в EPAM я проектирую интерфейсы для зарубежных заказчиков, читаю лекции для сотрудников и студентов лабы, менторю дизайнеров. В свободное время преподаю проектирование интерфейсов в магистратуре Университета ИТМО и веду Телеграм-канал о UX-дизайне.
В работе и преподавании я часто сталкиваюсь с проблемой: сложно организовать компоненты интерфейса так, чтобы было всегда понятно, какой компонент использовать, чтобы похожие компоненты не плодились и не путали дизайнеров и разработчиков.

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

Что вообще такое компоненты

Графический интерфейс (GUI), как правило, состоит из кнопок, полей, чекбоксов, текстовых блоков и пр. Именно это мы называемкомпоненты эдакая интерактивная (или нет) обёртка контента:кнопка Оформить заказ; чекбокс Я принимаю условия соглашения и т.д.

Для UX-дизайнера интерфейс в первую очередь инструмент решения пользовательских задач. Задача всегда существует в контексте: регистрация в контексте сайта авиакомпании, покупка в контексте интернет-магазина одежды. Поэтому дизайнеру очень важно использовать реальные заголовки, названия кнопок, пункты списков. Именно так мы иллюстрируем решение определённой задачи в определённом контексте.

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

Посмотрим на простом примере

Дизайним карточку товара в интернет-магазине: картинка, информация, цена и кнопка Купить.

А ещё требуется карточка товара для корзины. Там нет кнопки Купить, зато есть кнопка Удалить и селектор количества товара. Звучит как новый компонент. Делаем!

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

И вот у нас уже 3 компонента Карточка.

Карточки во всем своем многообразииКарточки во всем своем многообразии

Теперь мы хотим показать информацию о заказе в личном кабинете. Неплохо смотрелась бы Карточка!
Какую же из трёх использовать? Ни одна толком не подходит. Проще уже сделать новую.


Проходит время, карточки разработаны и живут в разных местах системы.

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

Три группы правок в нескольких местах на макетах и в системеТри группы правок в нескольких местах на макетах и в системе

Изменений бы потребовалось в три раза меньше, если бы карточку переиспользовали.

Зачем и как переиспользовать компоненты

Переиспользование компонентов помогает:

  1. облегчить жизнь себе и разработчикам;

  2. пользователям предсказывать поведение интерфейса (увидел компонент понял как работает знаю чего ожидать)

Во имя переиспользования, по примеру разработчиков React.js, делим компоненты на два типа:тупыеиумные.

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

Умный компонент привязан к бизнес-логике.Он исполняет определённую функцию в конкретном месте. Умный компонент является реализацией тупого.
Можно сказать, что тупой компонент шаблон, а умный компонент пример его применения.

Шаблон карточки и примеры его примененияШаблон карточки и примеры его применения

Пример с карточками сделан в Figma. Тупая карточка Figma-component с применениемAuto Layout.Благодаря этому элементы карточки можно удалять и менять, а её размер подстроится под изменения. Умная карточка Figma-instance.

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

Скругление всех картинок одним движениемСкругление всех картинок одним движением

Тупой UI kit

Простая и понятная библиотека компонентов (UI kit), элементы которой легко переиспользовать и обновлять турбо-ускоритель дизайна и разработки. И состоит такая библиотека из тупых компонентов. Я называю её Тупой UI kit.
Если на вашем проекте уже есть UI kit, попробуйте сделать все компоненты в нём тупыми. Скорее всего, вы с удивлением обнаружите, что многие компоненты можно унифицировать: объединить похожие или удалить повторяющиеся. Отупить UI kit может быть непросто, понадобится время на ревизию системы и сильный дизайн-инструмент.Figmaотлично справляется, но иSketchне отстает.

Тупой UI kit облегчит работу над дизайном, избавив от необходимости плодить компоненты и изобретать велосипед. Разработчики тоже скажут вам спасибо за то, что они могут переиспользовать код компонента.
Тупой UI kit также может стать основой для созданияStorybookпроекта.

Выводы

Разделяя компоненты на умные и не очень, вы:

  1. создаете унифицированный интерфейс;

  2. оптимизируете дизайн и разработку, не выдумывая новые компоненты без необходимости;

  3. оставляете возможность легко вносить изменения в дизайн и код;

  4. делаете поведение компонентов предсказуемым для пользователей.


Больше о проектировании интерфейсов и UX можно почитать в моём телеграм-канале Поясни за UX.

Подробнее..

Как создают и поддерживают веб-страницы tinkoff.ru

22.04.2021 14:23:53 | Автор: admin

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

Как это работает сейчас

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

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

Макет блока в FigmaМакет блока в Figma

Страницы собираются из блоков в нашем Конструкторе.

Конструктор страницКонструктор страниц

Вот пример такого блока с карточками:

Блок Продуктовые карточкиБлок Продуктовые карточки

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

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

Экран собранных страниц и черновиковЭкран собранных страниц и черновиков

Мы тестируем много страниц и иногда для теста создаём новые блоки. Раньше создание нового блока занимало у команды разработки до 2 недель. При этом если если тест не проходит, то от блока отказываемся.

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

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

Десктоп и мобильная версия страницы Тинькофф ПлатинумДесктоп и мобильная версия страницы Тинькофф Платинум

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

Как было раньше

Вернемся на пять лет назад и посмотрим, как у нас был выстроен процесс создания страниц.

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

Тогда придумали систему, назовем ее версия 0. К сожалению, ее скриншоты не сохранились. Вся страница выглядела как один большой блок с формой. Даже такой подход всех устроил, его стали активно использовать и сделали админку с Конструктором версии 1.0.

Конструктор версии 1.0Конструктор версии 1.0

У старой админки еще не было интерфейса, в котором можно было собирать лайв-превью страниц, не было превью блоков, но она уже обладала рядом функций, которые всех устраивали. Функционала становилось все больше, добавилась, например, возможность просматривать историю публикаций и изменений, появилась сложная настройка блоков. Админка превратилась в версию 2.0.

Конструктор версии 2.0.Конструктор версии 2.0.

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

Сформулировали цели для новой админки:

  • Уменьшить порог вхождения новых пользователей. Нам было важно, чтобы не только контент-менеджеры могли собирать страницы, но и дизайнеры, заказчики и менеджеры.

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

  • Сократить время выпуска новых страниц.

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

Результатом стала админка версии 3.0.

Конструктор версии 3.0.Конструктор версии 3.0.

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

Проблема с блоками

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

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

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

Какие у этого минусы::

  • Затраты на разработку. Разные команды делают одно и то же.

  • Много одинаковых блоков.

  • Один и тот же блок выглядит по-разному на разных страницах, из-за этого у нас нет консистентности.

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

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

Проблема с процессом сборки страниц

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

Давайте посмотрим на средний состав участников процесса создания страниц:

  • Заказчик человек, которому нужна страница, чаще всего это product owner.

  • Копирайтер пишет текстовый прототип страницы.

  • Дизайнер собирает макет.

  • Разработчики подключаются, когда нужна разработка нового блока.

  • Контент-менеджеры все еще специалисты нашей админки, которые настраивают сложные блоки (футер, шапку, формы с различными шагами).

  • Аналитики вносят свои настройки в страницу.

Например, заказчик мог пойти в админку, собрать и опубликовать страницу. Либо он мог отдать задачу на создание текстового прототипа копирайтеру. Копирайтер все это передает контент-менеджеру, а контент-менеджер в админке собирает страницу. Либо заказчик ставит задачу дизайнеру, дизайнер в Figma собирает макет, контент-менеджер по макету из Figma собирает в админке страницу. Таких процессов у нас было очень много, но усредненно процесс выглядел так:

Заказчик ставит задачу копирайтеру, копирайтер отдает текстовый прототип дизайнеру, дизайнер собирает в Figma макет, контент-менеджер по figma-макету собирает в админке страницу, все ребята его согласовывают. Если нужен блок, ставят задачу разработчикам и затем передают аналитику для окончательной настройки, после чего страница запускается.

Минусы такого подхода:

  • Планирование времени участниками. Случались ситуации, когда новый участник процесса узнавал о задаче за неделю до выпуска.

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

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

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

  • Нет понимания зон ответственности и результатов работы.

  • Увеличивается срок выпуска страниц.

Решение

Столкнувшись с вышеописанными проблемами, мы решили:

  • Описать правильный и понятный процесс выпуска страниц, потому что у ребят уже были свои чек-листы, гайды и так далее.

  • Описать зоны ответственности. Собрать все вместе и превратить в нормальный процесс.

  • Дизайнеры собирают страницы в Конструкторе. Перевести всех дизайнеров со сборки страниц в Figma в наш Конструктор, чтобы они понимали, как работают блоки, и из них составляли страницы.

  • Все команды подключаются на старте.

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

Копирайтер может использовать артефакты: он может пойти в сторибук, посмотреть, как работают блоки, если ему так проще составлять прототип; может пойти в Figma либо в Конструктор. Результатом его работы будет текстовый прототип (на самом деле не только текстовый прототип может быть собран в PDF или в Figma, и это будет результатом работы).

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

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

Как создают новые блоки?

Дизайнер рисует мобильную версию страницы и в процессе понимает, что ему нужен блок (карточка с иконкой), которого у нас еще нет. Согласно процессу, он идет в команду дизайна и говорит: Нам нужен новый блок. Ребята понимают, что это многофункциональный блок, который может понадобиться всем, его можно встраивать и на другие страницы. Тогда задача передается в команду админки, у которой есть два варианта:

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

  2. Нужного блока нет, и похожего тоже нет. Тогда дизайнер идет в Figma и там собирает макет для разработчиков, отрисовывает все состояния и, когда макет собран, он передается в команду админки, которая создает схемы и UI блока. То есть команда админки решает, какие опции должны быть в настройках карточки. Команда блоков верстает и тестирует этот блок.

Макет блока для вёрсткиМакет блока для вёрстки

При разработке блоков используют инструмент Multistory, который разработала команда блоков.

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

MultiStoryMultiStory

Когда блок уже сделан, он попадает в библиотеку блоков нашего Конструктора, где его могут все использовать.

Библиотека блоков в КонструктореБиблиотека блоков в Конструкторе

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

RealStoryRealStory

Вернемся к процессу

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

Если, например, контент-менеджер не настроил аналитику сам, то задача аналитика настроить эти блоки: какие блоки надо трекать, настройки шеринга либо настройки оптимизации и персонализации. После задача попадает обратно в эпик.

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

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

Процесс в NotionПроцесс в Notion

Что будет дальше?

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

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

Подробнее..

Компонентный подход. Компонент SQL миграций на PHP

05.05.2021 18:14:12 | Автор: admin

Не писал на Хабре еще о том, как я пришел к мысли формирования компонентов для своих будущих проектов или текущий вместо прямого написания кода. Если очень коротко сказать про это, то было все примерно так... Много писал разных проектов, придумывал псевдо компоненты и каждый раз натыкался на то, что в одном проекте ужасно удобно это использовать, а в другом ужасно не удобно. Попробовал перенести "удобные" компоненты в проект и стало все еще более не удобно... Короче, руки не из того места, голова слишком амбициозная... Со временем я дошел до другой мысли: "Надо делать репозитории на GitHub с отдельными компонентами, которые не будут иметь зависимость от других компонентов"... Все шло хорошо, но дошел я до того самого компонента, которые хочет работать с другим компонентом... В итоге на помощь пришли интерфейсы с методами. И вот теперь поговорим о компоненте SQL миграций в том ключе, как я его вижу.

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

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

Идея

Компонент миграций, поскольку он исключительно для SQL операций, будет иметь в основе своем 2 SQL файла. Да, вот тут сейчас будет шквал критики по поводу входного порога и прочего, но скажу сразу, что со временем работы в компании мы от SQLBuilder перешли на чистый SQL, так как это быстрее. К тому же, большинство современных IDE может генерировать DDL для операций с базой данных. И вот представьте, надо вам создать таблицу, наполнить ее данными, а также что-то изменить в другой таблице. С одной стороны вы получаете длинный код билдером, с другой стороны можете использовать SQL чистый в том же билдере, а еще может быть эта ситуация вперемешку... Короче, тут я понял и решил, что в моем компоненте и подходе к программированию в целом будет как можно меньше двойственности. В связи с этим, я решил использовать только SQL код.

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

Разбор компонента

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

Как пример реализации обертки был реализован классConsoleSqlMigration, которые наследуется отSqlMigrationи переопределяет его методы. Переопределение первоначально вызываетparent::после чего реализует дополнительную логику в выводе сообщений в консоль (терминал).

Для реализации компонента необходимо передать класс реализующий интерфейсDatabaseInterfaceи массив настроек. Обязательными параметрами в настройках являются:

  • schema- схема в базе данных для миграций

  • table- таблица в базе данных для миграций

  • path- путь в файловой структуре для папки с миграциями

Компонент самостоятельно проверяет и создает необходимые (указанные) схемы, таблицы и папки при наличии заранее определенных разрешений (прав). Для корректной работы с базой данных необходимо заранее установить соединение с ней.

Для реализации класса SqlMigration потребуется реализовать его интерфейс. Конечно, класс уже готов, но мало ли вы захотите реализовать как-то по своему. В интерфейсе всего немного методов:

  1. public function up(int $count = 0): array;

  2. public function down(int $count = 0): array;

  3. public function history(int $limit = 0): array;

  4. public function create(string $name): bool;

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

/** * Применяет указанное количество миграций * * @param int $count Количество миграция (0 - относительно всех) * * @return array Возвращает список применения и ошибочных миграций. Список может иметь вид: * 1. Случай, когда отсутствуют миграции для выполнения, то возвращается пустой массив * 2. Когда присутствуют миграции для выполнения: * [ *  'success' => [...], *  'error' => [...] * ] * Ключ error добавляется только в случае ошибки выполнения миграции. * * @throws SqlMigrationException */public function up(int $count = 0): array;/** * Отменяет указанное количество миграций * * @param int $count Количество миграция (0 - относительно всех) * * @return array Возвращает список отменных и ошибочных миграций. Список может иметь вид: * 1. Случай, когда отсутствуют миграции для выполнения, то возвращается пустой массив * 2. Когда присутствуют миграции для выполнения: * [ *  'success' => [...], *  'error' => [...] * ] * Ключ error добавляется только в случае ошибки выполнения миграции. * * @throws SqlMigrationException */public function down(int $count = 0): array;/** * Возвращает список сообщений о примененных миграций * * @param int $limit Ограничение длины списка (null - полный список) * * @return array */public function history(int $limit = 0): array;/** * Создает новую миграцию и возвращает сообщение об успешном создании миграции * * @param string $name Название миграции * * @return bool Возвращает true, если миграция была успешно создана. В остальных случаях выкидывает исключение * * @throws RuntimeException|SqlMigrationException */public function create(string $name): bool;

Теперь перейдем непосредственно к классу SqlMigration. Для начала определим константы операций. Это надо будет для того, чтобы в последующих универсальных методах определить точное действие миграции:

/** * Константы для определения типа миграции */public const UP = 'up';public const DOWN = 'down';

Для работы компонента нужен массив его настроек и интерфейс для работы с БД. Для работы с БД будет использоваться мой персональный интерфейс DatabaseInterface. В конструкторе нашего класса мы будем устанавливать зависимости (DI) и проверять корректность переданных настроек:

/** * SqlMigration constructor. * * @param DatabaseInterface $database Компонент работы с базой данных * @param array $settings Массив настроек * * @throws SqlMigrationException */public function __construct(DatabaseInterface $database, array $settings) {$this->database = $database;$this->settings = $settings;foreach (['schema', 'table', 'path'] as $settingsKey) {if (!array_key_exists($settingsKey, $settings)) {throw new SqlMigrationException("Отсутствуют {$settingsKey} настроек.");}}}

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

/** * Создает схему и таблицу в случае их отсутствия * * @return bool Возвращает true, если схема и таблица миграции была создана успешно. В остальных случаях выкидывает * исключение * * @throws SqlMigrationException */public function initSchemaAndTable(): bool {$schemaSql = <<<SQLCREATE SCHEMA IF NOT EXISTS {$this->settings['schema']};SQL;if (!$this->database->execute($schemaSql)) {throw new SqlMigrationException('Ошибка создания схемы миграции');}$tableSql = <<<SQLCREATE TABLE IF NOT EXISTS {$this->settings['schema']}.{$this->settings['table']} ("name" varchar(180) COLLATE "default" NOT NULL,apply_time int4,CONSTRAINT {$this->settings['table']}_pk PRIMARY KEY ("name")) WITH (OIDS=FALSE)SQL;if (!$this->database->execute($tableSql)) {throw new SqlMigrationException('Ошибка создания таблицы миграции');}return true;}

Теперь надо подготовить методы для работы с миграциями. Начнем с генерации и валидации имени миграции (папки миграции):

/** * Проверяет имя миграции на корректность * * @param string $name Название миграции * * @throws SqlMigrationException */protected function validateName(string $name): void {if (!preg_match('/^[\w]+$/', $name)) {throw new SqlMigrationException('Имя миграции должно содержать только буквы, цифры и символы подчеркивания.');}}/** * Создает имя миграции по шаблону: m{дата в формате Ymd_His}_name * * @param string $name Название миграции * * @return string */protected function generateName(string $name): string {return 'm' . gmdate('Ymd_His') . "_{$name}";}

Следующим этапом будет создание самой миграции, а именно папки и файлов в ней. Папка будет иметь определенный формат имени: m_дата_пользовательское_имя - а проверка имени файла осуществляется на буквы, цифры и символы подчеркивания:

/** * @inheritDoc * * @throws RuntimeException|SqlMigrationException */public function create(string $name): bool {$this->validateName($name);$migrationMame = $this->generateName($name);$path = "{$this->settings['path']}/{$migrationMame}";if (!mkdir($path, 0775, true) && !is_dir($path)) {throw new RuntimeException("Ошибка создания директории. Директория {$path}не была создана");}if (file_put_contents($path . '/up.sql', '') === false) {throw new RuntimeException("Ошибка создания файла миграции {$path}/up.sql");}if (!file_put_contents($path . '/down.sql', '') === false) {throw new RuntimeException("Ошибка создания файла миграции {$path}/down.sql");}return true;}

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

/** * Возвращает список примененных миграций * * @param int $limit Ограничение длины списка (null - полный список) * * @return array */protected function getHistoryList(int $limit = 0): array {$limitSql = $limit === 0 ? '' : "LIMIT {$limit}";$historySql = <<<SQLSELECT "name", apply_timeFROM {$this->settings['schema']}.{$this->settings['table']}ORDER BY apply_time DESC, "name" DESC {$limitSql}SQL;return $this->database->queryAll($historySql);}

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

/** * @inheritDoc */public function history(int $limit = 0): array {$historyList = $this->getHistoryList($limit);if (empty($historyList)) {return ['История миграций пуста'];}$messages = [];foreach ($historyList as $historyRow) {$messages[] = "Миграция {$historyRow['name']} от " . date('Y-m-d H:i:s', $historyRow['apply_time']);}return $messages;}

Так, получили все миграции, которые были применены, а теперь сделаем возможность добавления и удаления миграции в БД. Это надо для того, чтобы при применении миграции записать ее в список уже примененных и не применять заново.

/** * Добавляет запись в таблицу миграций * * @param string $name Наименование миграции * * @return bool Возвращает true, если миграция была успешно применена (добавлена в таблицу миграций). * В остальных случаях выкидывает исключение. * * @throws SqlMigrationException */protected function addHistory(string $name): bool {$sql = <<<SQLINSERT INTO {$this->settings['schema']}.{$this->settings['table']} ("name", apply_time) VALUES(:name, :apply_time);SQL;if (!$this->database->execute($sql, ['name' => $name, 'apply_time' => time()])) {throw new SqlMigrationException("Ошибка применения миграция {$name}");}return true;}/** * Удаляет миграцию из таблицы миграций * * @param string $name Наименование миграции * * @return bool Возвращает true, если миграция была успешно отменена (удалена из таблицы миграций). * В остальных случаях выкидывает исключение. * * @throws SqlMigrationException */protected function removeHistory(string $name): bool {$sql = <<<SQLDELETE FROM {$this->settings['schema']}.{$this->settings['table']} WHERE "name" = :name;SQL;if (!$this->database->execute($sql, ['name' => $name])) {throw new SqlMigrationException("Ошибка отмены миграции {$name}");}return true;}

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

/** * Возвращает список не примененных миграций * * @return array */protected function getNotAppliedList(): array {$historyList = $this->getHistoryList();$historyMap = [];foreach ($historyList as $item) {$historyMap[$item['name']] = true;}$notApplied = [];$directoryList = glob("{$this->settings['path']}/m*_*_*");foreach ($directoryList as $directory) {if (!is_dir($directory)) {continue;}$directoryParts = explode('/', $directory);preg_match('/^(m(\d{8}_?\d{6})\D.*?)$/is', end($directoryParts), $matches);$migrationName = $matches[1];if (!isset($historyMap[$migrationName])) {$migrationDateTime = DateTime::createFromFormat('Ymd_His', $matches[2])->format('Y-m-d H:i:s');$notApplied[] = ['path' => $directory,'name' => $migrationName,'date_time' => $migrationDateTime];}}ksort($notApplied);return $notApplied;}

И теперь осталось написать методы для накатывания и отката миграции: up и down. Но тут есть маленький нюанс, up и down доступны для вызова и работают одинаково за исключением применяемого файла. Следовательно, надо сделать центральный метод, который выполняет миграцию. Такой метод на вход будет принимать список миграций для выполнения, количество миграций для ограничения (если надо) и тип (up/down - константы, которые мы указали в начале).

/** * Выполняет миграции * * @param array $list Массив миграций * @param int $count Количество миграций для применения * @param string $type Тип миграции (up/down) * * @return array Список выполненных миграций * * @throws RuntimeException */protected function execute(array $list, int $count, string $type): array {$migrationInfo = [];for ($index = 0; $index < $count; $index++) {$migration = $list[$index];$migration['path'] = array_key_exists('path', $migration) ? $migration['path'] :"{$this->settings['path']}/{$migration['name']}";$migrationContent = file_get_contents("{$migration['path']}/{$type}.sql");if ($migrationContent === false) {throw new RuntimeException('Ошибка поиска/чтения миграции');}try {if (!empty($migrationContent)) {$this->database->beginTransaction();$this->database->execute($migrationContent);$this->database->commit();}if ($type === self::UP) {$this->addHistory($migration['name']);} else {$this->removeHistory($migration['name']);}$migrationInfo['success'][] = $migration;} catch (SqlMigrationException | PDOException $exception) {$migrationInfo['error'][] = array_merge($migration, ['errorMessage' => $exception->getMessage()]);break;}}return $migrationInfo;}

Метод до жути простой:

  1. Идет по каждой миграции в ограничение количества миграций для выполнения и берем ее по индексу

  2. Получаем путь до миграции $migration['path'] = array_key_exists('path', $migration) ? $migration['path'] : "{$this->settings['path']}/{$migration['name']}";

  3. Далее получаем содержимое файла с определенным типом (говорили выше): $migrationContent = file_get_contents("{$migration['path']}/{$type}.sql");

  4. И далее просто выполняем все это дело в транзакции. Если UP - до добавляем в истории, а иначе удаляем из истории.

  5. В конце пишем информацию по примененным и ошибочным миграциям (будет одна, так как на этом все упадет).

Достаточно просто, согласитесь. Ну а теперь распишем одинаковые (почти) методы up и down:

/** * @inheritDoc */public function up(int $count = 0): array {$executeList = $this->getNotAppliedList();if (empty($executeList)) {return [];}$executeListCount = count($executeList);$executeCount = $count === 0 ? $executeListCount : min($count, $executeListCount);return $this->execute($executeList, $executeCount, self::UP);}/** * @inheritDoc */public function down(int $count = 0): array {$executeList = $this->getHistoryList();if (empty($executeList)) {return [];}$executeListCount = count($executeList);$executeCount = $count === 0 ? $executeListCount : min($count, $executeListCount);return $this->execute($executeList, $executeCount, self::DOWN);}

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

<?phpdeclare(strict_types = 1);namespace mepihindeveloper\components;use mepihindeveloper\components\exceptions\SqlMigrationException;use mepihindeveloper\components\interfaces\DatabaseInterface;use RuntimeException;/** * Class ConsoleSqlMigration * * Класс предназначен для работы с SQL миграциями с выводом сообщений в консоль (терминал) * * @package mepihindeveloper\components */class ConsoleSqlMigration extends SqlMigration {public function __construct(DatabaseInterface $database, array $settings) {parent::__construct($database, $settings);try {$this->initSchemaAndTable();Console::writeLine('Схема и таблица для миграции были успешно созданы', Console::FG_GREEN);} catch (SqlMigrationException $exception) {Console::writeLine($exception->getMessage(), Console::FG_RED);exit;}}public function up(int $count = 0): array {$migrations = parent::up($count);if (empty($migrations)) {Console::writeLine("Нет миграций для применения");exit;}foreach ($migrations['success'] as $successMigration) {Console::writeLine("Миграция {$successMigration['name']} успешно применена", Console::FG_GREEN);}if (array_key_exists('error', $migrations)) {foreach ($migrations['error'] as $errorMigration) {Console::writeLine("Ошибка применения миграции {$errorMigration['name']}", Console::FG_RED);}exit;}return $migrations;}public function down(int $count = 0): array {$migrations = parent::down($count);if (empty($migrations)) {Console::writeLine("Нет миграций для отмены");exit;}if (array_key_exists('error', $migrations)) {foreach ($migrations['error'] as $errorMigration) {Console::writeLine("Ошибка отмены миграции {$errorMigration['name']} : " .PHP_EOL .$errorMigration['errorMessage'],Console::FG_RED);}exit;}foreach ($migrations['success'] as $successMigration) {Console::writeLine("Миграция {$successMigration['name']} успешно отменена", Console::FG_GREEN);}return $migrations;}public function create(string $name): bool {try {parent::create($name);Console::writeLine("Миграция {$name} успешно создана");} catch (RuntimeException | SqlMigrationException $exception) {Console::writeLine($exception->getMessage(), Console::FG_RED);return false;}return true;}public function history(int $limit = 0): array {$historyList = parent::history($limit);foreach ($historyList as $historyRow) {Console::writeLine($historyRow);}return $historyList;}}

Соглашусь, что компонент вышел не прям убойный и есть вопросы по DI к нему, но работает исправно хорошо. Данный компонент можно посмотреть на GitHub и в Composer.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru