通过构建播客播放器来学习 Livewire 3、Volt 和 Folio

发布于 作者:

Learn Livewire 3, Volt, and Folio by building a podcast player image

昨天,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:installphp 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>
&middot;
<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 属性。

我们还将在此页面上包含一个播放按钮,以便用户可以在查看剧集详细信息时播放剧集。

<?php
use 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>
&middot;
<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"
>
&larr; 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 上分享你的想法!

Jason Beggs photo

TALL 堆栈(Tailwind CSS、Alpine.js、Laravel 和 Livewire)顾问以及 designtotailwind.com 的所有者。

Cube

Laravel 新闻

加入 40,000 多名其他开发者,永不错过新的技巧、教程等。

Laravel Forge logo

Laravel Forge

轻松创建和管理您的服务器,并在几秒钟内部署您的 Laravel 应用程序。

Laravel Forge
Tinkerwell logo

Tinkerwell

Laravel 开发人员必备的代码运行器。使用 AI、自动补全和对本地和生产环境的即时反馈进行 Tinker。

Tinkerwell
No Compromises logo

无妥协

Joel 和 Aaron,两位来自 No Compromises 播客的资深开发者,现在可以为您的 Laravel 项目聘请。 ⬧ 7500 美元/月的固定费用。 ⬧ 没有漫长的销售流程。 ⬧ 没有合同。 ⬧ 100%退款保证。

无妥协
Kirschbaum logo

Kirschbaum

提供创新和稳定性,以确保您的 Web 应用程序成功。

Kirschbaum
Shift logo

Shift

正在运行旧版本的 Laravel?即时、自动化的 Laravel 升级和代码现代化,让您的应用程序保持新鲜。

Shift
Bacancy logo

Bacancy

只需 2500 美元/月,即可让经验丰富的 Laravel 开发人员(拥有 4-6 年经验)为您的项目增光添彩。获得 160 小时的专业知识和 15 天的无风险试用。立即安排电话会议!

Bacancy
Lucky Media logo

Lucky Media

现在就来试试 Lucky - Laravel 开发的理想选择,拥有十多年的经验!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel 电子商务

Laravel 的电子商务。一个开源包,将现代无头电子商务功能的强大功能带入 Laravel。

Lunar: Laravel 电子商务
LaraJobs logo

LaraJobs

官方 Laravel 职位板

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS 启动套件

SaaSykit 是一个 Laravel SaaS 启动套件,包含运行现代 SaaS 所需的所有功能。支付、精美结帐、管理面板、用户仪表板、身份验证、即用型组件、统计数据、博客、文档等。

SaaSykit: Laravel SaaS 启动套件
Rector logo

Rector

您实现 Laravel 无缝升级、降低成本和加速创新的合作伙伴,助力公司取得成功

Rector
MongoDB logo

MongoDB

通过 MongoDB 和 Laravel 的强大集成增强您的 PHP 应用程序,使开发人员能够轻松高效地构建应用程序。支持事务性、搜索、分析和移动用例,同时使用熟悉的 Eloquent API。了解 MongoDB 的灵活、现代数据库如何改变您的 Laravel 应用程序。

MongoDB
Maska is a Simple Zero-dependency Input Mask Library image

Maska 是一个简单的零依赖输入掩码库

阅读文章
Add Swagger UI to Your Laravel Application image

将 Swagger UI 添加到您的 Laravel 应用程序

阅读文章
Assert the Exact JSON Structure of a Response in Laravel 11.19 image

在 Laravel 11.19 中断言响应的精确 JSON 结构

阅读文章
Build SSH Apps with PHP and Laravel Prompts image

使用 PHP 和 Laravel 提示构建 SSH 应用程序

阅读文章
Building fast, fuzzy site search with Laravel and Typesense image

使用 Laravel 和 Typesense 构建快速、模糊的网站搜索

阅读文章
Add Comments to your Laravel Application with the Commenter Package image

使用 Commenter 包为您的 Laravel 应用程序添加评论

阅读文章