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

Навигация⚓︎

Многие современные веб-приложения строятся как «одностраничные приложения» (SPA — Single Page Applications). В таких приложениях каждая страница, отображаемая приложением, больше не требует полной перезагрузки страницы в браузере, что позволяет избежать лишних затрат на повторную загрузку JavaScript и CSS при каждом запросе.

Альтернатива одностраничному приложению — это многостраничное приложение. В таких приложениях при каждом клике по ссылке браузер запрашивает и полностью отрисовывает новую HTML-страницу.

Хотя большинство PHP-приложений традиционно были многостраничными, Livewire позволяет получить опыт одностраничного приложения с помощью простого атрибута, который можно добавить к ссылкам в вашем приложении: wire:navigate.

Базовое использование⚓︎

Рассмотрим пример использования wire:navigate. Ниже приведён типичный файл маршрутов Laravel (routes/web.php) с тремя Livewire-компонентами, определёнными как маршруты:

<?php

use App\Livewire\Dashboard;
use App\Livewire\ShowPosts;
use App\Livewire\ShowUsers;

Route::livewire('/', 'pages::dashboard');

Route::livewire('/posts', 'pages::show-posts');

Route::livewire('/users', 'pages::show-users');

Добавив wire:navigate к каждой ссылке в меню навигации на каждой странице, Livewire предотвратит стандартную обработку клика по ссылке и заменит её на свою, более быструю версию:

<nav>
    <a href="/" wire:navigate>Дашборд</a>
    <a href="/posts" wire:navigate>Сообщения</a>
    <a href="/users" wire:navigate>Пользователи</a>
</nav>

Ниже приведён разбор того, что происходит при клике по ссылке с wire:navigate:

  • Пользователь кликает по ссылке
  • Livewire предотвращает переход браузера на новую страницу
  • Вместо этого Livewire запрашивает страницу в фоновом режиме и показывает индикатор загрузки в верхней части страницы
  • Когда HTML новой страницы получен, Livewire заменяет текущий URL страницы, тег <title> и содержимое <body> на соответствующие элементы из новой страницы

Эта техника обеспечивает гораздо более быструю загрузку страниц — часто в два раза быстрее — и заставляет приложение ощущаться как одностраничное приложение на JavaScript.

Перенаправления⚓︎

Когда один из ваших Livewire-компонентов перенаправляет пользователя на другой URL внутри приложения, вы также можете указать Livewire использовать функциональность wire:navigate для загрузки новой страницы. Для этого передайте аргумент navigate в метод redirect():

<?php

return $this->redirect('/posts', navigate: true);

Теперь вместо полного запроса страницы для перенаправления пользователя на новый URL, Livewire заменит содержимое и URL текущей страницы на новые.

Предварительная загрузка ссылок⚓︎

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

  • Пользователь нажимает кнопку мыши
  • Livewire начинает запрашивать страницу
  • Пользователь отпускает кнопку мыши, завершая клик
  • Livewire завершает запрос и выполняет переход на новую страницу

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

Если вы хотите ещё более агрессивный подход к предварительной загрузке, можно использовать модификатор .hover на ссылке:

<a href="/posts" wire:navigate.hover>Сообщения</a>

Модификатор .hover указывает Livewire выполнять предварительную загрузку страницы после того, как пользователь навёл курсор на ссылку на протяжении 60 миллисекунд.

Предварительная загрузка при наведении увеличивает нагрузку на сервер

Поскольку не все пользователи, наведшие курсор на ссылку, обязательно по ней кликнут, использование .hover приведёт к запросу страниц, которые могут оказаться ненужными. Впрочем, Livewire старается снизить эту нагрузку, ожидая 60 миллисекунд перед началом предварительной загрузки.

Сохранение элементов между переходами по страницам⚓︎

Иногда в пользовательском интерфейсе есть элементы, которые нужно сохранить между загрузками страниц — например, аудио- или видеоплееры. В приложении для подкастов пользователь может захотеть продолжать слушать эпизод, переходя по другим страницам.

В Livewire для этого используется директива @persist.

