furry.cat 07 октября 2020

🐢 Каркасные экраны: реализация в React

Каркасные экраны - один из самых полезных UX-паттернов. Добавляем их в приложение React с помощью библиотеки React Loading Skeleton.

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

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

На смену лоадерам пришли "каркасные" экраны (skeleton screens), которые не просто "тянут время", но и лучше обозначают прогресс загрузки, уменьшая негативные ощущения юзера (loading-time frustration). Другими словами, создают иллюзию того, что контент вот-вот появится.

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

Разница между интерфейсом с лоадером и каркасным экраном
Разница между интерфейсом с лоадером и каркасным экраном

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

Что такое каркасные экраны?

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

Другими словами, это занятый плейсхолдерами вместо реальных данных "скелет" страницы.

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

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

Другие названия техники:

  • призрачные элементы (ghost elements),
  • заполнители контента (content placeholders),
  • загрузчики контента (content loaders).

Этим подходом пользуются многие сайты, в том числе порталы крупных технологических компаний: Blockchain.com, YouTube, Facebook, Medium.

Интерфейс Blockchain.com с частично подгруженным контентом. Для представления графиков также используется скелетный плейсхолдер.
Интерфейс Blockchain.com с частично подгруженным контентом. Для представления графиков также используется скелетный плейсхолдер.
 Каркасный экран на сайте medium
Каркасный экран на сайте medium
Главная страница сайта LinkedIn в 2018 до загрузки контента
Главная страница сайта LinkedIn в 2018 до загрузки контента

Типы каркасных экранов

Каркасные экраны бывают разными. Самые популярные – это текстовые и графические (цветные) заполнители.

Текстовые используются чаще всего, так как они максимально просты в реализации. Разработчику не нужно знать особенности реального контента, чтобы заменить его заполнителем.

Графические каркасы сложнее – они зависят от контента, который замещают, и встречаются реже.

Готовые решения

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

React Placeholder

Репозиторий: buildo/react-placeholder

Загрузка пакета:

        npm install --save react-placeholder

    

Достоинства

  • Поддерживается пульсирующая анимация (создает эффект движения на элементах-плейсхолдерах).
  • API на основе компонентов.

Недостатки

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

Пример

Пример создания скелетного компонента с использованием react-placeholder:

        import { TextBlock, RectShape } from 'react-placeholder/lib/placeholders';
import ReactPlaceholder from 'react-placeholder';

const GhostPlaceholder = () => (
  <div className='my-placeholder'>
    <RectShape color='gray' style={{width: 25, height: 70}} />
    <TextBlock rows={6} color='blue'/>
  </div>
);
<ReactPlaceholder ready={ready} customPlaceholder={<GhostPlaceholder />}>
  <MyComponent />
</ReactPlaceholder>
    

За создание каркасного плейсхолдера отвечает компонент ReactPlaceholder. Он просто оборачивает нужный компонент приложения в родительский тег. Визуальное представление мы должны собрать сами из доступных элементов и форм и передать в атрибут customPlaceholder.

Создаем для этого функциональный компонент GhostPlaceholder. Внутри него используем элементы TextBlock (блок текста) и RectShape (прямоугольник), импортированные из react-placeholder/lib/placeholder.

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

Чтобы определить, когда показывать каркас, а когда уже подъехал контент, используется булево свойство ready компонента ReactPlaceholder.

Больше информации вы найдете в документации модуля.

React Loading Skeleton

Репозиторий: dvtng/react-loading-skeleton

Загрузка пакета:

        npm install --save react-loading-skeleton

    

Достоинства

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

Недостатки

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

Пример

Пример создания скелетного компонента с использованием react-loading-skeleton:

        import Skeleton, { SkeletonTheme } from "react-loading-skeleton";

const SkeletonComponent = () => (
  <SkeletonTheme color="#202020" highlightColor="#444">
    <section>
      <Skeleton height={50} width={50} />
    </section>
  </SkeletonTheme>
);
    

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

Создаем функциональный компонент SkeletonComponent. SkeletonTheme служит оберткой для всего элемента и принимает в пропсах цвета (color, highlightColor), которые следует использовать в оформлении плейсхолдеров и создании эффектов. Внутри него располагается компонент Skeleton, размеры которого мы задаем с помощью пропсов width и height.

Рассмотрим этот модуль подробнее: разработаем с его помощью скелетный экран YouTube.

Каркасные экраны в стиле YouTube

Установка React

Самый простой способ установить и настроить React в новом приложении – использовать утилиту Create React App. При этом вам почти ничего не придется делать – только набрать команду в терминале. Сначала установите модуль глобально, если вы этого еще не сделали:

        npm install -g create-react-app
    

Затем создайте новый проект skeleton-screens:

        npx create-react-app skeleton-screens 


    

Когда установка завершится, перейдите в папку проекта и запустите локальный сервер:

        cd skeleton-screens
yarn start
    

Вы увидите приветственный экран React:

Создание интерфейса

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

Создайте файл data.js в папке src и скопируйте туда следующий код:

        const dummyData= [
  {
    section: "Recommended",
    channel: "CNN",
    items: [
      {
        id: "fDObf2AeAP4",
        image: "https://img.youtube.com/vi/fDObf2AeAP4/maxresdefault.jpg",
        title: "75 million Americans ordered to stay home",
        views: "1.9M views",
        published: "3 days agos"
      },
      {
        id: "3AzIgAa0Cm8",
        image: "https://img.youtube.com/vi/3AzIgAa0Cm8/maxresdefault.jpg",
        title: "Gupta: The truth about using chloroquine to fight coronavirus pandemic",
        views: "128K views",
        published: "4 hours ago"
      },
      {
        id: "92B37aXykYw",
        image: "https://img.youtube.com/vi/92B37aXykYw/maxresdefault.jpg",
        title: "Willie Jones STUNS Simon Cowell In Pitch Perfect Performance of 'Your Man'!",
        views: "2.47 million views",
        published: "1 month ago"
      },
      {
        id: "J6rVaFzOEP8",
        image: "https://img.youtube.com/vi/J6rVaFzOEP8/maxresdefault.jpg",
        title: "Guide To Becoming A Self-Taught Software Developer",
        views: "104K views",
        published: "17 days ago"
      },
      {
        id: "Wbk8ZrfU3EM",
        image: "https://img.youtube.com/vi/Wbk8ZrfU3EM/maxresdefault.jpg",
        title: "Tom Hanks and Rita Wilson test positive for coronavirus",
        views: "600k views",
        published: "1 week ago"
      },
      {
        id: "ikHpFgKJax8",
        image: "https://img.youtube.com/vi/ikHpFgKJax8/maxresdefault.jpg",
        title: "Faces Of Africa- The Jerry Rawlings story",
        views: "2.3 million views",
        published: "2014"
      }
    ]
  },
  {
    section: "Breaking News",
    channel: "CGTN America",
    items: [
      {
        id: "tRLDPy1A8pI",
        image: "https://img.youtube.com/vi/tRLDPy1A8pI/maxresdefault.jpg",
        title: "Is Trump blaming China for COVID-19? You decide.",
        views: "876k views",
        published: "9 days ago"
      },
      {
        id: "2ulH1R9hlG8",
        image: "https://img.youtube.com/vi/2ulH1R9hlG8/maxresdefault.jpg",
        title: "Journalist still goes to office during pandemic, see her daily routine",
        views: "873 views",
        published: "3 hours ago"
      },
      {
        id: "\_TkfQ9MaIgU",
        image: "https://img.youtube.com/vi/_TkfQ9MaIgU/maxresdefault.jpg",
        title: "How are small businesses going to survive the economic downturn of the COVID-19 era?",
        views: "283 views",
        published: "4 day ago"
      }
    ]
  }
];
export default dummyData;

    

