导航

你是视觉学习者吗?
使用我们深入的屏幕录制掌握 Livewire
立即观看

许多现代 Web 应用程序都构建为“单页应用程序”(SPA)。在这些应用程序中,应用程序呈现的每个页面不再需要完全重新加载浏览器页面,从而避免了每次请求重新下载 JavaScript 和 CSS 资产的开销。

单页应用程序 的替代方案是多页应用程序。在这些应用程序中,每当用户单击链接时,都会请求一个全新的 HTML 页面并在浏览器中呈现。

虽然大多数 PHP 应用程序传统上都是多页应用程序,但 Livewire 通过你可以添加到应用程序中链接的简单属性提供单页应用程序体验:wire:navigate

基本用法

让我们探讨一个使用 wire:navigate 的示例。下面是一个典型的 Laravel 路由文件 (routes/web.php),其中三个 Livewire 组件被定义为路由

use App\Livewire\Dashboard;
use App\Livewire\ShowPosts;
use App\Livewire\ShowUsers;
 
Route::get('/', Dashboard::class);
 
Route::get('/posts', ShowPosts::class);
 
Route::get('/users', ShowUsers::class);

通过将 wire:navigate 添加到每个页面导航菜单中的每个链接,Livewire 将阻止对链接点击的标准处理,并用其自己的更快的版本替换它

<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 参数

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

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

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

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

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

如果您希望采用更激进的预取方法,则可以在链接上使用 .hover 修饰符

<a href="/posts" wire:navigate.hover>Posts</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 组件之外。一种常见做法是将持久元素放置在主布局中,例如 resources/views/components/layouts/app.blade.php

<!-- resources/views/components/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>
</head>
<body>
<main>
{{ $slot }}
</main>
 
@persist('player')
<audio src="{{ $episode->file }}" controls></audio>
@endpersist
</body>
</html>

保留滚动位置

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

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

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

JavaScript 钩子

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

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

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

下面是一个为每个这些事件注册侦听器的示例

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 naviation
// 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 nagivated 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 函数。

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

手动访问新页面

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

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

与分析软件一起使用

在你的应用中使用 wire:navigate 导航页面时,<head> 中的任何 <script> 标记只会在页面最初加载时评估。

这给诸如 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 仅在第一次页面访问时触发,而不是后续访问。

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

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

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

侦听此事件对于诸如初始化第三方库之类的操作很有用。

<head> 中的脚本加载一次

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

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

新的 <head> 脚本被评估

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

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

<!-- Page one -->
<head>
<script src="/app.js"></script>
</head>
 
<!-- Page two -->
<head>
<script src="/app.js"></script>
<script src="/third-party.js"></script>
</head>
头部资产会阻塞

如果你正在导航到包含类似 <script src="..."> 资产的新页面,该资产将在导航完成并新页面替换之前被获取并处理。这可能令人惊讶,但它确保任何依赖于这些资产的脚本都可以立即访问它们。

资产更改时重新加载

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

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

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

<!-- 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>

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

如果你正在使用 Laravel 的 Vite 插件 来捆绑并提供你的资产,Livewire 会自动向呈现的 HTML 资产标记添加 data-navigate-track。你可以像往常一样继续引用你的资产和脚本

<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> 标记都将运行

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

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

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

自定义进度条

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

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

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