Обернув элемент в @persist и указав ему имя, при запросе новой страницы через wire:navigate Livewire будет искать на новой странице элемент с таким же @persist. Вместо замены элемента, как обычно, Livewire оставит существующий DOM-элемент с предыдущей страницы, сохранив всё его состояние.

Вот пример элемента <audio>-плеера, который сохраняется между страницами с помощью @persist:

@persist('player')
    <audio src="{{ $episode->file }}" controls></audio>
@endpersist

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

Обратите внимание: сохраняемый элемент должен находиться вне ваших Livewire-компонентов. Распространённая практика — размещать такой элемент в главном шаблоне (layout), например, в файле resources/views/layouts/app.blade.php.

resources/views/layouts/app.blade.php
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">

        <title>{{ $title ?? config('app.name') }}</title>

        @vite(['resources/css/app.css', 'resources/js/app.js'])

        @livewireStyles
    </head>
    <body>
        <main>
            {{ $slot }}
        </main>

        @persist('player')
            <audio src="{{ $episode->file }}" controls></audio>
        @endpersist

        @livewireScripts
    </body>
</html>

Подсветка активных ссылок⚓︎

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

<nav>
    <a href="/" class="@if (request->is('/')) font-bold text-zinc-800 @endif">Дашборд</a>
    <a href="/posts" class="@if (request->is('/posts')) font-bold text-zinc-800 @endif">Сообщения</a>
    <a href="/users" class="@if (request->is('/users')) font-bold text-zinc-800 @endif">Пользователи</a>
</nav>

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

Использование атрибута data-current⚓︎

Livewire автоматически добавляет атрибут data-current ко всем ссылкам с wire:navigate, которые соответствуют текущей странице. Это позволяет стилизовать активные ссылки с помощью CSS или Tailwind без каких-либо дополнительных директив:

<nav>
    <a href="/dashboard" wire:navigate class="data-current:font-bold data-current:text-zinc-800">Дашборд</a>
    <a href="/posts" wire:navigate class="data-current:font-bold data-current:text-zinc-800">Сообщения</a>
    <a href="/users" wire:navigate class="data-current:font-bold data-current:text-zinc-800">Пользователи</a>
</nav>

Когда посещается страница /posts, ссылка «Posts» автоматически получит атрибут data-current и будет стилизована соответствующим образом.

Вы также можете использовать обычный CSS для стилизации активных ссылок:

[data-current] {
    font-weight: bold;
    color: #18181b;
}

Если вы хотите отключить это поведение, продолжая использовать wire:navigate, можно добавить директиву wire:current.ignore:

<a href="/posts" wire:navigate wire:current.ignore>Сообщения</a>

Использование директивы wire:current⚓︎

Альтернативно, вы можете использовать директиву wire:current от Livewire, чтобы добавлять CSS-классы к текущей активной ссылке:

<nav>
    <a href="/dashboard" ... wire:current="font-bold text-zinc-800">Дашборд</a>
    <a href="/posts" ... wire:current="font-bold text-zinc-800">Сообщения</a>
    <a href="/users" ... wire:current="font-bold text-zinc-800">Пользователи</a>
</nav>

Теперь, когда посещается страница /posts, ссылка «Сообщения» будет иметь более сильное выделение шрифтом по сравнению с другими ссылками.

Предпочитайте data-current для простоты

Хотя оба подхода работают хорошо, использование атрибута data-current часто оказывается проще и гибче, поскольку не требует дополнительной директивы и отлично сочетается с вариантами data-атрибутов в Tailwind.

Подробнее в документации wire:current.

Сохранение позиции прокрутки⚓︎

По умолчанию Livewire сохраняет позицию прокрутки страницы при навигации вперёд и назад между страницами. Однако иногда требуется сохранить позицию прокрутки отдельного элемента, который сохраняется между загрузками страниц.

Для этого нужно добавить wire:navigate:scroll к элементу, содержащему полосу прокрутки, следующим образом:

@persist('sidebar')
<div class="overflow-y-scroll" wire:navigate:scroll>
    <!-- ... -->
</div>
@endpersist

JavaScript-хуки⚓︎

Каждая навигация по страницам запускает три хука жизненного цикла:

  • livewire:navigate
  • livewire:navigating
  • livewire:navigated