В этом фрагменте описаны два блока страницы – "Рекомендованное" и "Последние новости" – каждый из которых содержит несколько видео-превью.

Теперь используем эти данные для верстки интерфейса. Нам потребуется три компонента:

  1. Card – превью видео. Содержит миниатюру видео, его название, количество просмотров, название канала и дату публикации.
  2. CardList – группа превью.
  3. App – корневой компонент приложения. Он получает объект dummyData, показывает каркасный экран в течение двух секунд, имитируя загрузку данных, а затем отображает компонент CardList.

Создадим папку components внутри src – в ней будем размещать файлы компонентов.

Card

Создайте файл Card.js в папке components:

Card.js
        import React from "react";
const Card = ({ item, channel }) => {
    return (
      <li className="card">
        <a
          href={`https://www.youtube.com/watch?v=${item.id}`}
          target="\_blank"
          rel="noopener noreferrer"
          className="card-link"
        >
          <img src={item.image} alt={item.title} className="card-image" />
          <img src={item.image} alt={item.title} className="channel-image" />
          <h4 className="card-title">{item.title}</h4>
          <p className="card-channel">
            <i>{channel}</i>
          </p>
          <div className="card-metrics">
            {item.views} • {item.published}
          </div>
        </a>
      </li>
    );
  };
export default Card;
    

Простой функциональный React-компонент, который выводит данные в шаблон.

CardList

Файл components/CardList.js:

CardList.js
        import React from "react";
import Card from "./Card";
const CardList = ({ list }) => {
    return (
      <ul className="list">
        {list.items.map((item, index) => {
          return <Card key={index} item={item} channel={list.channel} />;
        })}
      </ul>
    );
  };
export default CardList;
    

Этот код еще проще – он выводит на экран массив превьюшек Card.

App

Наконец, откройте файл App.js, который уже создан при инициализации проекта и обновите в нем код:

App.js
        import React, { useState, useEffect } from "react";
import "./App.css";
import dummyData from "./data";
import CardList from "./components/CardList";

const App = () => {
  const [videos, setVideos] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);
    const timer = setTimeout(() => {
      setVideos(dummyData);
      setLoading(false);
    }, 2000);
    return () => clearTimeout(timer);
  }, []);
  return (
    <div className="App">
      {
        videos.map((list, index) => {
          return (
            <section key={index}>
              <h2 className="section-title">{list.section}</h2>
              <CardList list={list} />
              <hr />
            </section>
          );
        })}
    </div>
  );
};
export default App;
    

Этот компонент сложнее, так как он имеет собственное состояние.

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

Хук useEffect используется для имитации загрузки данных с сервера с задержкой: старый-добрый setTimeout. Только вместо реальных данных мы сохраним заранее подготовленный массив из файла data.js.

После "загрузки" выводим отдельно каждый блок видео с помощью метода videos.map. Массив карточек передается в атрибуте list компоненту CardList, который отвечает за его визуализацию.

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

Стилизация

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

        .App {
  max-width: 960px;
  margin: 0 auto;
  font-size: 16px;
}
.list {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  list-style: none;
  padding: 0;
}
.section-title {
  margin-top: 30px;
}
.card {
  width: calc(33% - 10px);
  margin: 20px 0;
}
.card-link {
  color: inherit;
  text-decoration: none;
}
.card-image {
  width: 100%;
}
.channel-image {
  border-radius: 100%;
  padding: 0, 10px, 0, 0;
  width: 40px;
  height: 40px;  
}
.card-title {
  margin-top: 10px;
  margin-bottom: 0;
}
.card-channel {
  margin-top: 5px;
  margin-bottom: 5px;
  font-size: 14px;
}
/* Tablets */
@media (max-width: 1000px) {
  .App {
    max-width: 600px;
  }
  .card {
    width: calc(50% - 22px);
  }
}
/* Mobiles */
@media (max-width: 640px) {
  .App {
    max-width: 100%;
    padding: 0 15px;
  }
  .card {
    width: 100%;
  }
}

    

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

