Skip to content

页面导航

许多现代 Web 应用程序构建为"单页应用程序"(SPA)。在这些应用程序中,应用程序渲染的每个页面不再需要完整的浏览器页面重新加载,从而避免了每次请求时重新下载 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 将阻止链接点击的标准处理,并用它自己更快的版本替换它:

blade
<nav>
    <a href="/" wire:navigate>Dashboard</a>
    <a href="/posts" wire:navigate>Posts</a>
    <a href="/users" wire:navigate>Users</a>
</nav>

以下是点击 wire:navigate 链接时发生的情况分解:

  • 用户点击链接
  • Livewire 阻止浏览器访问新页面
  • 相反,Livewire 在后台请求页面并在页面顶部显示加载进度条
  • 当收到新页面的 HTML 时,Livewire 会将当前页面的 URL、<title> 标签和 <body> 内容替换为新页面的元素

这种技术可以大大加快页面加载时间——通常快两倍——并使应用程序"感觉"像是由 JavaScript 驱动的单页应用程序。

重定向

当你的某个 Livewire 组件将用户重定向到应用程序内的另一个 URL 时,你也可以指示 Livewire 使用其 wire:navigate 功能来加载新页面。要实现这一点,请向 redirect() 方法提供 navigate 参数:

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

现在,Livewire 将替换当前页面的内容和 URL 为新页面,而不是使用完整的页面请求来将用户重定向到新的 URL。

默认情况下,Livewire 包含一个温和的策略,在用户点击链接之前 预加载 页面:

  • 用户按下鼠标按钮
  • Livewire 开始请求页面
  • 用户松开鼠标按钮完成 点击
  • Livewire 完成请求并导航到新页面

令人惊讶的是,用户按下和松开鼠标按钮之间的时间通常足以从服务器加载半个甚至整个页面。

如果你想要更激进的预加载方法,可以在链接上使用 .hover 修饰符:

blade
<a href="/posts" wire:navigate.hover>Posts</a>

.hover 修饰符将指示 Livewire 在用户将鼠标悬停在链接上 60 毫秒后预加载页面。

悬停预加载会增加服务器使用量

因为并非所有用户都会点击他们悬停的链接,添加 .hover 将请求可能不需要的页面,尽管 Livewire 尝试通过在预加载页面前等待 60 毫秒来减轻部分开销。

在页面访问之间持久化元素

有时,用户界面的某些部分需要在页面加载之间持久化,例如音频或视频播放器。例如,在播客应用程序中,用户可能希望在浏览其他页面时继续收听某一集。

你可以使用 @persist 指令在 Livewire 中实现这一点。

通过使用 @persist 包装元素并为其提供名称,当使用 wire:navigate 请求新页面时,Livewire 将在新页面上查找具有匹配 @persist 的元素。Livewire 不会像正常情况那样替换该元素,而是将上一页的现有 DOM 元素重用到新页面中,从而保留元素内的任何状态。

以下是使用 @persist 跨页面持久化 <audio> 播放器元素的示例:

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

如果上述 HTML 同时出现在两个页面上——当前页面和下一个页面——原始元素将在新页面上被重用。在音频播放器的情况下,从一个页面导航到另一个页面时,音频播放不会被中断。

请注意,持久化元素必须放置在 Livewire 组件之外。常见的做法是将持久化元素放置在主布局中,例如 resources/views/layouts/app.blade.php

html
<!-- 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 ?? 'Page Title' }}</title>

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

        @persist('player') <!-- [tl! highlight:2] -->
            <audio src="{{ $episode->file }}" controls></audio>
        @endpersist

        @livewireScripts
    </body>
</html>

你可能习惯于使用服务器端 Blade 在导航栏中突出显示当前活动页面链接,如下所示:

blade
<nav>
    <a href="/" class="@if (request->is('/')) font-bold text-zinc-800 @endif">Dashboard</a>
    <a href="/posts" class="@if (request->is('/posts')) font-bold text-zinc-800 @endif">Posts</a>
    <a href="/users" class="@if (request->is('/users')) font-bold text-zinc-800 @endif">Users</a>