Важно понимать, что эти события отправляются при навигациях любого типа. Это включает:

  • ручную навигацию через Livewire.navigate(),
  • перенаправления с включённой навигацией,
  • нажатия кнопок «Назад» и «Вперёд» в браузере.

Вот пример регистрации слушателей для каждого из этих событий:

document.addEventListener('livewire:navigate', (event) => {
    // Срабатывает, когда навигация инициирована.

    // Навигацию можно отменить (запретить её выполнение):
    event.preventDefault()

    // Содержит полезный контекст о причине навигации:
    let context = event.detail

    // Объект URL — адрес, куда планируется переход...
    context.url

    // Булево значение [true/false] — была ли навигация вызвана
    // нажатием кнопок «Назад» / «Вперёд» (history state)...
    context.history

    // Булево значение [true/false] — есть ли уже закэшированная
    // версия этой страницы, которую можно использовать вместо
    // нового сетевого запроса...
    context.cached
})

document.addEventListener('livewire:navigating', (e) => {
    // Срабатывает непосредственно перед тем, как новый HTML будет
    // подставлен на страницу...

    // Отличное место для изменения любого HTML до того,
    // как страница будет покинута...

    // Можно зарегистрировать callback onSwap, который выполнится
    // сразу после подстановки нового HTML, но до загрузки скриптов.
    // Удобно для применения критически важных стилей (например, dark mode),
    // чтобы избежать мерцания...
    e.detail.onSwap(() => {
        // ...
    })
})

document.addEventListener('livewire:navigated', () => {
    // Срабатывает на финальном этапе любой навигации по странице...

    // Также срабатывает при загрузке страницы вместо события "DOMContentLoaded"...
})

Слушатели событий сохраняются между страницами

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

Простой способ удалить слушатель события после его однократного выполнения — передать опцию {once: true} третьим параметром в метод addEventListener.

document.addEventListener('livewire:navigated', () => {
    // ...
}, { once: true })

Ручной переход на новую страницу⚓︎

Помимо атрибута wire:navigate, вы можете вручную вызвать метод Livewire.navigate() из JavaScript, чтобы инициировать переход на новую страницу:

<script>
    // ...

    Livewire.navigate('/new/url')
</script>

Использование с аналитическим ПО⚓︎

При навигации по страницам с помощью wire:navigate в вашем приложении любые теги <script> в секции <head> выполняются только при первоначальной загрузке страницы.

Это создаёт проблему для аналитического ПО, такого как Fathom Analytics. Такие инструменты рассчитывают на то, что фрагмент <script> будет выполняться при каждом изменении страницы, а не только при первой загрузке.

Инструменты вроде Google Analytics достаточно умны, чтобы автоматически справляться с этой ситуацией, однако при использовании Fathom Analytics вам необходимо добавить атрибут data-spa="auto" к вашему тегу скрипта, чтобы каждая смена страницы отслеживалась корректно:

<head>
    <!-- ... -->

    <!-- Fathom Analytics -->
    @if (! config('app.debug'))
        <script src="https://cdn.usefathom.com/script.js" data-site="ABCDEFG" data-spa="auto" defer></script>
    @endif
</head>

Выполнение скриптов⚓︎

При переходе на новую страницу с помощью wire:navigate создаётся ощущение, что браузер действительно сменил страницу; однако с точки зрения браузера вы технически всё ещё находитесь на исходной странице.

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

Вот несколько важных нюансов и сценариев, о которых стоит знать при использовании wire:navigate.

Не полагайтесь на DOMContentLoaded⚓︎

Обычная практика — помещать JavaScript внутрь слушателя события DOMContentLoaded, чтобы код выполнялся только после полной загрузки страницы.

При использовании wire:navigate событие DOMContentLoaded срабатывает только при первом посещении страницы, но не при последующих переходах.

Чтобы код выполнялся при каждом посещении страницы, замените все случаи использования DOMContentLoaded на livewire:navigated:

document.addEventListener('livewire:navigated', () => {
    // ...
})

Теперь любой код, помещённый внутрь этого слушателя, будет выполняться как при первоначальном посещении страницы, так и после того, как Livewire завершит переход на последующие страницы.

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

