通过构建播客播放器来学习 Livewire 3、Volt 和 Folio
发布于 作者: Jason Beggs
昨天,Laravel 团队发布了 Laravel Folio - 一个功能强大的基于页面的路由器,旨在简化 Laravel 应用程序中的路由。今天,他们 发布了 Volt - Livewire 的一个优雅的函数式 API,允许组件的 PHP 逻辑和 Blade 模板在同一个文件中共存,从而减少了样板代码。
虽然它们可以单独使用,但我认为将它们一起使用是构建 Laravel 应用程序的一种新的、高效的方式。
在本文中,我将教您如何构建一个简单的应用程序,该应用程序列出 Laravel News 播客的剧集,并允许用户播放它们,播放器可以跨页面加载无缝地继续播放。
设置 Livewire、Volt 和 Folio
首先,我们需要创建一个新的 Laravel 应用程序并安装 Livewire、Volt、Folio 和 Sushi(用于创建一些虚拟数据)。
laravel new composer require livewire/livewire:^3.0@beta livewire/volt:^1.0@beta laravel/folio:^1.0@beta calebporzio/sushi
Livewire v3、Volt 和 Folio 仍处于测试阶段。它们应该相当稳定,但请自行承担风险使用它们。
在要求包之后,我们需要运行 php artisan volt:install
和 php artisan folio:install
。这将为 Volt 和 Folio 需要的一些文件夹和服务提供者搭建脚手架。
Episode
模型
对于虚拟数据,我将创建一个 Sushi 模型。 Sushi 是由 Caleb Pozio 编写的包,允许您创建从模型文件中直接编写的数组中查询数据的 Eloquent 模型。当您构建示例应用程序或拥有不需要经常更改的数据时,这非常有用。
创建一个模型,然后删除 HasFactory
特性并将其替换为 Sushi
特性。我将 4 个最新的 Laravel News 播客剧集的详细信息添加为本示例的数据。
我不会详细介绍所有这些工作的原理,因为这不是本文的重点,并且如果您要构建自己的播客播放器,您可能会使用真正的 Eloquent 模型。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model;use Sushi\Sushi; class Episode extends Model{ use Sushi; protected $casts = [ 'released_at' => 'datetime', ]; protected $rows = [ [ 'number' => 195, 'title' => 'Queries, GPT, and sinking downloads', 'notes' => '...', 'audio' => 'https://media.transistor.fm/c28ad926/93e5fe7d.mp3', 'image' => 'https://images.transistor.fm/file/transistor/images/show/6405/full_1646972621-artwork.jpg', 'duration_in_seconds' => 2579, 'released_at' => '2023-07-06 10:00:00', ], [ 'number' => 194, 'title' => 'Squeezing lemons, punching cards, and bellowing forges', 'notes' => '...', 'audio' => 'https://media.transistor.fm/6d2d53fe/f70d9278.mp3', 'image' => 'https://images.transistor.fm/file/transistor/images/show/6405/full_1646972621-artwork.jpg', 'duration_in_seconds' => 2219, 'released_at' => '2023-06-21 10:00:00', ], [ 'number' => 193, 'title' => 'Precognition, faking Stripe, and debugging Blade', 'notes' => '...', 'audio' => 'https://media.transistor.fm/d434305e/975fbb28.mp3', 'image' => 'https://images.transistor.fm/file/transistor/images/show/6405/full_1646972621-artwork.jpg', 'duration_in_seconds' => 2146, 'released_at' => '2023-06-06 10:00:00', ], [ 'number' => 192, 'title' => 'High octane, sleepy code, and Aaron Francis', 'notes' => '...', 'audio' => 'https://media.transistor.fm/b5f81577/c58c90c8.mp3', 'image' => 'https://images.transistor.fm/file/transistor/images/show/6405/full_1646972621-artwork.jpg', 'duration_in_seconds' => 1865, 'released_at' => '2023-05-24 10:00:00', ], // ... ];}
布局视图
我们需要一个布局文件来加载 Tailwind、添加徽标并添加一些基本样式。由于 Livewire 和 Alpine 现在会自动注入它们的脚本和样式,因此我们甚至不需要在布局中加载它们!我们将在 resources/views/components/layout.blade.php
创建一个匿名 Blade 组件作为布局。
<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Laravel News Podcast Player</title> <script src="https://cdn.tailwindcss.com?plugins=typography"></script> </head> <body class="min-h-screen bg-gray-50 font-sans text-black antialiased"> <div class="mx-auto max-w-2xl px-6 py-24"> <a href="/episodes" class="mx-auto flex max-w-max items-center gap-3 font-bold text-[#FF2D20] transition hover:opacity-80" > <img src="/images/logo.svg" alt="Laravel News" class="mx-auto w-12" /> <span>Laravel News Podcast</span> </a> <div class="py-10">{{ $slot }}</div> </div> </body></html>
剧集列表页面
首先,我们需要一个页面来显示播客的所有剧集。
使用 Folio,我们可以轻松地在 resources/views/pages
目录中创建一个新页面,Laravel 会自动为该页面创建一个路由。我们希望我们的路由为 /episodes
,因此我们可以运行 php artisan make:folio episodes/index
。这将在 resources/views/pages/episodes/index.blade.php
创建一个空白视图。
在此页面上,我们将插入布局组件,然后循环遍历所有播客剧集。Volt 为大多数 Livewire 功能提供了命名空间函数。在这里,我们将打开常规的 <?php ?>
开启和关闭标签。在这些标签内,我们将使用 computed
函数创建一个 $episodes
变量,该变量运行一个查询来获取所有 Episode 模型 ($episodes = computed(fn () => Episode::get());
)。我们可以使用 $this->episodes
在模板中访问计算属性。
我还创建了一个 $formatDuration
变量,它是一个函数,用于将每个剧集的 duration_in_seconds
属性格式化为可读格式。我们可以使用 $this->formatDuration($episode->duration_in_seconds)
在模板中调用该函数。
我们还需要将页面上的动态功能包装在 @volt
指令中,以便将其在 Folio 页面内注册为 "匿名 Livewire 组件"。
<?php use App\Models\Episode;use Illuminate\Support\Stringable;use function Livewire\Volt\computed;use function Livewire\Volt\state; $episodes = computed(fn () => Episode::get()); $formatDuration = function ($seconds) {
return str(date('G\h i\m s\s', $seconds)) ->trim('0h ') ->explode(' ') ->mapInto(Stringable::class) ->each->ltrim('0') ->join(' ');}; ?> <x-layout> @volt <div class="rounded-xl border border-gray-200 bg-white shadow"> <ul class="divide-y divide-gray-100"> @foreach ($this->episodes as $episode) <li wire:key="{{ $episode->number }}" class="flex flex-col items-start gap-x-6 gap-y-3 px-6 py-4 sm:flex-row sm:items-center sm:justify-between" > <div> <h2> No. {{ $episode->number }} - {{ $episode->title }} </h2> <div class="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-gray-500" > <p> Released: {{ $episode->released_at->format('M j, Y') }} </p> · <p> Duration: {{ $this->formatDuration($episode->duration_in_seconds) }} </p> </div> </div> <button type="button" class="flex shrink-0 items-center gap-1 text-sm font-medium text-[#FF2D20] transition hover:opacity-60" > <img src="/images/play.svg" alt="Play" class="h-8 w-8 transition hover:opacity-60" /> <span>Play</span> </button> </li> @endforeach </ul> </div> @endvolt</x-layout>
剧集播放器
从那里,我们需要添加一些交互性。我想添加一个剧集播放器,这样我们就可以从剧集列表中收听剧集。这可以是我们渲染在布局文件中的一个常规 Blade 组件。
<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Laravel News Podcast Player</title> <script src="https://cdn.tailwindcss.com?plugins=typography"></script> </head> <body class="min-h-screen bg-gray-50 font-sans text-black antialiased"> <div class="mx-auto max-w-2xl px-6 py-24"> <a href="/episodes" class="mx-auto flex max-w-max items-center gap-3 font-bold text-[#FF2D20] transition hover:opacity-80" > <img src="/images/logo.svg" alt="Laravel News" class="mx-auto w-12" /> <span>Laravel News Podcast</span> </a> <div class="py-10">{{ $slot }}</div> <x-episode-player /> </div> </body></html>
我们可以通过添加 resources/views/components/episode-player.blade.php
文件来创建该组件。在组件内部,我们将添加一个 <audio>
元素,它具有一些 Alpine 代码来存储活动剧集以及一个更新活动剧集并启动音频的函数。我们只会在设置了活动剧集的情况下显示播放器,并且会在包装器中添加一个漂亮的淡入淡出过渡。
<div x-data="{ activeEpisode: null, play(episode) { this.activeEpisode = episode this.$nextTick(() => { this.$refs.audio.play() }) }, }" x-show="activeEpisode" x-transition.opacity.duration.500ms class="fixed inset-x-0 bottom-0 w-full border-t border-gray-200 bg-white" style="display: none"> <div class="mx-auto max-w-xl p-6"> <h3 x-text="`Playing: No. ${activeEpisode?.number} - ${activeEpisode?.title}`" class="text-center text-sm font-medium text-gray-600" ></h3> <audio x-ref="audio" class="mx-auto mt-3" :src="activeEpisode?.audio" controls ></audio> </div></div>
如果我们重新加载页面,我们将不会看到任何更改。这是因为我们还没有添加播放剧集的方法。我们将使用事件从 Livewire 组件到播放器进行通信。首先,在播放器中,我们将添加 x-on:play-episode.window="play($event.detail)"
来监听窗口上的 play-episode
事件,然后调用 play
函数。
<div x-data="{ activeEpisode: null, play(episode) { this.activeEpisode = episode this.$nextTick(() => { this.$refs.audio.play() }) }, }" x-on:play-episode.window="play($event.detail)" ...> <!-- ... --></div>
接下来,回到 episodes/index
页面,我们将为每个剧集的播放按钮添加一个单击侦听器。按钮将分派 play-episode
事件,该事件将被剧集播放器接收并在那里处理。
<button x-data x-on:click="$dispatch('play-episode', @js($episode))" ...> <img src="/images/play.svg" alt="Play" class="h-8 w-8 transition hover:opacity-60" /> <span>Play</span></button>
剧集详细信息页面
接下来,我想添加一个剧集详细信息页面来显示每个剧集的节目说明和其他详细信息。
Folio 在文件名中为路由模型绑定提供了一些非常酷的约定。为了为 /episodes/{episode:id}
创建一个等效路由,请在 resources/views/pages/episodes/[Episode].blade.php
创建一个页面。要使用除主键以外的路由参数,您可以在文件名中使用 [Model:some_other_key].blade.php
语法。我想在 URL 中使用剧集编号,因此我们将创建一个位于 resources/views/pages/episodes/[Episode:number].blade.php
的文件。
Folio 将自动查询 Episode 模型以获取具有我们传递给 URL 的编号的剧集,并在我们的 <?php ?>
代码中将其作为 $episode
变量提供。然后,我们可以使用 Volt 的 state
函数将其转换为 Livewire 属性。
我们还将在此页面上包含一个播放按钮,以便用户可以在查看剧集详细信息时播放剧集。
<?phpuse Illuminate\Support\Stringable;use function Livewire\Volt\state; state(['episode' => fn () => $episode]); $formatDuration = function ($seconds) {
return str(date('G\h i\m s\s', $seconds)) ->trim('0h ') ->explode(' ') ->mapInto(Stringable::class) ->each->ltrim('0') ->join(' ');}; ?> <x-layout> @volt <div class="overflow-hidden rounded-xl border border-gray-200 bg-white shadow"> <div class="p-6"> <div class="flex items-center justify-between gap-8"> <div> <h2 class="text-xl font-medium"> No. {{ $episode->number }} - {{ $episode->title }} </h2> <div class="mt-1 flex items-center gap-3 text-sm text-gray-500" > <p> Released: {{ $episode->released_at->format('M j, Y') }} </p> · <p> Duration: {{ $this->formatDuration($episode->duration_in_seconds) }} </p> </div> </div> <button x-on:click="$dispatch('play-episode', @js($episode))" type="button" class="flex items-center gap-1 text-sm font-medium text-[#FF2D20] transition hover:opacity-60" > <img src="/images/play.svg" alt="Play" class="h-8 w-8 transition hover:opacity-60" /> <span>Play</span> </button> </div> <div class="prose prose-sm mt-4"> {!! $episode->notes !!} </div> </div> <div class="bg-gray-50 px-6 py-4"> <a href="/episodes" class="text-sm font-medium text-gray-600" > ← Back to episodes </a> </div> </div> @endvolt</x-layout>
现在,我们需要从索引页面链接到详细信息页面。回到 episodes/index
页面,让我们将每个剧集的 <h2>
包装在锚标签中。
@foreach ($this->episodes as $episode) <li wire:key="{{ $episode->number }}" class="flex flex-col items-start gap-x-6 gap-y-3 px-6 py-4 sm:flex-row sm:items-center sm:justify-between" > <div> <a href="/episodes/{{ $episode->number }}" class="transition hover:text-[#FF2D20]" > <h2> No. {{ $episode->number }} - {{ $episode->title }} </h2> </a> </div> {{-- ... --}} </li>@endforeach
SPA 模式
我们快到了。应用程序看起来相当不错并且功能良好,但有一个问题。如果用户正在收听剧集,然后导航到其他页面,剧集播放器会丢失其活动剧集状态并消失。
幸运的是,Livewire 现在有 wire:navigate
和 @persist
指令来帮助解决这些问题!
在我们的布局文件中,让我们将徽标和剧集播放器包装在 @persist
块中。Livewire 会检测到这一点,并在我们更改页面时跳过重新渲染这些块。
<!DOCTYPE html><html lang="en"> ... <body class="min-h-screen bg-gray-50 font-sans text-black antialiased"> <div class="mx-auto max-w-2xl px-6 py-24"> @persist('logo') <a href="/episodes" class="mx-auto flex max-w-max items-center gap-3 font-bold text-[#FF2D20] transition hover:opacity-80" > <img src="/images/logo.svg" alt="Laravel News" class="mx-auto w-12" /> <span>Laravel News Podcast</span> </a> @endpersist <div class="py-10">{{ $slot }}</div> @persist('player') <x-episode-player /> @endpersist </div> </body></html>
最后,我们需要将 wire:navigate
属性添加到应用程序中的所有链接。例如
<a href="/episodes/{{ $episode->number }}" class="transition hover:text-[#FF2D20]" wire:navigate> <h2> No. {{ $episode->number }} - {{ $episode->title }} </h2></a>
当您使用 wire:navigate
属性时,在幕后,Livewire 将使用 AJAX 获取新页面的内容,然后神奇地交换浏览器中的内容,而无需执行完整的页面重新加载。这使得页面加载感觉非常快,并启用了诸如持久性之类的功能。它启用了以前只能通过构建 SPA 来完成的功能。
结论
使用 Volt 和 Folio 构建这个演示应用真的很有趣。如果你想查看完整源代码或自己尝试一下,我已经将演示应用上传到了 这里!
你觉得怎么样?Livewire v3 + Volt + Folio 是现在构建 Laravel 应用最简单的堆栈吗?我认为它真的很酷,并且对于那些习惯于使用 Next.js 和 Nuxt.js 等 JavaScript 框架构建应用的人来说可能更熟悉。将一个页面所有代码放在一起也很棒 - 样式(通过 Tailwind)、JS(通过 Alpine)和后端代码都在同一个文件中。欢迎在 Twitter 上分享你的想法!
TALL 堆栈(Tailwind CSS、Alpine.js、Laravel 和 Livewire)顾问以及 designtotailwind.com 的所有者。