</nav>

但是,这在持久化元素内部不起作用,因为它们会在页面加载之间被重用。相反,你应该使用 Livewire 的 wire:current 指令来突出显示当前活动链接。

只需将你想要应用到当前活动链接的任何 CSS 类传递给 wire:current

blade
<nav>
    <a href="/dashboard" ... wire:current="font-bold text-zinc-800">Dashboard</a>
    <a href="/posts" ... wire:current="font-bold text-zinc-800">Posts</a>
    <a href="/users" ... wire:current="font-bold text-zinc-800">Users</a>
</nav>

现在,当访问 /posts 页面时,"Posts" 链接将比其他链接具有更强的字体处理。

wire:current 文档 中了解更多信息。

保持滚动位置

默认情况下,Livewire 会在页面之间来回导航时保持页面的滚动位置。但是,有时你可能希望保持在页面加载之间持久化的单个元素的滚动位置。

为此,你必须将 wire:scroll 添加到包含滚动条的元素上,如下所示:

html
@persist('scrollbar')
<div class="overflow-y-scroll" wire:scroll> <!-- [tl! highlight] -->
    <!-- ... -->
</div>
@endpersist

JavaScript 钩子

每次页面导航都会触发三个生命周期钩子:

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

需要注意的是,这三个钩子事件会在所有类型的导航上分发。这包括使用 Livewire.navigate() 的手动导航、启用导航的重定向以及浏览器中的后退和前进按钮按下。

以下是为这些事件注册监听器的示例:

js
document.addEventListener('livewire:navigate', (event) => {
    // Triggers when a navigation is triggered.

    // Can be "cancelled" (prevent the navigate from actually being performed):
    event.preventDefault()

    // Contains helpful context about the navigation trigger:
    let context = event.detail

    // A URL object of the intended destination of the navigation...
    context.url

    // A boolean [true/false] indicating whether or not this navigation
    // was triggered by a back/forward (history state) navigation...
    context.history

    // A boolean [true/false] indicating whether or not there is
    // cached version of this page to be used instead of
    // fetching a new one via a network round-trip...
    context.cached
})

document.addEventListener('livewire:navigating', () => {
    // Triggered when new HTML is about to swapped onto the page...

    // This is a good place to mutate any HTML before the page
    // is navigated away from...
})

document.addEventListener('livewire:navigated', () => {
    // Triggered as the final step of any page navigation...

    // Also triggered on page-load instead of "DOMContentLoaded"...
})

事件监听器将在页面之间持久化

当你将事件监听器附加到文档时,在导航到其他页面时不会删除它。如果你需要代码仅在导航到特定页面后运行,或者你在每个页面上添加相同的事件监听器,这可能会导致意外行为。如果不删除事件监听器,它可能会在其他页面上查找不存在的元素时导致异常,或者你可能会在每次导航时多次执行事件监听器。

删除事件监听器运行后的简单方法是将选项 {once: true} 作为第三个参数传递给 addEventListener 函数。

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

手动访问新页面

除了 wire:navigate,你还可以手动调用 Livewire.navigate() 方法来使用 JavaScript 触发对新页面的访问:

html
<script>
    // ...

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

与分析软件一起使用

在应用程序中使用 wire:navigate 导航页面时,<head> 中的任何 <script> 标签仅在页面最初加载时评估。

这给 Fathom Analytics 等分析软件带来了问题。这些工具依赖于在每次页面更改时评估 <script> 片段,而不仅仅是第一次。

Google Analytics 这样的工具足够智能,可以自动处理这个问题,但是,在使用 Fathom Analytics 时,你必须在脚本标签中添加 data-spa="auto" 以确保正确跟踪每次页面访问:

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

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

脚本执行

使用 wire:navigate 导航到新页面时,感觉 像浏览器已更改页面;但是,从浏览器的角度来看,从技术上讲,你仍然在原始页面上。

因此,样式和脚本在第一页上正常执行,但在后续页面上,你可能必须调整通常编写 JavaScript 的方式。

以下是使用 wire:navigate 时应注意的一些注意事项和场景。

不要依赖 DOMContentLoaded

