Использование JavaScript для создания динамической таблицы содержимого для длинных статей

Почему динамическая таблица контента имеет значение для контента в формате

Длинные статьи, учебные пособия и страницы документации могут перегружать читателей, если навигация ограничена ручной прокруткой. Динамическая таблица содержимого (TOC) решает эту проблему, предоставляя кликабельный контур, который автоматически выделяет раздел, который просматривает читатель. Это улучшает удобство использования, снижает показатель отказов и делает ваш контент более доступным для пользователей, которые хотят быстро переключаться между темами. В отличие от статического TOC, написанного вручную, TOC, созданный на JavaScript, обновляется при добавлении новых разделов, сохраняет ссылки синхронизированными с идентификаторами заголовков и реагирует на поведение прокрутки в реальном времени.

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

Основные концепции за динамической таблицей содержимого

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

  • Семантическая структура HTML — каждый заголовок раздела должен иметь уникальный атрибут , чтобы JavaScript мог нацеливаться на него.
  • DOM-траверсал и манипуляции — ваш скрипт сканирует заголовки, создает вложенный список ссылок и добавляет этот список к элементу контейнера.
  • Обработка событий скролла — эффективный слушатель проверяет, какой заголовок в настоящее время виден, и добавляет класс к соответствующей ссылке TOC.

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

Шаг 1: Подготовьте свою HTML-структуру

Прежде чем запустить любой JavaScript, вам нужно две вещи в HTML:

Присваивать уникальные идентификаторы заголовкам

Каждый заголовок, который должен появиться в TOC (обычно , или ), должен иметь уникальный . Это важно, потому что ссылки TOC используют идентификаторы фрагментов (например, ) для прокрутки в правильное положение. Вот пример:

<h2 id="introduction">Introduction</h2>
<p>...</p>
<h2 id="setup">Setting Up the Environment</h2>
<p>...</p>
<h3 id="installing-dependencies">Installing Dependencies</h3>
<p>...</p>
<h2 id="implementation">Implementation</h2>
<p>...</p>

Если вы не можете изменить HTML напрямую, вы можете генерировать идентификаторы из текста заголовка с помощью функции JavaScript (например, [FLT: 8]), но более удобно добавлять их вручную или со статическим генератором сайта.

Создать контейнер для TOC

Поместите пустой элемент (обычно или ), где вы хотите, чтобы TOC появился.

<nav id="table-of-contents" aria-label="Table of Contents"></nav>

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

Шаг 2: Создание TOC с помощью JavaScript

Теперь мы пишем JavaScript, который сканирует заголовки и строит список. Следующий фрагмент создает плоский список заголовков . Для более продвинутого TOC, который включает подзаголовки, вам понадобятся вложенные списки, которые мы рассмотрим позже.

Базовый пример TOC

const tocContainer = document.getElementById('table-of-contents');
const headings = document.querySelectorAll('h2');

// Bail early if there's no container or no headings
if (!tocContainer || headings.length === 0) return;

const ul = document.createElement('ul');
ul.setAttribute('role', 'list'); // accessibility enhancement

headings.forEach((heading, index) => {
 // Ensure the heading has an id; if not, generate one
 if (!heading.id) {
 heading.id = 'section-' + index;
 }
 const li = document.createElement('li');
 const a = document.createElement('a');
 a.textContent = heading.textContent;
 a.href = '#' + heading.id;
 a.setAttribute('data-section', heading.id); // useful for active detection
 li.appendChild(a);
 ul.appendChild(li);
});

tocContainer.appendChild(ul);

Основные точки:

  • Метод FLT:15 возвращает статический метод FLT:16; он работает для страниц, где заголовки не меняются динамически.
  • Если в заголовке отсутствует , мы автоматически генерируем один с помощью индекса.
  • Мы добавляем атрибут для каждого ссылки для более легкого выбора позже.

Обработка вложенных головок (H2, H3, H4)

Более полезный ТОС отражает иерархию документа. Для создания вложенных списков отследить текущий и вставить для своих детей. Вот упрощенный подход с использованием стека:

const tocContainer = document.getElementById('table-of-contents');
const headings = document.querySelectorAll('h2, h3, h4');
if (!tocContainer || headings.length === 0) return;

const root = document.createElement('ul');
const stack = [{ element: root, level: 2 }]; // level refers to heading level

