Как стать автором
Обновить

Skeleton Mammoth — или как я решал проблему переиспользуемых скелетон лоадеров

Уровень сложностиСредний
Время на прочтение12 мин
Количество просмотров5.6K
Автор оригинала: Oleksandr Tkachenko
Skeleton Mammoth logotype.
Skeleton Mammoth logotype.

Введение

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

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

Что такое Скелетон загрузчики?

Примечание: Вы можете пропустить этот раздел, если знаете, что такое скелетон.

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

Вот примеры скелетонов из LinkedIn и Youtube:

LinkedIn skeleton screen
LinkedIn skeleton screen
YouTube skeleton screen
YouTube skeleton screen

Почему нужно использовать скелетоны?

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

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

  • Плавные переходы: Создают более плавные переходы между различными состояниями страницы или приложения.

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

Проблемы большинства существующих скелетонов

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

  • Ограниченная настраиваемость: Многие существующие скелетоны имеют ограниченные возможности настройки. Это приводит к несоответствию стилей фактического дизайна и скелетона.

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

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

  • Сложность обслуживания: По мере развития веб-сайтов и изменения контента, поддержание скелетонов в актуальном состоянии может стать довольно сложным.

Альтернативы

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

Спиннер

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

Плюсы:

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

  • Универсальное понимание: Спиннеры широко известны на разных платформах и приложениях, гарантируя, что пользователи понимают что контент загружается.

Минусы:

  • Ограниченная информация: Спиннеры не предоставляют никакой информации о загружаемом контенте.

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

Спиннеры — неотъемлемая часть интерфейсов, но они не совсем подходят для замены скелетонов.

Прогресс Бар (Индикатор выполнения)

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

Плюсы:

  • Точная обратная связь: Обеспечивает точную обратную связь о статусе завершения задачи.

  • Оценка времени: Прогресс бары могут дать пользователям оценку оставшегося времени, необходимого для завершения.

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

Минусы:

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

  • Сложность реализации: Создание индикаторов выполнения с точным представлением и плавной анимацией может быть сложным, особенно при работе с задачами разной продолжительности и временем отклика.

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

Отсутствие какого-либо визуала

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

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

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

Универсальный и переиспользуемый

Существует много примеров с достаточно сложными подходами, где вам нужно создать отдельный скелетон для каждого компонента, в котором вы хотите его использовать.
В моем случае, я хотел чтобы это было что-то уникальное, что можно было бы повторно использовать в большинстве сценариев и не привязываться к какому-либо 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 переопределяют/дополняют стили элемента и отображают скелетон.

Skeleton Mammoth structure.
Skeleton Mammoth structure.

Таким образом, стили и размеры элементов (в нашем случае компонента карточки) сохраняются и наследуются скелетоном. Дополнительно хочу сказать, что при таком подходе мы гарантируем, что все дочерние элементы классов 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;
}

Демо

Skeleton Mammoth Live Demo.
Skeleton Mammoth Live Demo.

Вы можете опробовать готовый результат в действии по следующей ссылке: Live demo.

Давайте подытожим

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

Ваша поддержка

Если вы находите для себя полезной мою библиотеку и хотели бы выразить свою поддержку, есть простые способы сделать это:

Ставьте звездочку в GitHub репозитории: это помогает повысить его видимость и позволяет другим узнать, что библиотека имеет сильную пользовательскую базу.

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

Ниже я размещу список полезных ссылок, в том числе ссылку на библиотеку.

Полезные ссылки

Теги:
Хабы:
Всего голосов 2: ↑2 и ↓0+2
Комментарии9

Публикации

Истории

Работа

Ближайшие события

27 августа – 7 октября
Премия digital-кейсов «Проксима»
МоскваОнлайн
28 – 29 сентября
Конференция E-CODE
МоскваОнлайн
28 сентября – 5 октября
О! Хакатон
Онлайн
30 сентября – 1 октября
Конференция фронтенд-разработчиков FrontendConf 2024
МоскваОнлайн
3 – 18 октября
Kokoc Hackathon 2024
Онлайн
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн