Skeleton Mammoth — или как я решал проблему переиспользуемых скелетон лоадеров
- четверг, 3 августа 2023 г. в 00:00:18
Существует множество отличных статей, посвященных скелетон загрузчикам, в которых рассказывается об их типах, случаях и необходимости их использования. Я не буду перечислять их здесь, вы можете легко найти их в своем любимом поисковике.
После подробного изучения этой темы, я решил создать очень простое, гибкое, переиспользуемое, настраиваемое и легкое решение, которое подойдет для большинства случаев использования. В этой статье я опишу процесс создания этого решения и превращения его в библиотеку, а также трудности, с которыми я столкнулся при работе над ним.
Примечание: Вы можете пропустить этот раздел, если знаете, что такое скелетон.
Скелетон загрузчик, также известный как скелетон экран или заполнитель, представляет собой шаблон проектирования пользовательского интерфейса, используемый для улучшения взаимодействия с пользователем во время загрузки содержимого в веб и мобильных приложениях. Когда данные загружаются или обрабатываются в фоновом режиме, вместо отображения пустого экрана, скелетон имитирует макет страницы, предоставляя пользователям визуальную подсказку о том, чего ожидать, сокращая воспринимаемое время загрузки и смягчая потенциальное разочарование.
Вот примеры скелетонов из LinkedIn и Youtube:
Улучшенное взаимодействия с пользователем: Скелетоны улучшают взаимодействие с пользователем, предоставляя визуальную обратную связь и уменьшая восприятие задержек загрузки контента.
Уменьшение показателей отказов: Они могут препятствовать тому, что пользователь покинет страницу из-за задержек загрузки.
Плавные переходы: Создают более плавные переходы между различными состояниями страницы или приложения.
В отличие от спиннеров, скелетоны привлекают внимание пользователя к прогрессу, а не к времени ожидания.
Принимая во внимание существование множества примеров создания собственных скелетонов и наличие готовых библиотек, которые сделали это за вас, с ними все равно остается ряд недостатков.
Ограниченная настраиваемость: Многие существующие скелетоны имеют ограниченные возможности настройки. Это приводит к несоответствию стилей фактического дизайна и скелетона.
Хотя их целью является обеспечение визуального представления, большинство из них не адаптированы для пользователей с нарушениями зрения, или тех, кто использует программы для помощи чтения с экрана.
Универсальность и возможность повторного использования. Большинство подходов к созданию скелетонов предлагают либо создание поверхностных копий компонентов в виде заполнителей, в результате чего получается множество копий, либо существенное изменение структуры существующих компонентов. Оба подхода требуют достаточно много усилий для дополнительной разработки и ресурсов.
Сложность обслуживания: По мере развития веб-сайтов и изменения контента, поддержание скелетонов в актуальном состоянии может стать довольно сложным.
Существуют некоторые «альтернативы» использованию скелетонов.
Забегая вперед и отвечая, действительно ли существуют альтернативы, мой ответ скорее нет, чем да. Если говорить о правильном использовании, то скелетон — одно из лучших решений. Ниже я все же приведу пару примеров вместе с их плюсами и минусами.
Спиннер - распространенная альтернатива скелетонам. Они состоят из анимированных иконок, которые постоянно вращаются, визуально показывая, что контент загружается.
Плюсы.
Простота: Простая реализация, часто требующая всего нескольких строк кода или использующая предварительно разработанные библиотеки.
Универсальное понимание: Спиннеры широко известны на разных платформах и приложениях, гарантируя, что пользователи понимают что контент загружается.
Минусы.
Ограниченная информация: Спиннеры не предоставляют никакой информации о загружаемом контенте.
Перекрывает всю страницу или ее большую часть, а не отдельные элементы. Что дает ощущение загрузки не отдельных элементов, а всего сайта в целом.
Спиннеры — неотъемлемая часть интерфейсов, но они не совсем подходят для замены скелетонов.
Прогресс Бар — это визуальный элемент, указывающий на статус завершения задачи или процесса. Он обеспечивает линейное представление, обычно с заполняемой частью, которая постепенно увеличивается.
Плюсы.
Точная обратная связь: Обеспечивает точную обратную связь о статусе завершения задачи.
Оценка времени: Прогресс бары могут дать пользователям оценку оставшегося времени, необходимого для завершения.
Многоцелевые: Можно использовать в различных контекстах и сценариях, что делает их универсальным компонентом в веб-разработке и разработке приложений.
Минусы.
Недостаток контекста: В некоторых случаях индикаторы выполнения могут не давать достаточного контекста о фактической задаче или процессе, который они представляют.
Сложность реализации: Создание индикаторов выполнения с точным представлением и плавной анимацией может быть сложным, особенно при работе с задачами разной продолжительности и временем отклика.
Прогресс бары больше подходят для сценариев, показывающих ход загрузки файла или количественный прогресс. Они часто используются в верхней части страниц, чтобы показать ход загрузки всей страницы. Но они не могут служить равноценной заменой скелетону, потому что предназначены для других целей.
Да, отсутствие загрузчиков или заполнителей также является альтернативой. И в ряде случаев это будет лучшим решением, чем использование неподходящих элементов. Главный и пожалуй единственный плюс в том, что вам не нужно тратить дополнительное время и ресурсы на внедрение. Но здесь появляются очевидные минусы — менее привлекательный дизайн вашего сайта и восприятие более медленного времени загрузки.
После того, как я получил достаточно знаний о том, что такое скелетоны, когда их использовать, какие они бывают и подходы к их разработке, я попытался определить для себя, каким должен быть мой конечный результат.
Существует много примеров с достаточно сложными подходами, где вам нужно создать отдельный скелетон для каждого компонента, в котором вы хотите его использовать.
В моем случае, я хотел чтобы это было что-то уникальное, что можно было бы повторно использовать в большинстве сценариев и не привязываться к какому-либо JavaScript фреймворку (например, React.js или Vue.js).
Поскольку каждый проект и каждый случай могут быть совершенно разными, мой скелетон должен быть настраиваемый.
Помимо стандартного набора, хотелось добавить в него поддержку дополнительных полезных и нужных функций.
Легковесный и максимально свободный от других сторонних зависимостей.
Все эти ожидания и исследования привели меня к тому, что мой будущий скелетон должен был быть написан на чистом CSS, без какого-либо JavaScript и сторонних зависимостей. Это позволяет быть легковесным и свободным от зависимостей. Основная идея заключается в том, что он наследует макеты/стили компонентов, к которым он применяется, и кастомизирует их с помощью собственных стилей. Возможно, в будущем для целей разработки имеет смысл все это переписать на синтаксис SCSS, так как это сделает код короче и переиспользуемым, а финальная сборка все равно будет компилироваться в чистый CSS.
В качестве базового примера и в целях демонстрации я буду использовать React.js и возьму разметку компонента обычной базовой карточки, чтобы показать, как это работает. Но напомню, что библиотека не привязана ни к одному из фреймворков, а в конце статьи будут ссылки на исходный код библиотеки и демо.
Вот пример разметки карточки, которая имеет свои стили и еще не знает о существовании скелетона.
<div className='card'>
<div className='card__img-wrapper'>
<img className='card__img' src={require(`../../images/cards/${imgUrl}`)}/>
</div>
<div className='card__body'>
<div className='card__details'>
<p className='card__title'>{title}</p>
<p className='card__subtitle'>{subtitle}</p>
</div>
</div>
</div>
Для того чтобы активировать скелетон, необходимо лишь применить родительский класс sm-loading
к самой карточке, и дочерние классы sm-item-primary
или sm-item-secondary
к тем элементам, на которых мы хотим видеть скелетон. Таким образом, обновленный результат будет выглядеть так:
<div className={`card ${dataState.dataStatus === 'loading' ? "sm-loading" : ""}`}>
<div className='card__img-wrapper sm-item-primary'>
<img className='card__img' src={require(`../../images/cards/${imgUrl}`)}/>
</div>
<div className='card__body'>
<div className='card__details'>
<p className='card__title sm-item-secondary'>{title}</p>
<p className='card__subtitle sm-item-secondary'>{subtitle}</p>
</div>
</div>
</div>
Давайте я объясню что тут происходит поэтапно. В данной строке:
<div className={`card ${dataState.dataStatus === 'loading' ? "sm-loading" : ""}`}>
Я применяю класс sm-loading
в зависимости от условия. Если статус dataState.dataStatus
имеет значение loading
, то класс будет применен, иначе — нет. Класс sm-loading
должен быть установлен/присутствовать только во время загрузки ваших данных. Это что-то вроде переключателя. Только при его наличии дочерние элементы с наличием соответствующих классов sm-item-primary
или sm-item-secondary
будут отображать скелетон. Таким образом, всего 3 класса приведут скелетон в действие.
Корневые переменные.
Чтобы иметь аккуратный и переиспользуемый код, а также возможность дальнейшей настройки (переопределения), я создал корневые переменные с базовыми стилями.
/* Root variables.
--------------------------------------------------------------------------------*/
:root {
/* Light theme colors. */
--sm-color-light-primary: 204, 204, 204, 1;
--sm-color-light-secondary: 227, 227, 227, 1;
--sm-color-light-animation-primary: color-mix(
in srgb,
#fff 15%,
rgba(var(--sm-color-light-primary))
);
--sm-color-light-animation-secondary: color-mix(
in srgb,
#fff 15%,
rgba(var(--sm-color-light-secondary))
);
/* Dark theme colors. */
--sm-color-dark-primary: 37, 37, 37, 1;
--sm-color-dark-secondary: 41, 41, 41, 1;
--sm-color-dark-animation-primary: color-mix(
in srgb,
#fff 2%,
rgba(var(--sm-color-dark-primary))
);
--sm-color-dark-animation-secondary: color-mix(
in srgb,
#fff 2%,
rgba(var(--sm-color-dark-secondary))
);
/* Animations. */
--sm-animation-duration: 1.5s;
--sm-animation-timing-function: linear;
--sm-animation-iteration-count: infinite;
}
Здесь задаются значения цветов для статического (без анимации) и анимированного скелетона, а также настройки анимации.
Базовые стили.
Следующий раздел файла посвящен базовым стилям, не связанными ни с какой цветовой схемой или конфигурацией.
/* Base styles.
Applied by default and not related to any of the color scheme.
--------------------------------------------------------------------------------*/
.sm-loading .sm-item-primary,
.sm-loading .sm-item-secondary {
border-color: transparent !important;
color: transparent !important;
cursor: wait;
outline: none;
position: relative;
user-select: none;
}
.sm-loading .sm-item-primary:before,
.sm-loading .sm-item-secondary:before {
clip: rect(1px, 1px, 1px, 1px);
content: "Loading, please wait.";
inset: 0;
overflow: hidden;
position: absolute;
white-space: nowrap;
}
.sm-loading .sm-item-primary::placeholder,
.sm-loading .sm-item-secondary::placeholder {
color: transparent !important;
}
.sm-loading .sm-item-primary *,
.sm-loading .sm-item-secondary * {
visibility: hidden;
}
.sm-loading .sm-item-primary :empty:after,
.sm-loading .sm-item-primary:empty:after,
.sm-loading .sm-item-secondary :empty:after,
.sm-loading .sm-item-secondary:empty:after {
content: "\00a0";
}
/* Animations related styles. */
@keyframes --sm--animation-wave {
to {
background-position-x: -200%;
}
}
@keyframes --sm--animation-wave-reverse {
to {
background-position-x: 200%;
}
}
@keyframes --sm--animation-pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.6;
}
100% {
opacity: 1;
}
}
.sm-loading .sm-item-primary,
.sm-loading .sm-item-secondary {
animation: var(--sm-animation-duration) --sm--animation-wave
var(--sm-animation-timing-function) var(--sm-animation-iteration-count);
}
Как указывалось ранее, sm-loading
родительского класса используется для активации стилей скелетона. Классы sm-item-primary
и sm-item-secondary
переопределяют/дополняют стили элемента и отображают скелетон.
Таким образом, стили и размеры элементов (в нашем случае компонента карточки) сохраняются и наследуются скелетоном. Дополнительно хочу сказать, что при таком подходе мы гарантируем, что все дочерние элементы классов sm-item-primary
или sm-item-secondary
будут скрыты и как минимум имеют символ Неразрывного пробела. Если элемент совсем не имеет содержимого, этот символ обеспечивает отображение и визуализацию элемента. Также есть часть, которая отвечает за пользователей программ помогающий чтению с экрана (screen readers) и дает им знать, что контент находится в процессе загрузки.
Далее идет разделение на тематические разделы, такие как цветовая схема, анимация, доступность. Давайте посмотрим на стили для светлой темы.
/* Light theme.
The library's default color scheme.
Styles applied to the light color scheme.
--------------------------------------------------------------------------------*/
.sm-loading .sm-item-primary {
background: rgba(var(--sm-color-light-primary));
}
.sm-loading .sm-item-secondary {
background: rgba(var(--sm-color-light-secondary));
}
/* Animations related styles. */
.sm-loading .sm-item-primary {
background: linear-gradient(
90deg,
transparent 40%,
var(--sm-color-light-animation-primary) 50%,
transparent 60%
)
rgba(var(--sm-color-light-primary));
background-size: 200% 100%;
}
.sm-loading .sm-item-secondary {
background: linear-gradient(
90deg,
transparent 40%,
var(--sm-color-light-animation-secondary) 50%,
transparent 60%
)
rgba(var(--sm-color-light-secondary));
background-size: 200% 100%;
}
Цветовая схема.
С помощью CSS медиа функции prefers-color-scheme, я реализовал автоматическую поддержку светлой и темной темы. В зависимости от настроек пользователей, она будет применяться автоматически. Конечно же есть возможность установить тему вручную, об этом я расскажу далее в статье.
/* Dark theme.
Styles to apply if a user's device settings are set to use dark color scheme.
https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme
--------------------------------------------------------------------------------*/
@media (prefers-color-scheme: dark) {
/*Omitted pieces of code.*/
}
Анимации.
По умолчанию в скелетоне я решил сделать анимацию включенной, но бывают случаи, когда разработчики или пользователи предпочли бы ее выключить. И если для первых это может быть продиктовано дизайном и требованиями, то для вторых это может быть обусловлено вестибулярно-двигательными нарушениями. В этом случае на помощь приходит CSS медиа функция prefers-reduced-motion.
/* Accessibility.
Disable animations if a user's device settings are set to reduced motion.
https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion
--------------------------------------------------------------------------------*/
@media (prefers-reduced-motion) {
/*Omitted pieces of code.*/
.sm-loading .sm-item-primary,
.sm-loading .sm-item-secondary {
animation: none;
}
/*Omitted pieces of code.*/
}
На этом этапе основные стили закончены, и скелетон можно считать готовым. Но, меня преследовала мысль, что я должен иметь возможность настраивать все вышеперечисленное. Что делать, если я хочу отключить анимацию, или если я хочу всегда использовать темную тему? Поскольку CSS не может принимать какие-либо значения в качестве аргументов, как это делают функции JavaScript, а добавление JavaScript было исключено (по крайней мере, на данном этапе), потому что это полностью нарушило бы основную концепцию простоты и легкости.
Но все же, мы можем реализовать что-то похожее на аргументы, если заранее знаем их значения. И здесь нам на помощь приходят атрибуты data-*. С их помощью мы можем проверить наличие нужного нам значения в атрибуте, и применить нужные стили.
Я покажу вам, как я это реализовал на небольшом фрагменте кода, а полную реализацию вы можете найти в исходном коде по ссылке в конце статьи.
Например, если вы хотите явно использовать темную тему, вам нужно создать JSON объект:
const config = JSON.stringify({
theme: "dark",
})
Примечание:
Атрибутыdata-*
могут работать только со строками, поэтому важно применить методJSON.stringify()
к объекту конфигурации.
Далее передаем этот объект в пользовательский атрибут data-sm-config
:
<div class="card sm-loading" data-sm-config={config}>
<!-- Omitted pieces of code. -->
</div>
Вот как это выглядит в CSS файле. Если в data-sm-config
есть значение "theme":"dark"
, применяются соответствующие стили.
.sm-loading[data-sm-config*='"theme":"dark"'] .sm-item-primary,
.sm-loading[data-sm-config*='"theme":"dark"'] .sm-item-secondary {
/* Omitted pieces of code. */
}
Переопределение стилей с помощью глобальных переменных.
Каждый проект и случай уникальны, и невозможно предугадать и сделать все универсальным. Особенно, когда дело касается цвета. Именно поэтому, как было сказано в начале статьи, большая часть значений помещена в глобальыне переменные. Если вы хотите настроить стили по умолчанию, просто переопределите соответствующие переменные в вашем собственном файле *.css
внутри CSS псевдокласса :root.
Так, например, если вы хотите изменить цвет основного элемента (с классом sm-item-primary
), вам нужно всего лишь перезаписать соответствующую переменную:
/* Your own custom.css file: */
:root {
--sm-color-light-primary: 255, 0, 0, 0.5;
}
Вы можете опробовать готовый результат в действии по следующей ссылке: Live demo.
После того, как я долго изучал тему скелетонов, их разновидностей, использования, подходов к разработке, мне удалось собрать суть полезной информации и превратить ее в конечный продукт. Собрав лучшие практики, улучшив их и объединив в единое целое, я создал библиотеку под названием Skeleton Mammoth. Я считаю, что мне удалось достичь поставленных целей и создать довольно неплохую библиотеку со всеми достоинствами, описанными в этой статье. Я надеюсь, что эта библиотека способна принести пользу людям при ее использовании или дать новые знания и опыт для создания чего-то своего.
Если вы находите для себя полезной мою библиотеку и хотели бы выразить свою поддержку, есть простые способы сделать это:
Ставьте звездочку в GitHub репозитории: это помогает повысить его видимость и позволяет другим узнать, что библиотека имеет сильную пользовательскую базу.
Распространяйте информацию: вы можете познакомить новых пользователей с библиотекой, поделившись информацией о ней на любой платформе. Например, если вы напишете об этом в блоге, упомянете в социальных сетях или обсудите в соответствующих сообществах разработчиков, это будет очень полезно.
Ниже я размещу список полезных ссылок, в том числе ссылку на библиотеку.
Skeleton Mammoth - GitHub репозиторий библиотеки.
Live demo - Демо.
Live demo source code - Демо GitHub репозиторий.