headings.forEach((heading) => {
 const level = parseInt(heading.tagName.substring(1), 10); // 'H2' -> 2
 if (!heading.id) heading.id = 'section-' + Math.random().toString(36).substr(2, 9);

 const li = document.createElement('li');
 const a = document.createElement('a');
 a.textContent = heading.textContent;
 a.href = '#' + heading.id;
 li.appendChild(a);

 // Pop stack until we reach the parent level
 while (stack.length > 0 && stack[stack.length - 1].level >= level) {
 stack.pop();
 }
 const parent = stack[stack.length - 1].element;
 parent.appendChild(li);

 // If next heading is lower, we need a nested list
 const nextLevel = headings.item(Array.from(headings).indexOf(heading) + 1);
 if (nextLevel && parseInt(nextLevel.tagName.substring(1), 10) > level) {
 const nestedUl = document.createElement('ul');
 li.appendChild(nestedUl);
 stack.push({ element: nestedUl, level: level });
 }
});

tocContainer.appendChild(root);

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

Шаг 3: Выделение активного раздела на прокрутке

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

Эффективный слушатель свитков

const tocLinks = document.querySelectorAll('#table-of-contents a');
const sections = Array.from(headings).map(h => ({
 id: h.id,
 top: h.offsetTop
}));

function updateActiveLink() {
 const scrollY = window.pageYOffset || document.documentElement.scrollTop;
 let currentId = '';

 // Iterate backwards for better performance
 for (let i = sections.length - 1; i >= 0; i--) {
 if (scrollY >= sections[i].top - 150) {
 currentId = sections[i].id;
 break;
 }
 }

 tocLinks.forEach(link => {
 link.classList.remove('active');
 if (link.getAttribute('href') === '#' + currentId) {
 link.classList.add('active');
 }
 });
}

// Throttle scroll events for performance
let ticking = false;
window.addEventListener('scroll', () => {
 if (!ticking) {
 window.requestAnimationFrame(() => {
 updateActiveLink();
 ticking = false;
 });
 ticking = true;
 }
});

Оптимизация:

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

Шаг 4: Добавление плавного прокрутки и доступности

Плавная прокрутка делает перескакивание между разделами приятным. Это можно сделать с помощью CSS, но также и с помощью JavaScript для более тонкого управления.

// Add click handler on the TOC container to use smooth scrolling
tocContainer.addEventListener('click', (e) => {
 const link = e.target.closest('a');
 if (link && link.getAttribute('href').startsWith('#')) {
 e.preventDefault();
 const targetId = link.getAttribute('href').substring(1);
 const target = document.getElementById(targetId);
 if (target) {
 target.scrollIntoView({ behavior: 'smooth' });
 // Update the URL hash without causing a scroll jump
 history.pushState(null, '', '#' + targetId);
 }
 }
});

Улучшения доступности:

  • В этом случае [[[[[[]]]]][[[[[[[[[[[[]]]]]]]]][[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]][[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]
  • Добавьте к активной ссылке: вместо . Это помогает читателям экрана анонсировать текущий раздел.
  • Используйте и , если по умолчанию / семантика переопределена стилем.

Шаг 5: Укладка динамического TOC

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

#table-of-contents {
 position: sticky;
 top: 2rem;
 max-height: calc(100vh - 4rem);
 overflow-y: auto;
 border-left: 2px solid #ccc;
 padding-left: 1rem;
 font-size: 0.9rem;
}
#table-of-contents ul {
 list-style: none;
 padding: 0;
}
#table-of-contents li {
 margin-bottom: 0.25rem;
}
#table-of-contents a {
 color: #333;
 text-decoration: none;
}
#table-of-contents a.active {
 font-weight: bold;
 color: #007bff;
}
#table-of-contents a[aria-current="location"] {
 border-left: 2px solid #007bff;
 margin-left: -1rem;
 padding-left: calc(1rem - 2px);
}

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

Расширенные усовершенствования

1.Отказ от масштаба событий

Если высота обзорного поля изменяется (например, при изменении ориентации на мобильном устройстве), значения заголовков могут измениться. Пересчитайте массив на дебюнкционированную величину:

let sections = [];
function recalcSections() {
 sections = Array.from(headings).map(h => ({
 id: h.id,
 top: h.offsetTop
 }));
}
let resizeTimer;
window.addEventListener('resize', () => {
 clearTimeout(resizeTimer);
 resizeTimer = setTimeout(recalcSections, 250);
});

2. Intersection Observer для скролл-основывающих выделений

Альтернативой прокрутке слушателей является API . Он более эффективен и удобен в управлении. Пример:

const observer = new IntersectionObserver((entries) => {
 entries.forEach(entry => {
 if (entry.isIntersecting) {
 const id = entry.target.id;
 tocLinks.forEach(link => {
 link.classList.remove('active');
 if (link.getAttribute('href') === '#' + id) {
 link.classList.add('active');
 }
 });
 }
 });
}, { rootMargin: '-80px 0px -70% 0px' });

headings.forEach(h => observer.observe(h));

Это происходит только тогда, когда товарный знак входит или выходит из расчетной зоны, уменьшая накладные расходы. определяет, когда раздел считается «активным».

3.Ленивая погрузка или динамический контент

Если статья загружает разделы динамически (например, через AJAX), вы должны регенерировать TOC после появления нового контента. Один из способов — использовать MutationObserver в контейнере статьи и снова вызывать функцию генерации TOC. Однако будьте осторожны, чтобы не дублировать записи.

Соображения в отношении эффективности

  • Избегайте тяжелых запросов DOM внутри обработчиков прокрутки. Кэшируйте все селекторы один раз при инициализации.
  • Использовать пассивные слушатели событий для прокрутки: . Это улучшает производительность прокрутки, особенно на мобильных устройствах.
  • Не дрожать с — — более эффективен, потому что он синхронизируется с циклом рендеринга браузера.
  • Уменьшите и отложите сценарий , чтобы он не блокировал загрузку страницы. Поместите сценарий прямо перед или используйте атрибут .

Интеграция со статичным генератором сайтов (SSG) или CMS

Если вы используете статический генератор сайтов, вы можете предварительно визуализировать TOC с помощью встроенных функций (например, коллекций Eleventy, Hugo ). Однако для динамического прокрутки по-прежнему требуется JavaScript на стороне клиента. Преимущество TOC на стороне сервера заключается в том, что он доступен сразу, даже до запуска JavaScript, что помогает SEO и доступности.

Для CMS, такой как WordPress или Directus, вы можете использовать тот же подход JavaScript при хранении идентификаторов заголовков в контенте. Directus, например, поддерживает пользовательские интерфейсы, которые автоматически генерируют идентификаторы. Вы можете создать крючок, который работает на сохранении контента, чтобы добавлять идентификаторы в заголовки, а затем полагаться на интерфейс JavaScript для создания TOC.

Внешние ресурсы для более глубокого понимания:

Тестирование и отладка

  1. Убедитесь, что каждая заголовок имеет уникальный . Дублирующие идентификаторы заставляют браузер прокручивать только до первого совпадения.
  2. Проверьте TOC как на светлых, так и на темных темах, чтобы убедиться, что контрастность ссылок соответствует стандартам WCAG AA.
  3. Тест с навигацией по клавиатуре: нажатие Tab должно перемещаться между TOC-ссылками, а Введите следует прокрутить до раздела.
  4. Используйте вкладку производительности DevTools браузера, чтобы не допустить jank во время прокрутки.
  5. Если статья содержит изображения или ифрамы, то после загрузки этих элементов может измениться. Назовите функцию пересчета на или после загрузки всех изображений (например, ).

Потенциальные подводные камни и как их избежать

  • Разорванные ссылки при отсутствии идентификаторов заголовков. Всегда проверяйте и генерируйте один, если он отсутствует (используй утилиту слизи).
  • ТОК мерцание во время прокрутки — вызвано слишком большим количеством переливов. и кэш значения.
  • Перекрывающиеся секции — активная подсветка может переключаться слишком рано или слишком поздно. Настройте в IntersectionObserver или смещении в обработчике прокрутки.
  • Взятые вопросы вмятин TOC — тест с несколькими уровнями (H2 → H3 → H4) и обеспечение правильного отображения списка. Подход, основанный на стеке, выше работает, но может быть расширен для обработки зазоров (например, H2 непосредственно сопровождается H4).
  • Преимущество на длинных страницах — если у вас есть сотни заголовков, рассмотрите возможность ограничения TOC только на H2 и H3 или реализуйте виртуальную прокрутку для боковой панели.

Заключение

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

Внедрите подход, который лучше всего подходит для вашего стека: чистый JavaScript для простых сайтов или гибрид с SSG для начальной структуры TOC плюс выделение на стороне клиента.