anchor якорь —— Навигация
Якорь — это «оглавление длинного документа» на боковой панели документации: «базовое использование / API / замечания» справа на сайте документации; клик прокручивает к соответствующему заголовку, а при прокрутке подсвечивается текущий раздел. Отличие от menu в том, что якорь связан с позицией содержимого на этой же странице, а не с разными страницами.
Когда использовать
Заголовок раздела «Когда использовать»- Реализация
<LhAnchor>на всех концах: оглавление сайта документации, навигация по длинной статье, навигация по разделам API-документации, навигация по блокам лендинга. - Содержимое страницы настолько длинное, что нужен быстрый переход, а у блоков есть чёткие заголовки.
- Не следует использовать: навигация между страницами сайта (используйте
menu/breadcrumb); короткие страницы (в один экран оглавление не нужно); блоки без заголовков (якорь опирается на текст заголовка как на текст ссылки).
import { flattenAnchorItems, collectAnchorIds, resolveActiveAnchor, getAnchorClassNames, getAnchorLinkClassNames,} from '@lh-ui/share/core/anchor'import type { LhAnchorItem, LhFlatAnchorItem } from '@lh-ui/share/core/anchor'Замысел
Заголовок раздела «Замысел»Дерево → плоский список: конфигурация якорей — дерево (разделы с подразделами), а рендерится он одномерным списком (с отступами). flattenAnchorItems выполняет обход в глубину, сохраняя порядок документа — оглавление с нарушенным порядком хуже, чем отсутствие оглавления. level хранит исходную глубину и управляет стилем отступа.
Цепочка предков seen для защиты от циклических ссылок: seen — это множество предков на текущем пути DFS (не глобальное множество). В комментарии прямо сказано «цепочка предков для защиты от циклических ссылок» — если в данных children узла A содержит сам A (грязные данные), глобальный seen лишь уберёт дубликаты, а цепочка предков прервёт рекурсию напрямую и не даст зациклиться. Это защита от структурных ошибок в древовидных данных.
«Последний перешедший порог» в resolveActiveAnchor: среди упорядоченных по документу смещений ищется последний якорь, для которого offset <= currentTop. Ключевая деталь: при первом же offset > currentTop делается break — смещения упорядочены, и последующие будут только больше, дальнейший перебор — трата. Это O(n) с досрочным выходом.
Промах — возврат к первому элементу: при прокрутке выше всех якорей (вершина страницы) возвращается ids[0] — первый элемент всегда подсвечен, что избегает неприятного состояния «нет активного элемента». Для пустого массива возвращается пустая строка (UI-слой не рендерит подсветку).
offsets и ids должны быть согласованы по порядку: сигнатура функции требует два массива — смещения измеряются UI-слоем через getBoundingClientRect() для каждого элемента и передаются в том же порядке. При нарушении порядка получится неверный активный элемент; это контракт на стороне вызывающего, core не может его проверить.
Таблица функций
Заголовок раздела «Таблица функций»| Функция | Сигнатура | Возврат | Назначение |
|---|---|---|---|
flattenAnchorItems |
(items, level?, seen?) => LhFlatAnchorItem[] |
плоский список | обход в глубину с сохранением порядка |
collectAnchorIds |
(items) => string[] |
список id | массив id в порядке уплощения |
resolveActiveAnchor |
(ids, offsets, currentTop) => string |
активный id | определение при прокрутке |
getAnchorClassNames |
(state: { direction }) => string[] |
классы | направление контейнера |
getAnchorLinkClassNames |
(state: { active, level }) => string[] |
классы | активное состояние + отступ уровня |
Определения типов
Заголовок раздела «Определения типов»export interface LhAnchorItem { id: string // id целевого элемента title: string // отображаемый текст children?: LhAnchorItem[]}
export interface LhFlatAnchorItem { id: string title: string level: number // исходная глубина (корень = 0), управляет отступом}Доступность
Заголовок раздела «Доступность»Якорные ссылки указывают на якоря на этой же странице; стандартный подход — нативный <a href="#section">:
<a href="#usage" class="lh-anchor__link">Примеры использования</a>...<section id="usage" tabindex="-1">…</section>- Нативный переход по якорю + tabindex=“-1”: при клике по
<a href="#id">браузер прокручивает страницу и пытается передать фокус целевому элементу. Если цель не может получить фокус, после перехода фокус остаётся на месте — добавьте целиtabindex="-1", чтобы она могла принимать программный фокус, и скринридер начнёт чтение с этого места. Это рекомендованный WAI-ARIA шаблон доступных якорей. - aria-current отмечает текущий раздел:
--activeвgetAnchorLinkClassNames— визуальный класс; для доступности рекомендуется, чтобы UI-слой добавлял активной ссылкеaria-current="location"(семантика позиции на странице, в отличие отaria-current="page"вbreadcrumb). - Переход не должен менять URL hash: изменение hash попадает в историю браузера — после пяти кликов по якорям придётся пять раз нажать «назад», чтобы покинуть страницу. UI-слой может использовать
history.replaceStateилиscrollIntoViewвместо перехода по умолчанию — это компромисс в взаимодействии, core его не касается. - Обёртка
nav+ aria-label: как и вbreadcrumb,<nav aria-label="Содержание страницы">объявляет это зоной навигации.
Примеры использования
Заголовок раздела «Примеры использования»import { flattenAnchorItems, collectAnchorIds, resolveActiveAnchor,} from '@lh-ui/share/core/anchor'
const items = [ { id: 'intro', title: 'Введение', children: [ { id: 'why', title: 'Зачем использовать' }, ] }, { id: 'usage', title: 'Использование' },]
flattenAnchorItems(items)// [ { id: 'intro', title: 'Введение', level: 0 },// { id: 'why', title: 'Зачем использовать', level: 1 },// { id: 'usage', title: 'Использование', level: 0 } ]
collectAnchorIds(items) // ['intro', 'why', 'usage']
// Связь с прокруткой: offsets — позиции якорей относительно верха контейнера (в порядке ids)resolveActiveAnchor(['intro', 'why', 'usage'], [0, 300, 600], 350) // 'why'resolveActiveAnchor(['intro', 'why', 'usage'], [0, 300, 600], 50) // 'intro' (не перешёл why)resolveActiveAnchor(['intro', 'why', 'usage'], [0, 300, 600], 0) // 'intro' (промах — возврат к первому)resolveActiveAnchor([], [], 0) // '' (пустой список)// UI-слой: слежение за прокруткой + измерение смещенийconst activeId = ref('')
function refresh() { const flat = flattenAnchorItems(props.items) const ids = flat.map((item) => item.id) const offsets = ids.map((id) => { const el = document.getElementById(id) return el ? el.getBoundingClientRect().top - containerTop : Infinity }) activeId.value = resolveActiveAnchor(ids, offsets, scrollTop.value)}
onMounted(() => window.addEventListener('scroll', refresh, { passive: true }))<!-- Vue3 --><template> <LhAnchor :items="items" :offset-top="80" @click="onAnchorClick" /></template>// React: useEffect регистрирует слушатель scroll; измерение смещений через массив ref// и getBoundingClientRect; троттлинг через requestAnimationFrame или throttle// (scroll срабатывает с высокой частотой)// uniapp: <scroll-view scroll-into-view="id"> — нативный переход по якорю// в мини-программе; связь с прокруткой — через событие scroll объекта scroll-view,// откуда берётся scrollTopСтили и токены
Заголовок раздела «Стили и токены»| Класс | Семантика |
|---|---|
.lh-anchor |
контейнер |
.lh-anchor--{vertical|horizontal} |
направление (вертикальная боковая панель / горизонтальная верхняя навигация) |
.lh-anchor__link |
якорная ссылка |
.lh-anchor__link--level-{0..n} |
отступ уровня (padding-left увеличивается) |
.lh-anchor__link--active |
активное состояние (основной цвет + индикаторная полоса слева) |
Цвет активного состояния --lh-primary-color; обычная ссылка --lh-text-regular; единица отступа уровня --lh-anchor-indent (например, 12px/уровень).
Замечания
Заголовок раздела «Замечания»- Измерение смещения должно вычитать смещение контейнера: если якори находятся внутри прокручиваемого внутреннего контейнера (не прокрутка window), из
getBoundingClientRect().topнужно вычесть позицию верхней границы контейнера, иначе подсветка будет постоянно смещена. Это самая частая ошибка. getBoundingClientRectпринудительно вызывает layouts: вызов для N элементов при каждом событии scroll вызывает N рефлоу. Оптимизация: кэшировать смещения и пересчитывать только при изменении размеров содержимого; либо наблюдать за каждым элементом через IntersectionObserver.- Компенсация смещения при перекрытии закреплённой шапки: если на странице есть фиксированный заголовок (
affix), после перехода по якорю цель может быть им закрыта. Добавьте целевому элементуscroll-margin-top(CSS) либо вычитайте высоту шапки из прокрутки в UI-слое — это точка совместной работы раскладок. - Смещения устаревают при динамическом содержимом: после изменения высоты содержимого (свёртывание/разворачивание, загрузка картинок) кэшированные смещения становятся некорректными. При изменении содержимого нужно измерять заново.
- Семантика цепочки предков в
seen: третий параметрflattenAnchorItems— это предки на текущем пути, по умолчаниюnew Set(). Бизнес-сторона обычно его не передаёт — его главная задача: защита от зацикливания на грязных данных. Если два сестринских поддерева ссылаются на один и тот же узел (совместное использование в DAG), цепочка предков позволит ему появиться по одному разу в каждом пути (тот же компромисс, что и вtreeс цепочкой предков против глобального seen вtable). - Пустые и повторяющиеся
id:idдолжен быть глобально уникальным (требование якорей HTML).document.getElementByIdвозвращает только первое совпадение — повторяющиеся id приводят к ошибкам перехода, core не может это проверить. - Якорь против оглавления-роутинга: якорь связан с позицией на странице; если каждый пункт «оглавления» — отдельная страница, то это
menu, а неanchor— не смешивайте (якорь не перезагружает страницу, иначе управляется попадание в историю роутинга).
@lh-ui/share/core/anchor → resolveActiveAnchor( )(填好参数后点运行)Copyright © 2024-2026 FengMo WenXue