Интерфейс клона YouTube без каркасных экранов
Интерфейс клона YouTube без каркасных экранов

Подключение React Loading Skeleton

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

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

Темы

React Loading Skeleton поддерживает простую настройку тем. Вы можете легко поменять стиль сразу всех скелетных экранов, внеся изменения только в одном месте. За это отвечает компонент SkeletonTheme.

        import Skeleton, { SkeletonTheme } from "react-loading-skeleton";

<SkeletonTheme color="grey" highlightColor="#444">
  <p>
    <Skeleton height={250} width={300} count={1} />
  </p>
</SkeletonTheme>

<SkeletonTheme color="#990" highlightColor="#550">
  <p>
    <Skeleton height={250} width={300} count={1} />
  </p>
</SkeletonTheme>
    
Простая настройка темы для каркасных экранов
Простая настройка темы для каркасных экранов

Продолжительность

Помимо очевидных пропсов height, width и color, мы можем также указать свойство duration.

        <Skeleton duration={2} />

    

По умолчанию оно равно 1.2. Это значение определяет продолжительность одного цикла анимации экрана.

Больше информации о настройках компонента вы можете найти в документации.

Установка пакета

Чтобы подключить библиотеку react-loading-skeleton в проект, запустите следующую команду:

        npm install react-loading-skeleton

    

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

Теперь можно добавить каркасные экраны для видео-данных. В папке components создайте новый компонент SkeletonCard.js:

SkeletonCard.js
        import React from "react";
import Skeleton from "react-loading-skeleton";
const SkeletonCard = () => {
    return (
      <section>
        <h2 className="section-title">
          <Skeleton height={30} width={300} />
        </h2>

        <ul className="list">
          {Array(9)
            .fill()
            .map((item, index) => (
              <li className="card" key={index}>
                <Skeleton height={180} />
                <h4 className="card-title">
                  <Skeleton circle={true} height={50} width={50} />  
                  <Skeleton height={36} width={`80%`} />
                </h4>
                <p className="card-channel">
                  <Skeleton width={`60%`} />
                </p>
                <div className="card-metrics">
                  <Skeleton width={`90%`} />
                </div>
              </li>
            ))}
        </ul>
      </section>
    );
  };
  export default SkeletonCard;
    

Здесь мы создаем неупорядоченный список и заполняем его элементами (li) с помощью метода Array.fill(). Всего элементов 9: по количеству видеороликов в массиве dummyData, которые будут выведены на страницу после загрузки.

Чтобы вспомнить, как работает метод Array.fill, загляните в документацию на MDN. Нам сейчас важно только, что он создает массив с 9 элементами, которые мы можем проитерировать и с помощью метода Array.map превратить в массив элементов li.

Итак, для каждого фейкового видео у нас уже есть каркас, состоящий из нескольких блоков. Пропсы height и width отвечают за размеры блока, а circle={true} формирует круглый блок.

По умолчанию для компонента React Loading Skeleton подключена симпатичная пульсирующая анимация. При желании вы можете создать свою анимацию, мы же воспользуемся дефолтной.

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

App.js
        import SkeletonCard from './components/SkeletonCard';

const App = () => {
  // ...

  return (
    <div className="App">
      {loading && <SkeletonCard />}
      {!loading &&
        videos.map((list, index) => {
          return (
            <section key={index}>
              <h2 className="section-title">{list.section}</h2>
              <CardList list={list} />
              <hr />
            </section>
          );
        })}
    </div>
  );
};
    

Весь исходный код вы можете найти в репозитории проекта.

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

Вот так выглядит результат:

Заключение

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

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

Источники

РУБРИКИ В СТАТЬЕ

МЕРОПРИЯТИЯ

Какой паттерн для загрузки контента предпочитаете вы?

ВАКАНСИИ

Tableau developer
по итогам собеседования
Programmer Unity
Краснодар, по итогам собеседования
Frontend разработчик (react native)
по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

BUG