Создание скелетного загрузчика для Discourse

Привет :waving_hand:

Основная идея

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

Инструмент позволяет администратору выбирать реальные элементы на странице и превращать их в области скелета.

Например:

.title
.avatar
.topic-excerpt
.btn
.category-breadcrumb

Затем компонент использует эти селекторы для генерации скелета во время выполнения.

Предпросмотр скелета

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


Генерация полезных селекторов оказалась сложнее, чем ожидалось

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

Наивная реализация может легко произвести что-то вроде:

.container.list-container.--topic-list .row.full-width .contents ...

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

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

Мне действительно хотелось получить что-то ближе к:

.badge-category__name

или:

.badge-category__wrapper .d-icon

вместо селектора, описывающего весь путь в DOM.

Поэтому теперь инструмент генерирует несколько кандидатов и оценивает их на основе таких факторов, как:

  • глубина селектора
  • количество классов
  • повторяющиеся совпадения
  • классы, связанные со состоянием
  • технические классы SVG/иконок
  • соответствует ли селектор всё ещё выбранному элементу

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


Скрытые элементы

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

Например:

.alert.alert-info

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

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

Поэтому исключённые элементы не обрабатываются просто как список display: none — расчёт геометрии также должен понимать, что элемент не является частью финального макета.

Предпросмотр скелета


Навигация

Скорее всего, самым большим вызовом стала навигация.

Желаемое поведение было таким:

click

  ↓

показать скелет немедленно

  ↓

Discourse меняет маршрут

  ↓

появляется DOM назначения

  ↓

скрыть скелет

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

Оказалось, что это неправильный подход.

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

Урок был прост:

Скелет не должен становиться воротами готовности DOM.

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

Это сделало огромную разницу в воспринимаемой скорости навигации.


Вьюпорты

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

xs
sm
md
lg
xl
2xl

Конфигурация скелета дополнительно может быть сгруппирована как:

mobile → xs / sm
tablet → md
desktop → lg / xl / 2xl
all → всё

Это означает, что компоненту вообще не нужно знать фактические значения в пикселях.

Если Discourse изменит значения брейкпоинтов, компонент скелета не придётся переписывать под новые жёстко заданные числа.


Кэширование геометрии

Селекторы говорят нам, что должно быть отрисовано, но они не говорят точно, где должны появиться формы скелета.

Для этого я добавил захват геометрии.

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

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

Это было ещё одно важное различие:

определение селектора и отрисованная геометрия — это две разные вещи.


Черновики

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

Я не хотел такого рабочего процесса:

открыть инструмент

→ потратить 10 минут на настройку

→ закрыть инструмент

→ всё исчезло

Поэтому инструмент хранит незавершённый черновик отдельно от фактической настройки темы.

Черновик привязан к комбинации страницы/маршрута/вьюпорта, поэтому, например:

topic-list / lg
topic-list / md
topic-list / xs

не перезапишут друг друга случайно.

Закрытие инструмента не уничтожает проделанную работу.


Отмена

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

Инструмент хранит снимки состояния своей конфигурации:

{
  "regions": \[\],
  "excludes": \[\]
}

вместо попыток поддерживать историю операций с DOM.

Это делает систему отмены действий гораздо проще для понимания и также сохраняет её независимой от фактического DOM страницы.


Этот проект находится в активной разработке. Надеюсь, скоро он будет готов для компонента темы! :slightly_smiling_face:

3 лайка