ссылка на живой пример того, что мы будем кодировать!
Https://shapirodaniel.github.io/single-page-nav/
ссылка на репозиторий GitHub и исходные файлы
Статические одностраничные прокручиваемые сайты представляют собой интересную и очень интересную задачу навигации! Если вы создали одностраничные приложения с помощью React, скорее всего, вы использовали React Router для рендеринга своих компонентов по определенным URI - и если вы создали одностраничный прокручиваемый сайт, где все компоненты отображаются с одним и тем же URI, вы уже знаете, в чем состоит складка: а именно, как нам направить пользователя в конкретную область сайта и как мы можем отразить текущее положение пользователя на нашем сайте в навигации бар?
Как пользователь,
- Я хочу щелкнуть элемент навигации и прокрутить этот элемент в мое окно просмотра, и
- Я хочу прокрутить и увидеть свое текущее положение на сайте, отображаемое на панели навигации.
Пользовательская история №1 кажется простой: мы можем повесить containerRef
на любой элемент, который нам нравится, и использовать метод scrollIntoView
интерфейса Element
для перемещения этого контейнера в область просмотра при щелчке по соответствующему элементу навигации. Пользовательская история №2 немного сложнее - как наши элементы навигации узнают, какой контейнер в настоящее время находится в области просмотра, и как мы будем передавать эту информацию вверх по пищевой цепочке, чтобы стили навигации могли отображаться соответствующим образом?
Если бы от нас не требовалось следить за текущим местонахождением пользователя, мы могли бы просто применить стиль activeClass
вместе с нашим scrollIntoView
вызовом, сохранив navLinkId
в локальном состоянии Nav
компонента.
Ура! Мы выполнили пользовательскую историю №1 - переходим к следующему этапу.
Чувак, а где мой пользователь?
Отслеживать местонахождение пользователя не должно быть слишком сложно: у нас есть обязательный дескриптор (containerRef
) для каждой из наших навигационных целей, и мы можем использовать его, чтобы определить, виден ли контейнер ref
. Это отличный вариант использования веб-APIIntersectionObserver
, который позволит нам создать наблюдателя, чтобы уведомлять нас, когда - и, что важно, в какой степени - его наблюдаемая цель пересекается с нашим полем обзора.
Экземпляр IntersectionObserver
принимает функцию обратного вызова и объект параметров, состоящий из:
- элемент
root
, который позволит нам сравнить положение нашего возможно пересекающегосяcontainerRef
с элементом или, если не указано, с окном просмотра браузера; rootMargin
(записанное как строковое свойство поля CSS), которое позволит нам расширить или сузить размер ограничивающего прямоугольника нашегоroot
элемента перед вычислением пересечений; и,- значение
threshold
(или массив значений) в диапазоне 0,0–1,0, которое поможет нам откалибровать чувствительность нашего наблюдателя, установив точки останова в зависимости от того, насколько общая площадь наблюдаемого элемента пересекаетroot
.
Поскольку нам нужно только определить, вошел ли вертикально сложенный контейнер в видимую для пользователя область страницы, мы оставим root
неуказанным и оставим его по умолчанию для нашего окна просмотра браузера. Аналогичным образом, rootMargin
значение по умолчанию 0 пикселей выполнит свою работу, но в конечном итоге нам может потребоваться калибровка нашего threshold
(подробнее об этом позже).
Наш экземпляр observer
получает обратный вызов, который ожидает два аргумента - массив entries
и сам экземпляр observer
. Массив entries
- это набор IntersectionObserverEntry
экземпляров, которые подключаются к различным свойствам взаимосвязи между пересекающимся элементом и нашим root
. Мы будем использовать логическое значение entry.isIntersecting
, чтобы отслеживать каждый containerRef
, когда он входит и покидает область просмотра.
Здорово! Мы на шаг ближе к реализации пользовательской истории №2: когда наш пользователь прокручивает страницу, у нас будет способ узнать, где они находятся в нашем одностраничном макете. Давайте создадим настраиваемую ловушку, которая позволит нам предоставить containerRef
, присоединить observer
и вернуть логическое значение, указывающее, отображается ли элемент на экране. Вся заслуга принадлежит пользователю StackOverflow Creaforge, который создал специальный хук именно для этой цели!
Давайте создадим useOnScreen
для создания пользовательского хука, useNav
, который позволит нам генерировать и возвращать наблюдаемое containerRef
, ref.current.id
которого будет зарегистрировано на NavProvider
, доступ к которому осуществляется через useContext
!
Кстати: если вы немного не уверены в своем подходе к React Context API, ознакомьтесь с моей статьей Восстановление игры с императивным кодом с нуля в React, где я излагаю хорошую общую стратегию для реализации React Context API. - легкая альтернатива Redux, которая помогает разделять и управлять частями состояния.
Наш NavContext
состоит из React.createContext()
экземпляра и NavProvider
, чьи значения провайдера activeNavLinkId
и setActiveNavLinkId
- локальное состояние, ранее обрабатываемое нашим Nav
component, - будут доступны нашему ловушке useNav
и использоваться для обновления нашего пользовательского интерфейса навигации. Но зачем изобретать велосипед? У нас уже есть управление на местном уровне, и все, что нам нужно сделать, это изменить наш useNav
хук, чтобы он принимал setActiveNavLinkId
, передав его каждому компоненту, который мы хотим зарегистрировать для наблюдения.
В данном случае я решил использовать NavProvider
, поскольку он позволяет нам инкапсулировать всю нашу логику контекста в useNav
: после того, как ловушка установлена и наши Nav
и Компоненты NavLink
построены, для назначения функциональности навигации навигационной цели потребуется только одно объявление ловушки и массив из navLink
объектов, содержащих navLinkId
и scrollToId
, каждый из которых указывает на экземпляр NavLink
и идентификатор containerRef
.
Собираем все вместе
Теперь, чтобы развернуть наш кастомный хук и немного по навигации! Начнем с того, что обернем содержимое нашего App
компонента верхнего уровня в наш NavProvider
.
Наш Nav
компонент становится немного тоньше: поскольку мы передали управление нашим activeNavLinkId
NavContext
, мы можем удалить наш локальный useState
обработчик. Давайте также реорганизуем наш NavLink
компонент в отдельный файл для лучшей модульности.
Теперь любой компонент, который мы хотели бы использовать в качестве цели для навигации, просто должен импортировать ловушку useNav
, передать ему scrollToId
и назначить возвращенный containerRef
элементу верхнего уровня компонента вместе с идентификатором, равным scrollToId
.
useNav
слепо относится к его реализации - повесив ref
на любой понравившийся элемент, мы можем создать прокручиваемую цель навигации, которая будет:
- реагировать на прямую навигацию с помощью кликов,
- регистрировать пассивную навигацию всякий раз, когда прокручиваемая пользователем цель появляется в окне просмотра нашего браузера и синхронизируется со значениями
threshold
нашегоIntersectionObserver
экземпляра, и - отражать текущее положение пользователя в нашем
Nav
компоненте!
Инкапсулируя всю логику, которую мы используем для информирования нашего Nav
компонента в useNav
, мы пришли к готовому к использованию решению для одностраничной навигации, интерфейс которого имеет минимальную площадь поверхности, что позволяет нам, чтобы легко добавлять, удалять и перемешивать элементы в нашей иерархии компонентов с возможностью навигации. И наш NavContext
на самом деле является просто конкретной реализацией более общего IntersectionContext
, который может быть использован для удовлетворения других требований пользовательских историй, связанных с пересечением, таких как отложенная загрузка контента.
Отмена ложных подсказок навигации
Есть еще одна интересная ошибка, которую нужно исправить - как предотвратить появление ложных подсказок для пользователя при переходе по щелчку мыши к удаленному (т. Е. Несмежному) компоненту относительно своего текущего положения?
CSS спешит на помощь! Мы назначим transition-delay
маске NavLink
экземпляров, которые получают activeClass
, поскольку наш scrollToSection
метод прокручивает пользователя мимо других компонентов на пути к месту назначения. Это нивелирует эффект нашего useNav
хука для всех ссылок, которые получили бы activeClass
стиль, что позволяет нам улучшить UX - в конце концов, щелкнуть ссылку и увидеть, как загораются другие ссылки, довольно неприятно. !
Настройка порога наблюдения
Если мы обнаруживаем, что элемент навигации не отвечает на сигналы прокрутки, когда мы меняем направление среднего элемента - например, мы частично прокручивали элемент, только чтобы вернуться назад и снова пройти мимо - мы нашли хороший возможность настроить наш массив threshold
опций. Включая дополнительные значения точки останова в наш обработчик useOnScreen
, мы можем настроить чувствительность к коэффициентам пересечения или тому, какая часть навигационной цели находится в поле зрения относительно области просмотра браузера.
Готово!
И это все! У нас есть расширяемая одностраничная система навигации, поддерживаемая React, React Context и React Custom Hooks. Помните: пользовательские хуки - это просто функции, которые составляют стандартные хуки для инкапсуляции логики и предоставляют чистый, минимальный интерфейс для работы с нашими компонентами. Всякий раз, когда вы обнаруживаете, что пишете повторяющиеся последовательности хуков в своих компонентах, посмотрите, сможете ли вы абстрагироваться от деталей и предоставить вам и вашей команде оптимизированный API, такой как useNav
!
ссылка на живой пример того, что мы построили!
Https://shapirodaniel.github.io/single-page-nav/
ссылка на репозиторий GitHub и исходные файлы
Дэниел Шапиро - инженер-программист полного цикла и выпускник Fullstack Academy, где в настоящее время работает преподавателем. Когда он не занимается созданием крутых вещей, вы обычно можете увидеть, как он занимается своей прежней карьерой пекаря в многочисленных ремесленных пекарнях в Чикаго, выпекая партию английских кексов, бегая по пляжу со своей смесью биглей Лили или каякивая по озеру Мичиган. Свяжитесь с Дэниелом по адресу [email protected] или подключитесь к LinkedIn и обязательно посетите Note-ary, пакет для управления проектами с досками в стиле Канбан и общением в реальном времени, поддерживаемым socket.io!