通常的做法是将 JavaScript 放在 DOMContentLoaded 事件监听器中,以便你想要运行的代码仅在页面完全加载后执行。

使用 wire:navigate 时,DOMContentLoaded 仅在第一次页面访问时触发,而不是后续访问。

要在每次页面访问时运行代码,请将 DOMContentLoaded 的每个实例替换为 livewire:navigated

js
document.addEventListener('DOMContentLoaded', () => { // [tl! remove]
document.addEventListener('livewire:navigated', () => { // [tl! add]
    // ...
})

现在,放置在此监听器内的任何代码都将在初始页面访问时运行,并且在 Livewire 完成导航到后续页面后也会运行。

监听此事件对于初始化第三方库等操作很有用。

<head> 中的脚本仅加载一次

如果两个页面在 <head> 中包含相同的 <script> 标签,则该脚本仅在初始页面访问时运行,而不会在后续页面访问时运行。

blade
<!-- Page one -->
<head>
    <script src="/app.js"></script>
</head>

<!-- Page two -->
<head>
    <script src="/app.js"></script>
</head>

新的 <head> 脚本会被执行

如果后续页面在 <head> 中包含初始页面访问的 <head> 中不存在的新 <script> 标签,Livewire 将运行新的 <script> 标签。

在下面的示例中,page two 包含用于第三方工具的新 JavaScript 库。当用户导航到 page two 时,该库将被执行。

blade
<!-- Page one -->
<head>
    <script src="/app.js"></script>
</head>

<!-- Page two -->
<head>
    <script src="/app.js"></script>
    <script src="/third-party.js"></script>
</head>

Head 资源会阻塞

如果你导航到包含 head 标签中的资源(如 <script src="...">)的新页面。该资源将在导航完成和新页面交换之前被获取和处理。这可能是令人惊讶的行为,但它确保依赖于这些资源的任何脚本都可以立即访问它们。

资源更改时重新加载

在应用程序的主 JavaScript 文件名中包含版本哈希是常见的做法。这确保在部署应用程序的新版本后,用户将收到新的 JavaScript 资源,而不是从浏览器缓存提供的旧版本。

但是,现在你正在使用 wire:navigate,每次页面访问不再是新的浏览器页面加载,你的用户在部署后可能仍会收到过时的 JavaScript。

为了防止这种情况,你可以在 <head> 中的 <script> 标签中添加 data-navigate-track

blade
<!-- Page one -->
<head>
    <script src="/app.js?id=123" data-navigate-track></script>
</head>

<!-- Page two -->
<head>
    <script src="/app.js?id=456" data-navigate-track></script>
</head>

当用户访问 page two 时,Livewire 将检测到新的 JavaScript 资源并触发完整的浏览器页面重新加载。

如果你使用 Laravel 的 Vite 插件 来打包和提供资源,Livewire 会自动将 data-navigate-track 添加到渲染的 HTML 资源标签中。你可以继续像往常一样引用资源和脚本:

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

Livewire 将自动将 data-navigate-track 注入到渲染的 HTML 标签上。

仅跟踪查询字符串更改

Livewire 仅在 [data-navigate-track] 元素的查询字符串(?id="456")更改时重新加载页面,而不是 URI 本身(/app.js)。

<body> 中的脚本会重新执行

因为 Livewire 在每个新页面上替换 <body> 的全部内容,所以新页面上的所有 <script> 标签都将运行:

blade
<!-- Page one -->
<body>
    <script>
        console.log('Runs on page one')
    </script>
</body>

<!-- Page two -->
<body>
    <script>
        console.log('Runs on page two')
    </script>
</body>

如果你的 body 中有一个 <script> 标签,并且只想运行一次,可以将 data-navigate-once 属性添加到 <script> 标签,Livewire 将仅在初始页面访问时运行它:

blade
<script data-navigate-once>
    console.log('Runs only on page one')
</script>

自定义进度条

当页面加载时间超过 150ms 时,Livewire 将在页面顶部显示进度条。

你可以在 Livewire 的配置文件(config/livewire.php)中自定义此进度条的颜色或完全禁用它:

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