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>仪表板</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 功能来加载新页面。要实现这一点,向 redirect() 方法提供 navigate 参数:

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

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

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

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

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

如果你想要更积极的预取方法,你可以在链接上使用 .hover 修饰符:

blade
<a href="/posts" wire:navigate.hover>文章</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 ?? 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 在导航栏中高亮当前活动页面链接,如下所示:

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 自动向与当前页面匹配的任何 wire:navigate 链接添加 data-current 属性。这允许你使用 CSS 或 Tailwind 样式化活动链接,而无需任何额外的指令:

blade
<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 页面时,"文章"链接将自动接收 data-current 属性并相应地进行样式化。

你也可以使用纯 CSS 样式化活动链接:

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

如果你想在仍然使用 wire:navigate 的同时禁用此行为,你可以添加 wire:current.ignore 指令:

blade
<a href="/posts" wire:navigate wire:current.ignore>文章</a>

使用 wire:current 指令

或者,你可以使用 Livewire 的 wire:current 指令向当前活动链接添加 CSS 类:

blade
<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 属性通常更简单、更灵活,因为它不需要额外的指令,并且与 Tailwind 的数据属性变体无缝配合。

wire:current 文档 中阅读更多内容。

保留滚动位置

默认情况下,Livewire 在前后导航页面时会保留页面的滚动位置。然而,有时你可能想保留在页面加载之间持久化的单个元素的滚动位置。

要做到这一点,你必须向包含滚动条的元素添加 wire:navigate:scroll,如下所示:

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

JavaScript 钩子

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

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

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

这是为每个事件注册监听器的示例:

js
document.addEventListener('livewire:navigate', (event) => {
    // 当导航被触发时触发。

    // 可以被"取消"(阻止实际执行导航):
    event.preventDefault()

    // 包含有关导航触发器的有用上下文:
    let context = event.detail

    // 导航目标目的地的 URL 对象...
    context.url

    // 一个布尔值 [true/false] 指示此导航是否由
    // 后退/前进(历史状态)导航触发...
    context.history

    // 一个布尔值 [true/false] 指示是否有
    // 此页面的缓存版本可使用,而不是
    // 通过网络往返获取新版本...
    context.cached
})

document.addEventListener('livewire:navigating', (e) => {
    // 当新 HTML 即将被交换到页面时触发...

    // 这是在页面导航离开之前修改任何 HTML 的好地方...

    // 你可以注册一个 onSwap 回调,在新 HTML 被交换到页面后
    // 但在脚本加载之前运行代码。
    // 这是应用关键样式(如暗模式)以防止闪烁的好地方...
    e.detail.onSwap(() => {
        // ...
    })
})

document.addEventListener('livewire:navigated', () => {
    // 作为任何页面导航的最后一步触发...

    // 也在页面加载时触发,而不是 "DOMContentLoaded"...
})

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

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

删除运行后事件监听器的简单方法是将选项 {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>
    @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
<!-- 页面一 -->
<head>
    <script src="/app.js"></script>
</head>

<!-- 页面二 -->
<head>
    <script src="/app.js"></script>
</head>

新的 <head> 脚本会被评估

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

在下面的示例中,_页面二_包含一个用于第三方工具的新 JavaScript 库。当用户导航到_页面二_时,该库将被评估。

blade
<!-- 页面一 -->
<head>
    <script src="/app.js"></script>
</head>

<!-- 页面二 -->
<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
<!-- 页面一 -->
<head>
    <script src="/app.js?id=123" data-navigate-track></script>
</head>

<!-- 页面二 -->
<head>
    <script src="/app.js?id=456" data-navigate-track></script>
</head>

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

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

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

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

仅跟踪查询字符串更改

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

<body> 中的脚本会被重新评估

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

blade
<!-- 页面一 -->
<body>
    <script>
        console.log('在页面一上运行')
    </script>
</body>

<!-- 页面二 -->
<body>
    <script>
        console.log('在页面二上运行')
    </script>
</body>

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

blade
<script data-navigate-once>
    console.log('仅在页面一上运行')
</script>

自定义进度条

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

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

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

另请参阅