惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Cisco Talos Blog
Cisco Talos Blog
V
V2EX
C
Check Point Blog
GbyAI
GbyAI
D
Docker
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
B
Blog RSS Feed
H
Hackread – Cybersecurity News, Data Breaches, AI and More
N
Netflix TechBlog - Medium
T
Troy Hunt's Blog
博客园 - Franky
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Microsoft Security Blog
Microsoft Security Blog
P
Privacy & Cybersecurity Law Blog
WordPress大学
WordPress大学
The Cloudflare Blog
S
SegmentFault 最新的问题
Latest news
Latest news
Microsoft Azure Blog
Microsoft Azure Blog
P
Proofpoint News Feed
I
InfoQ
博客园 - 【当耐特】
NISL@THU
NISL@THU
A
About on SuperTechFans
T
Tailwind CSS Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
The Hacker News
The Hacker News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Scott Helme
Scott Helme
雷峰网
雷峰网
C
CXSECURITY Database RSS Feed - CXSecurity.com
Security Latest
Security Latest
V
Vulnerabilities – Threatpost
Security Archives - TechRepublic
Security Archives - TechRepublic
A
Arctic Wolf
Hacker News: Ask HN
Hacker News: Ask HN
N
News and Events Feed by Topic
IT之家
IT之家
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
aimingoo的专栏
aimingoo的专栏
T
Threat Research - Cisco Blogs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
SecWiki News
SecWiki News
大猫的无限游戏
大猫的无限游戏
S
Security Affairs
The Register - Security
The Register - Security
www.infosecurity-magazine.com
www.infosecurity-magazine.com
L
LINUX DO - 热门话题
T
Tor Project blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Navigation API теперь доступен в Baseline
aio350 (Time · 2026-05-20 · via Все публикации подряд на Хабре

Уровень сложностиПростой

Время на прочтение13 мин

Охват и читатели747

Обзор

Navigation API предоставляет возможность инициировать (программно запускать), перехватывать и управлять навигацией в браузере. Он также позволяет исследовать (traverse) сущности истории (history entries) приложения. Это улучшенный вариант предыдущих возможностей веб-платформы, связанных с навигацией, таких как History API и window.location, который решает их проблемы и специально предназначен для одностраничных приложений (single-page applications, SPA).

На сегодняшний день Navigation API поддерживается всеми основными браузерами:

❯ Концепции и использование

В SPA шаблон страницы имеет тенденцию оставаться одинаковым в процессе использования, содержимое страницы динамически модифицируется при посещении пользователем разных страниц или разделов. Как следствие, в браузер загружается только одна страница, что ломает пользовательский опыт по переходу назад и вперед между разными локациями в истории отображения. Эта проблема может быть частично решена с помощью History API, но он не рассчитан на SPA. Navigation API призван закрыть эту брешь.

Navigation API доступен через свойство Window.navigation, которое возвращает ссылку на глобальный объект Navigation. Каждый объект window содержит собственный экземпляр navigation.

❯ Обработка навигации

В интерфейс navigation входит несколько связанных с навигацией событий, самым важным из которых является navigate. Это событие возникает при любом типе навигации, что позволяет управлять всеми навигациями в одном месте. Это идеально подходит для функционала маршрутизации в SPA. Обработчику события navigate передается объект NavigateEvent, который содержит большое количество информации о навигации, которую можно использовать для определения логики обработки конкретного перехода.

Ключевые свойства NavigateEvent:

  • canIntercept — если имеет значение false, перехватить навигацию невозможно. Например, невозможен перехват навигации между разными источниками (cross-origin) (см. пример «Перехват навигации» ниже).

  • destination.url — URL, к которому выполняется переход. Пожалуй, самая важная информация, которую следует учитывать при навигации. hashChange — имеет значение true, если навигация осуществляется в пределах одного документа, и хеш — единственная часть, отличающаяся от текущего URL. В современных SPA хеш должен использоваться для ссылок на разные части текущего документа. Поэтому, если hashChange имеет значение true, вероятно, такую навигацию перехватывать не следует.

  • downloadRequest — если имеет значение true, то навигация была инициирована ссылкой (<a>) с атрибутом download. В большинстве случаев такую навигацию перехватывать не нужно.

  • formData — если значение не равно null, значит, эта навигация является частью отправки формы методом POST. Перехват такой навигации тоже под вопросом.

  • navigationType — один из вариантов: "reload", "push", "replace" или "traverse". В случае "traverse" навигацию нельзя отменить с помощью метода preventDefault().

Учитывая все это, можно реализовать такую вспомогательную функцию:

// Если функция возвращает true, навигацию перехватывать не нужно
function shouldNotIntercept(navigationEvent) {
  return (
    // Нельзя перехватить по умолчанию
    !navigationEvent.canIntercept ||
    // Если изменился только хеш, просто позволяем браузеру
    // выполнить прокрутку до соответствующего контента
    navigationEvent.hashChange ||
    // Если выполняется скачивание файла,
    // не мешаем браузеру выполнить эту операцию
    navigationEvent.downloadRequest ||
    // Если выполняется отправка формы,
    // пропускаем ее на сервер
    navigationEvent.formData
  );
}

Событие navigate также возникает при использовании старых методов навигации, таких location.assign(), pushState() и replaceState(). Сигнатуры старых методов остаются прежними, т.е. они не возвращают промисы в соответствии с новой спецификацией.

Обработчик события navigate централизует обработку изменения URL в SPA. Это сложная задача при использовании старых API. Например, вот как это может выглядеть при использовании History API:

function updatePage(event) {
  event.preventDefault(); // отключаем дефолтную обработку клика по ссылке браузером
  window.history.pushState(null, '', event.target.href);
  // Настраиваем страницу на основе нового URL
}
const links = [...document.querySelectorAll('a[href]')];
links.forEach(link => link.addEventListener('click', updatePage));

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

Кроме того, приведенный выше код не обрабатывает навигацию «Назад/Вперед». Для этого существует другое событие — popstate.

Объект NavigationEvent предоставляет два метода:

  • intercept() — позволяет определять кастомную логику навигации и принимает следующие аргументы:

    • handler — функция обратного вызова, позволяющая определять логику навигации как после фиксации (commit) навигации, так и непосредственно перед этим. Например, здесь можно загружать новый контент в UI на основе нового URL или выполнять перенаправление на страницу авторизации, если URL указывает на защищенную страницу, а пользователь не авторизован.

    • свойства, позволяющие включать/выключать дефолтное поведение браузера по управлению фокусом (focusReset) и прокруткой (scroll) в процессе навигации.

  • scroll() — позволяет вручную инициировать обработку прокрутки браузером (например, к идентификатору фрагмента в URL), когда это имеет смысл, вместо ожидания автоматической обработки прокрутки браузером.

После инициализации навигации и вызова обработчика intercept(), создается экземпляр NavigationTransition (доступный через Navigation.transition), который может использоваться для отслеживания процесса выполняемой навигации.

«transition» в контексте навигации означает переход от одной сущности истории к другой. Не путайте его с переходом CSS.

Когда промис, возвращаемый intercept(), разрешается, возникает событие navigatesuccess объекта Navigation, что позволяет выполнить очистку после завершения успешной навигации. Отклонение промиса означает провал навигации и вызывает событие navigateerror, что позволяет мягко обработать провал. Также имеется свойство finished в значении, возвращаемом методами навигации (такими как navigate()), которое разрешается или отклоняется одновременно с названными событиями, что предоставляет еще одну возможность по обработке результата навигации.

❯ Программное обновление и исследование истории навигации

Каждая новая локация, посещенная пользователем, приводит к созданию сущности (записи) истории навигации (history navigation entry). Каждая сущность истории представлена экземпляром NavigationHistoryEntry. Сущность содержит несколько свойств, таких как ключ (key), URL и информацию о состоянии (state). Текущая сущность доступна через Navigation.currentEntry, а массив всех сущностей можно получить с помощью Navigation.entries. Каждый объект NavigationHistoryEntry имеет событие dispose, которое возникает, когда сущность удаляется из истории браузера. Например, если пользователь перешел назад три раза, а затем перешел вперед в другое место, три сущности истории будут удалены.

Объект Navigation содержит все методы, необходимые для обновления и исследования истории навигации:

  • navigate() — выполняет переход к новому URL, создавая новую сущность истории навигации.

  • reload() — перезагружает текущую сущность истории навигации.

  • back() — выполняет переход к предыдущей сущности истории навигации, если это возможно.

  • forward() — выполняет переход к следующей сущности истории навигации, если это возможно.

  • traverseTo() — выполняет переход к определенной сущности истории навигации по значению ключа из свойства NavigationHistoryEntry.key.

Каждый из этих методов возвращает объект, содержащий два промиса — { committed, finished }. Это позволяет вызывающему ждать:

  • разрешения committed — изменился видимый URL, создана новая NavigationHistoryEntry

  • разрешения finished — все промисы, возвращенные intercept(), разрешились. Это эквивалент разрешения промиса NavigationTransition.finished при возникновении события navigatesuccess

  • отклонения любого промиса — провал навигации

Метод navigate() (и некоторые другие) в качестве аргументов принимает новый URL и опциональный объект со следующими настройками:

  • state — новое состояние сущности истории (см. раздел «Состояние» ниже)

  • history — значение "replace" заменяет текущую сущность истории

  • info — объект, передаваемый событию navigate и доступный через NavigateEvent.info. Может быть полезен, например, для определения анимации отображения следующей страницы. Важно отметить, что info не будет воспроизведен, если пользователь совершит навигацию, например, с помощью кнопок «Назад» и «Вперед». В этом случае info будет иметь значение undefined

❯ Состояние

Navigation API позволяет сохранять состояние в каждой сущности истории. Никаких требований к состоянию не предъявляется, оно может содержать любую информацию. Например, оно может содержать свойство visitCount с количеством посещений страницы или объект со свойствами UI для его восстановления при возврате пользователя на страницу.

Для получения состояния NavigationHistoryEntry используется метод getState(). По умолчанию состояние имеет значение undefined.

Установка состояния немного сложнее. getState() возвращает копию состояния, ее модификация не влияет на исходное состояние. Для обновления состояния сущности в метод navigate() или reload() передается опциональный объект со свойством state, содержащим новое состояние сущности истории. Состояние сущности обновляется после фиксации навигации.

Однако в некоторых случаях изменение состояния не зависит от навигации или перезагрузки страницы, например, когда страница содержит раскрываемый элемент <details>. В этом случае состояние раскрытости можно хранить в сущности истории для последующего восстановления при возврате пользователя или перезагрузки страницы. Для таких кейсов предназначен метод Navigation.updateCurrentEntry(). После изменения текущей сущности возникает событие currententrychange (см. пример «Обновление состояния сущности истории» ниже).

❯ Ограничения

Сейчас Navigation API имеет несколько заметных ограничений:

  1. Событие navigate не возникает при первой загрузке страницы. Это подходит для сайтов, использующих серверный рендеринг (server side rendering, SSR) — сервер возвращает правильное начальное состояние, что является самым быстрым способом доставки контента пользователям. Но сайтам, использующим рендеринг на стороне клиента (client side rendering, CSR), может потребоваться дополнительная функция для инициализации страницы.

  2. Navigation API работает только в одном фрейме — странице верхнего уровня или определенном <iframe>. На практике это уменьшает путаницу. В History API сущности истории страниц и <iframe> содержатся в одном стеке.

  3. Navigation API не позволяет программно модифицировать или менять порядок элементов в списке сущностей истории (обсуждение на GitHub). Однако в некоторых случаях может быть полезным иметь временное состояние. Например, выполняется навигация к временному модальному окну, запрашивающему определенную информацию о пользователе. Затем выполняется навигация к предыдущему URL. В этом случае требуется программно удалить сущность истории модалки, чтобы нажатие кнопки «Вперед» не приводило к ее повторному открытию.

❯ Примеры использования

Перехват навигации

Вызов intercept({ handler }) в обработчике события navigate сообщает браузеру, что страница подготавливается к новому, обновленному состоянию, и что навигация может занять некоторое время.

Браузер начинает с захвата положения прокрутки для текущего состояния, чтобы его можно было при необходимости восстановить позже, а затем вызывает handler(). Если handler() возвращает промис (что происходит автоматически в случае асинхронных функций), этот промис сообщает браузеру, сколько времени заняла навигация и была ли она успешной:

navigation.addEventListener("navigate", (event) => {
  // См. выше
  if (shouldNotIntercept(event)) return

  const url = new URL(event.destination.url);

  if (url.pathname.startsWith("/articles/")) {
    event.intercept({
      async handler() {
        // Получаем новый контент
        const articleContent = await getArticleContent(url.pathname);
        // И рендерим его
        renderArticlePage(articleContent);
      },
    });
  }
});

Существуют случаи, когда вызывать методы intercept() и preventDefault() нельзя. Нельзя перехватывать (intercept()) навигацию из другого источника (origin). Нельзя отменять (preventDefault()) навигацию, запущенную пользователем путем нажатия кнопок «Назад» или «Вперед» в браузере (обсуждение на GitHub).

Фиксация навигации

При перехвате навигации URL обновляется непосредственно перед вызовом handler(). Если обновление DOM занимает какое-то время, возникает период, когда старый контент отображается вместе с новым URL. Это влияет на такие вещи, как разрешение относительных URL при получении данных или загрузке новых подресурсов.

На GitHub обсуждается задержка обновления URL, но обычно рекомендуется немедленно обновить страницу, добавив какой-либо заполнитель для нового контента:

navigation.addEventListener('navigate', navigateEvent => {
  if (shouldNotIntercept(navigateEvent)) return;
  const url = new URL(navigateEvent.destination.url);

  if (url.pathname.startsWith('/articles/')) {
    navigateEvent.intercept({
      async handler() {
        // URL уже изменился, быстро показываем заглушку
        renderArticlePagePlaceholder();
        // Затем получаем реальные данные
        const articleContent = await getArticleContent(url.pathname);
        renderArticlePage(articleContent);
      },
    });
  }
});

Это не только позволяет избежать проблем с разрешением URL, но и создает ощущение скорости, поскольку пользователь мгновенно получает ответ.

Отмена навигации

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

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

  • пользователь нажимает кнопку «Стоп» в браузере

Для обработки таких кейсов NavigateEvent предоставляет свойство signal, которое представляет собой AbortSignal.

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

navigation.addEventListener('navigate', navigateEvent => {
  if (shouldNotIntercept(navigateEvent)) return;

  const url = new URL(navigateEvent.destination.url);

  if (url.pathname.startsWith('/articles/')) {
    navigateEvent.intercept({
      async handler() {
        renderArticlePagePlaceholder();
        const articleContentURL = new URL(
          '/get-article-content',
          location.href
        );
        articleContentURL.searchParams.set('path', url.pathname);
        const response = await fetch(articleContentURL, {
          // Запрос будет отменен при отмене навигации
          signal: navigateEvent.signal,
        });
        const articleContent = await response.json();
        renderArticlePage(articleContent);
      },
    });
  }
});

Отправка формы

Событие navigate возникает также при отправке формы методом POST. Данные формы доступны через свойство NavigateEvent.formData:

navigation.addEventListener('navigate', (event) => {
  // Нас интересует только отправка формы
  if (event.formData) {
    event.intercept({
      async handler() {
        const data = event.formData;
        console.log(`Отправка данных формы...`);

        const username = data.get('username');

        // Отправляет данные на сервер
        postFormData(data);

        // Обновляем UI без перезагрузки страницы
        renderSuccessMessage(username);
      }
    });
  }
});

// Стандартная форма HTML (JS-обработчик 'onsubmit' не требуется)
// <form action="/login" method="POST">
//   <input name="username" type="text" required />
//   <button type="submit">Login</button>
// </form>

Установка фокуса

Как только промис, возвращаемый handler(), разрешается, браузер фокусируется на первом элементе с атрибутом autofocus или на элементе <body> при отсутствии таких элементов.

Отключить это поведение можно следующим образом:

navigateEvent.intercept({
  focusReset: 'manual',
  async handler() {
    // ...
  },
});

Обработка прокрутки

Метод NavigationEvent.scroll() позволяет управлять восстановлением прокрутки браузером.

По умолчанию браузер пытается восстановить положение прокрутки при вызове intercept(). Но что если содержимое нашей страницы в этот момент еще не готово (например, мы еще не получили ответ от нашего API)? Если браузер выполнит прокрутку перед рендерингом контента, положение прокрутки будет установлено неправильно или останется наверху страницы.

Предположим, что пользователь нажал кнопку «Назад» для возврата к длинному списку элементов. Нам нужно получить и отрендерить эти элементы перед выполнением прокрутки:

navigation.addEventListener('navigate', (event) => {
  if (!event.canIntercept) return;

  event.intercept({
    // Отключаем дефолтную обработку прокрутки браузером
    scroll: 'manual',

    async handler() {
      // Получаем данные и рендерим их
      const data = await fetchListData();
      renderItems(data);

      // Теперь, когда элементы добавлены в DOM, и страница имеет правильную высоту,
      // можно безопасно выполнить прокрутку к сохраненному положению (для навигации "Назад/Вперед")
      // или в начало страницы (для новой навигации)
      event.scroll();
    }
  });
});

Переход к определенной сущности истории

Как отмечалось выше, navigation.currentEntry предоставляет доступ к текущей сущности истории. Это объект, описывающий текущую локацию пользователя. Эта сущность включает текущий URL, метаданные, которые можно использовать для идентификации этой сущности, а также состояние, предоставленное разработчиком.

Даже сайты, которые явно не используют Navigation API, будут иметь currentEntry, и эта сущность будет обновляться или заменяться, даже при использовании старых методов History API.

Метаданные включают key — уникальное строковое значение каждой сущности. Этот ключ остается неизменным, даже если URL или состояние текущей сущности меняются. Он будет находиться в том же слоте (slot). И наоборот, если пользователь нажимает кнопку «Назад», а затем снова открывает ту же страницу, key изменится, поскольку новая сущность истории создаст новый слот.

Для разработчика key может быть полезен для перенаправления пользователя к определенной сущности истории навигации:

// При запуске приложения получаем ключ первой загруженной страницы,
// чтобы пользователь всегда мог к ней вернуться
const { key } = navigation.currentEntry;
backToHomeButton.onclick = () => navigation.traverseTo(key);

// Несмотря на переход к другой странице, backToHomeButton будет работать
await navigation.navigate('/another_url').finished;

Обновление состояния сущности истории

Как уже упоминалось, модификация копии состояния сущности истории не влияет на ее исходное состояние, поэтому:

const state = navigation.currentEntry.getState();
console.log(state.count); // 1
state.count++;
console.log(state.count); // 2
// Но
console.info(navigation.currentEntry.getState().count); // по-прежнему равняется 1

Правильный способ обновления состояния сущности истории:

navigation.navigate(url, { state: newState });
// Или
navigation.reload({ state: newState });

Где newState — любой клонируемый объект.

Для обновления состояния текущей сущности истории лучше всего выполнить навигацию, заменяющую текущую сущность:

navigation.navigate(location.href, { state: newState, history: 'replace' });

Обработчик события navigate “видит” это изменение через NavigateEvent.destination:

navigation.addEventListener('navigate', navigateEvent => {
  console.log(navigateEvent.destination.getState());
});

Как правило, лучше обновлять состояние асинхронно с помощью navigation.reload({ state: newState }), тогда обработчик события navigate сможет использовать это состояние. Однако иногда изменение состояния сущности истории не зависит от навигации, например, когда пользователь переключает состояние элемента <details> или изменяет состояние поля ввода формы. В таких случаях может потребоваться обновление состояния таким образом, чтобы эти изменения сохранялись при перезагрузке и обходе (traverse) страницы. Это возможно с помощью метода updateCurrentEntry():

navigation.updateCurrentEntry({ state: newState });

Вот как можно узнать об этих изменениях:

navigation.addEventListener('currententrychange', () => {
  console.log(navigation.currentEntry.getState());
});

Поскольку состояние может быть структурированным объектом, возникает соблазн использовать его для хранения всего состояния приложения. Однако во многих случаях лучше хранить это состояние в URL.

Переходы отображения

Navigation API и View Transitions API могут использоваться совместно для создания плавных переходов в SPA.

При перехвате события навигации обновления DOM можно «завернуть» в document.startViewTransition(). Это укажет браузеру захватить старое состояние, выполнить необходимые обновления DOM и анимировать их к новому состоянию:

navigation.addEventListener('navigate', (event) => {
  if (shouldNotIntercept(event)) return;

  const url = new URL(event.destination.url);

  event.intercept({
    async handler() {
      // Сначала получаем содержимое новой страницы (опционально, но рекомендуется)
      const content = await fetchNewPageContent(url.pathname);

      // Запускаем переход отображения
      document.startViewTransition(() => {
        // Обновляем DOM в коллбэке.
        // Браузер делает снимки (snapshots) старого UI
        // перед обновлением DOM и нового UI после обновления DOM
        document.getElementById('app').innerHTML = content;
      });
    }
  });
});

Источники:


Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале