导航
许多现代 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::livewire('/', 'pages::dashboard');
Route::livewire('/posts', 'pages::show-posts');
Route::livewire('/users', 'pages::show-users');在每个页面的导航菜单里给每个链接加上 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);此时不再用整页请求把用户重定向到新 URL,而是由 Livewire 用新页面替换当前页面的内容与 URL。
预取链接
默认情况下,Livewire 采用一种温和策略,在用户点击链接之前 预取 页面:
- 用户按下鼠标按键
- Livewire 开始请求该页面
- 用户松开鼠标按键,完成 _点击_
- Livewire 完成请求并导航到新页面
出人意料的是,用户按下到松开鼠标按键之间的时间,往往足以从服务器加载半页甚至整页。
若希望采用更激进的预取策略,可以在链接上使用 .hover 修饰符:
<a href="/posts" wire:navigate.hover>Posts</a>.hover 修饰符会指示 Livewire:用户将鼠标悬停在链接上 60 毫秒后预取该页面。
WARNING
悬停预取会增加服务器负载
因为并非所有用户都会点击他们悬停过的链接,添加 .hover 会请求可能用不到的页面;不过 Livewire 会先等待 60 毫秒再预取,以尽量减轻部分开销。
跨页面访问持久化元素
有时,界面中有些部分需要在页面加载之间保持不变,例如音频或视频播放器。例如在播客应用中,用户可能希望在浏览其他页面时继续收听某一集。
在 Livewire 中可以用 @persist 指令实现这一点。
用 @persist 包裹元素并为其提供名称后,当通过 wire:navigate 请求新页面时,Livewire 会在新页面上查找具有匹配 @persist 的元素。不会像通常那样替换该元素,而是把上一页已有的 DOM 元素用到新页面中,从而保留元素内的任何状态。
下面示例用 @persist 让 <audio> 播放器元素跨页面持久化:
@persist('player')
<audio src="{{ $episode->file }}" controls></audio>
@endpersist若上述 HTML 同时出现在当前页与下一页,原元素会在新页面上被复用。就音频播放器而言,从一页导航到另一页时,播放不会中断。
请注意:持久化元素必须放在 Livewire 组件之外。常见做法是把它放在主布局中,例如 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') <!-- [tl! highlight:2] -->
<audio src="{{ $episode->file }}" controls></audio>
@endpersist
@livewireScripts
</body>
</html>高亮当前链接
你可能习惯用服务端 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>不过,这在持久化元素内不起作用,因为它们会在页面加载之间被复用。导航过程中高亮活动链接有两种做法:
使用 data-current 属性
Livewire 会自动给匹配当前页面的任意 wire:navigate 链接添加 data-current 属性。这样你无需额外指令,就能用 CSS 或 Tailwind 样式化活动链接:
<nav>
<a href="/dashboard" wire:navigate class="data-current:font-bold data-current:text-zinc-800">Dashboard</a>
<a href="/posts" wire:navigate class="data-current:font-bold data-current:text-zinc-800">Posts</a>
<a href="/users" wire:navigate class="data-current:font-bold data-current:text-zinc-800">Users</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>Posts</a>使用 wire:current 指令
或者,可以用 Livewire 的 wire:current 指令给当前活动链接添加 CSS 类:
<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」链接的字体样式会比其他链接更醒目。
TIP
为简洁起见,优先使用 data-current
两种做法都能很好地工作,但使用 data-current 属性通常更简单、更灵活:不需要额外指令,且能与 Tailwind 的 data 属性变体无缝配合。
详见 wire:current 文档。
保留滚动位置
默认情况下,Livewire 在页面间前进后退导航时会保留页面滚动位置。不过有时你可能希望保留在页面加载之间持久化的某个单独元素的滚动位置。
为此,必须在带滚动条的元素上添加 wire:navigate:scroll,如下所示:
@persist('sidebar')
<div class="overflow-y-scroll" wire:navigate:scroll> <!-- [tl! highlight] -->
<!-- ... -->
</div>
@endpersistJavaScript 钩子
每次页面导航都会触发三个生命周期钩子:
livewire:navigatelivewire:navigatinglivewire: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 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', (e) => {
// Triggered when new HTML is about to be swapped onto the page...
// This is a good place to mutate any HTML before the page
// is navigated away from...
// You can register an onSwap callback to run code after the
// new HTML is swapped onto the page but before scripts are loaded.
// This is a good place to apply critical styles such as dark mode
// to prevent flickering...
e.detail.onSwap(() => {
// ...
})
})
document.addEventListener('livewire:navigated', () => {
// Triggered as the final step of any page navigation...
// Also triggered on page-load instead of "DOMContentLoaded"...
})WARNING
事件监听器会跨页面保留
当你把事件监听器挂到 document 上时,导航到其他页面后它不会被移除。若你需要代码只在导航到特定页面后运行,或在每个页面都添加同一个事件监听器,这可能导致意外行为。若不移除监听器,它可能在其他页面因查找不存在的元素而抛出异常,也可能导致每次导航执行多次。
一种简单做法是:把选项 {once: true} 作为第三个参数传给 addEventListener,让监听器在运行后自行移除。
手动访问新页面
除了 wire:navigate,你还可以手动调用 Livewire.navigate() 方法,用 JavaScript 触发对新页面的访问:
<script>
// ...
Livewire.navigate('/new/url')
</script>与分析软件配合使用
在应用中用 wire:navigate 导航页面时,<head> 中的任何 <script> 标签仅在页面初次加载时求值。
这会给 Fathom Analytics 这类分析软件带来问题。这些工具依赖 <script> 片段在每一次页面变更时都求值,而不仅仅是第一次。
Google Analytics 这类工具足够智能,能自动处理这种情况;但使用 Fathom Analytics 时,必须在 script 标签上添加 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> <!-- [tl! highlight] -->
@endif
</head>脚本求值
用 wire:navigate 导航到新页面时,感觉 像浏览器换了页面;但从浏览器角度看,你技术上仍停留在原始页面。
因此,样式和脚本在第一页会正常执行;但在后续页面上,你可能需要调整平时写 JavaScript 的方式。
使用 wire:navigate 时,有一些注意事项和场景需要了解。
不要依赖 DOMContentLoaded
常见做法是把 JavaScript 放在 DOMContentLoaded 事件监听器里,以便只在页面完全加载后再运行你想执行的代码。
使用 wire:navigate 时,DOMContentLoaded 仅在首次页面访问时触发,后续访问不会触发。
要在每次页面访问时运行代码,把每一处 DOMContentLoaded 换成 livewire:navigated:
document.addEventListener('DOMContentLoaded', () => { // [tl! remove]
document.addEventListener('livewire:navigated', () => { // [tl! add]
// ...
})现在,放在该监听器内的任何代码都会在首次页面访问时运行,也会在 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> 标签,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>INFO
Head 资源会阻塞导航
若你导航到的新页面在 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 会自动把 data-navigate-track 注入到渲染出的 HTML 标签上。
WARNING
只跟踪查询字符串的变更
仅当 [data-navigate-track] 元素的查询字符串(?id="456")发生变化时,Livewire 才会重载页面,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>若 body 中有只想运行一次的 <script> 标签,可为其添加 data-navigate-once 属性,Livewire 就只会在首次页面访问时运行它:
<script data-navigate-once>
console.log('Runs only on page one')
</script>自定义进度条
当页面加载超过 150ms 时,Livewire 会在页面顶部显示进度条。
你可以在 Livewire 的配置文件(config/livewire.php)中自定义该进度条的颜色,或完全禁用它:
'navigate' => [
'show_progress_bar' => false,
'progress_bar_color' => '#2299dd',
],另见
- 页面 — 创建可路由的页面组件
- 重定向 — 从操作中以编程方式导航
- @persist — 跨页面导航持久化元素
- wire:navigate — 为链接添加 SPA 导航