Скрипты в <head> загружаются только один раз⚓︎

Если две страницы включают один и тот же тег <script> в секции <head>, этот скрипт будет выполнен только при первоначальном посещении страницы и не будет выполняться при последующих переходах.

<!-- Страница 1 -->
<head>
    <script src="/app.js"></script>
</head>

<!-- Страница 2 -->
<head>
    <script src="/app.js"></script>
</head>

Новые скрипты в <head> выполняются⚓︎

Если последующая страница включает новый тег <script> в секции <head>, которого не было в <head> при первоначальном посещении страницы, Livewire выполнит этот новый тег <script>.

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

<!-- Страница 1 -->
<head>
    <script src="/app.js"></script>
</head>

<!-- Страница 2 -->
<head>
    <script src="/app.js"></script>
    <script src="/third-party.js"></script>
</head>

Ресурсы в <head> блокируют навигацию

Если при переходе на новую страницу в теге <head> присутствует ресурс, например <script src="...">, этот ресурс будет загружен и обработан до завершения навигации и подстановки новой страницы. Это поведение может показаться неожиданным, но оно гарантирует, что любые скрипты, зависящие от этих ресурсов, получат к ним немедленный доступ.

Перезагрузка при изменении ресурсов⚓︎

Обычная практика — включать хэш версии в имя основного JavaScript-файла приложения. Это гарантирует, что после развёртывания новой версии приложения пользователи получат свежий JavaScript-ресурс, а не старую версию из кэша браузера.

Однако теперь, когда вы используете wire:navigate и каждое посещение страницы больше не является полной загрузкой страницы браузером, пользователи могут продолжать получать устаревший JavaScript после деплоя.

Чтобы этого избежать, вы можете добавить атрибут data-navigate-track к тегу <script> в <head>:

<!-- Страница 1 -->
<head>
    <script src="/app.js?id=123" data-navigate-track></script>
</head>

<!-- Страница 2 -->
<head>
    <script src="/app.js?id=456" data-navigate-track></script>
</head>

Когда пользователь переходит на вторую страницу, Livewire обнаружит новый JavaScript-ресурс и инициирует полную перезагрузку страницы в браузере.

Если вы используете плагин Vite в Laravel для сборки и раздачи ресурсов, Livewire автоматически добавляет атрибут data-navigate-track к отрендеренным HTML-тегам ресурсов. Вы можете продолжать ссылаться на свои ресурсы и скрипты как обычно:

<head>
    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>

Livewire автоматически добавляет атрибут data-navigate-track к отрендеренным HTML-тегам.

Отслеживаются только изменения в query string

Livewire перезагрузит страницу только в случае изменения query string (?id="456") у элемента с атрибутом [data-navigate-track], но не при изменении самого пути URI (/app.js).

Скрипты в <body> выполняются заново⚓︎

Поскольку Livewire полностью заменяет содержимое <body> при каждом переходе на новую страницу, все теги <script> на новой странице будут выполнены заново:

<!-- Страница 1 -->
<body>
    <script>
        console.log('Выполняется на первой странице')
    </script>
</body>

<!-- Страница 2 -->
<body>
    <script>
        console.log('Выполняется на второй странице')
    </script>
</body>

Если у вас есть тег <script> в <body>, который вы хотите выполнить только один раз, вы можете добавить к нему атрибут data-navigate-once. В этом случае Livewire выполнит этот скрипт только при первоначальном посещении страницы:

<script data-navigate-once>
    console.log('Выполняется только на первой странице')
</script>

Настройка индикатора прогресса⚓︎

Когда загрузка страницы занимает больше 150 мс, Livewire отображает индикатор прогресса в верхней части страницы.

Вы можете изменить цвет этой полосы или полностью отключить её в конфигурационном файле Livewire (config/livewire.php):

'navigate' => [
    'show_progress_bar' => false,
    'progress_bar_color' => '#2299dd',
],

Смотрите также⚓︎

  • Страницы — Создание маршрутизируемых компонентов-страниц
  • Перенаправления — Программная навигация из действий
  • @persist — Сохранение элементов между навигациями по страницам
  • wire:navigate — Добавление SPA-навигации к ссылкам