Перейти к содержанию

Локальная интерактивность и острова

Статья уже строится заранее из Markdown. Добавим небольшой совет, который можно раскрыть кнопкой, не превращая всю библиотеку в клиентское приложение. Для этого создадим custom element reading-hint и подключим обработанный script внутри Astro-компонента. Это пример локальной интерактивности; директивы гидратации React или другого UI-фреймворка здесь не используются.

Исходный HTML содержит текст совета, а кнопка скрыта. Без JavaScript совет остаётся видимым. После регистрации компонента он переводит текст в закрытое состояние и показывает кнопку. Таким образом основное содержание не исчезает в ситуации, когда обработчик вообще не загрузился. В отличие от обязательной формы, совет является дополнительным способом пояснить структуру чтения.

Остров как граница интерактивности

connectedCallback вызывается, когда элемент подключён к документу. Он не гарантирует, что это произойдёт только один раз за всю жизнь объекта. Поэтому признак data-ready защищает от повторной регистрации обработчика. В нашем проекте нет клиентского роутера, который постоянно переносит элементы, однако сам компонент выражает устойчивый договор и для повторного подключения.

Сценарий ищет кнопку и абзац внутри this, а не во всём документе. Если на странице окажутся два совета, каждый управляет собственным текстом. Глобальный querySelector мог бы случайно открыть первый блок независимо от нажатой кнопки. Локальная область состояния — важная часть самостоятельного компонента даже в маленьком примере.

Кнопка обновляет aria-expanded вместе с hidden у текста. Значение должно описывать фактическое состояние, а не просто последнее событие. Мы не используем цвет как единственный признак открытия. Если компонент получит сложное содержимое, связь через уникальный ID и aria-controls может сделать договор ещё явнее; здесь локальная структура остаётся небольшой.

Перед определением custom element выполняется customElements.get. Повторный импорт обработанного script не должен привести к попытке зарегистрировать то же имя заново. Astro обрабатывает обычный script компонента, но не следует автоматически предполагать, что любая конструкция с is:inline будет иметь ту же дедупликацию и сборку. Изменение способа подключения — отдельное решение.

Называем элемент островом по границе поведения, а не выдаём его за пример client:visible. Client-директивы применяются к поддерживаемым UI-framework компонентам и управляют их гидратацией. Custom element уже является собственным браузерным механизмом. Для маленького раскрывающегося совета добавление React только ради одной кнопки усложнило бы набор зависимостей без необходимого результата.

Основная статья, её заголовки и canonical остаются статическими. Сценарий не загружает их повторно и не восстанавливает всю страницу после навигации. Посетитель может читать материал, пока дополнительное поведение ещё не готово. Такое разделение полезно при расширении библиотеки: один виджет не должен становиться условием доступности всего урока.

Ожидаемый эффект — независимая кнопка, которая раскрывает и закрывает совет. Браузерное исполнение не проводилось. Для будущего опыта добавьте два экземпляра на одну страницу и рассмотрите, меняется ли только нужный блок. Затем отключите JavaScript: оба текста должны оставаться читаемыми, а не превратиться в недоступные закрытые области.

Полный файл src/components/ReadingHint.astro текущего этапа:

<reading-hint><button type="button" hidden>Показать совет</button><p>Читайте урок по порядку: понятие, пример, разбор результата.</p></reading-hint>
<script>
  class ReadingHint extends HTMLElement {
    connectedCallback() {
      if (this.dataset.ready) return;
      this.dataset.ready = 'true';
      const button = this.querySelector<HTMLButtonElement>('button')!;
      const text = this.querySelector<HTMLParagraphElement>('p')!;
      text.hidden = true;
      button.hidden = false;
      button.setAttribute('aria-expanded', 'false');
      button.addEventListener('click', () => {
        text.hidden = !text.hidden;
        button.setAttribute('aria-expanded', String(!text.hidden));
      });
    }
  }
  if (!customElements.get('reading-hint')) customElements.define('reading-hint', ReadingHint);
</script>

Разбор состояния библиотеки

У custom element нет собственного Shadow DOM в этом примере. Его кнопка и текст находятся в обычной разметке и используют общие стили. Это упрощает чтение исходника, но означает, что глобальные правила могут влиять на виджет. Если позже понадобится изоляция, Shadow DOM добавит другую модель стилизации и связи элементов. Не считайте её обязательной частью каждого небольшого острова.

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

Состояние hidden хранится у конкретного абзаца. Дополнительная переменная open могла бы разойтись с DOM после внешнего изменения. Здесь обработчик читает и меняет фактическое значение, затем отражает его в aria-expanded. Для сложного приложения может быть полезна управляемая модель, но она требует одного определённого источника данных и синхронизации. Маленький виджет позволяет обойтись простым локальным договором.

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

Повторное подключение и уничтожение компонента имеют отдельные правила. В этом примере обработчик прикреплён к кнопке внутри самого объекта, а внешних подписок нет. Если добавить listener на window или таймер, потребуется очистка при отключении, иначе старый компонент может продолжить работу. Не переносите общий жизненный цикл из другого фреймворка на custom element без сверки его событий.

Наконец, размер острова определяется не только количеством HTML. Если одна кнопка импортирует большую клиентскую библиотеку, её ресурсы могут превысить расходы всей статической статьи. Наш виджет использует собственный короткий script и без внешних зависимостей объясняет механизм. Фактический размер собранного файла не измерен, поэтому мы говорим о выбранной границе поведения, а не гарантированном числе килобайт.

Подключённый script предназначен для браузера, в отличие от frontmatter компонента. Это конкретное место, где код доставляется посетителю и регистрирует custom element. Такое разделение важно при чтении Astro-файла: одинаковый TypeScript-синтаксис не означает одинаковую среду выполнения. Не помещайте доступ к window в верхнюю подготовительную часть страницы, если она выполняется при статической генерации. Среда должна соответствовать используемому API.

Условия для самостоятельного наблюдения

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

Условие Ожидаемое состояние и его граница
Один экземпляр Кнопка меняет только текст внутри собственного custom element.
Два экземпляра Локальный поиск не должен связать оба виджета с первым найденным абзацем.
Повторное подключение Data-ready предотвращает дублирование обработчика у того же объекта.
JavaScript отсутствует Текст совета остаётся видимым и не требует раскрытия.
Имя уже зарегистрировано Проверка customElements.get предотвращает повторное define.
Нужна framework гидратация Client-директива относится к другому договору, здесь её работа не демонстрируется.

Оглавление серии, курсы ProfessorWeb и полный архив исходников. Для этого урока выберите lesson-09.

Первичные источники: Scripts.

Редакционный статус: локальный черновик. Установка, учебные программы, браузерные сценарии, тесты и публикация не выполнялись. Описанные эффекты ожидаемые.