Вчора команда Laravel випустила Laravel Folio - потужний сторінковий маршрутизатор, призначений для спрощення маршрутизації в додатках Laravel. Сьогодні вони випустили Volt - елегантно створений функціональний API для Livewire, що дозволяє логіці 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
Для фіктивних даних я збираюся створити модель суші. Суші - це пакет, написаний Калебом Поціо, який дозволяє створювати красномовні моделі, які запитують свої дані з масиву, написаного безпосередньо у файлі моделі. Це чудово працює, коли ви створюєте приклади додатків або маєте дані, які не потрібно часто змінювати.
Створіть модель, потім видаліть ознаку HasFactory
і замініть її ознакоюSushi
. Я додав деталі 4 останніх епізодів подкасту Laravel News як дані для цього прикладу.
Я не буду вдаватися в подробиці про те, як все це працює, оскільки це не суть статті, і ви, ймовірно, скористаєтеся справжньою красномовною моделлю, якщо створите власний програвач подкастів.
<?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 автоматично вводять свої сценарії та стилі зараз, нам навіть не потрібно завантажувати їх у макет! Ми створимо макет як анонімний компонент Blade на .resources/views/components/layout.blade.php
<!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
змінної, яка запускає запит, щоб отримати всі моделі епізодів ($episodes = computed(fn () => Episode::get());
). Ми можемо отримати доступ до обчисленої властивості в шаблоні за допомогою $this->episodes
.
Я також створив $formatDuration
змінну, яка є функцією для форматування властивості кожного епізоду duration_in_seconds
у формат, придатний для читання. Ми можемо викликати цю функцію в шаблоні за допомогою $this->formatDuration($episode->duration_in_seconds)
.
Нам також потрібно обернути динамічну функціональність на сторінці в директиву@volt
, щоб зареєструвати її як "анонімний компонент Livewire" на сторінці Folio.
<?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 автоматично запитає моделі епізодів для епізоду з номером, який ми передаємо в URL-адресі, і зробить його доступним як $episode
змінну в нашому <?php ?>
коді. Потім ми можемо перетворити це на властивість Livewire за допомогою функції Вольтаstate
.
Ми також додамо кнопку відтворення на цій сторінці, щоб користувачі могли відтворювати епізод під час перегляду його деталей.
<?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>
·
<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, а потім чарівним чином замінить вміст у вашому браузері, не виконуючи повного перезавантаження сторінки. Це робить завантаження сторінок неймовірно швидким і дозволяє працювати таким функціям, як persist! Це дозволяє функції, які раніше ви могли досягти, лише побудувавши SPA.
Висновок Це
був дійсно веселий демонстраційний додаток, який можна створити під час вивчення Вольта та Фоліо. Я завантажив демонстраційний додаток сюди і @bosunski створив phpsandbox, якщо ви хочете побачити повний вихідний код або спробувати його самостійно!
Як ти гадаєш? Чи є Livewire v3 + Volt + Folio найпростішим стеком для створення програм Laravel зараз? Я думаю, що це дійсно круто і може здатися більш знайомим людям, які звикли створювати додатки в JavaScript фреймворках.js таких як Next і Nuxt.js. Також приємно мати весь свій код для сторінки - стиль (через Tailwind), JS (через Alpine) та бекенд-код в одному файлі. Надішліть мені свої думки у Twitter!