Wczoraj zespół Laravel wydał Laravel Folio - potężny router stronicowy zaprojektowany w celu uproszczenia routingu w aplikacjach Laravel. Dzisiaj wydali Volt - elegancko wykonane funkcjonalne API dla Livewire, umożliwiające współistnienie logiki PHP komponentu i szablonów Blade w tym samym pliku przy zmniejszonej liczbie wzorców.
Chociaż mogą być używane osobno, myślę, że używanie ich razem jest nowym, niezwykle produktywnym sposobem tworzenia aplikacji Laravel.
W tym artykule nauczę Cię, jak zbudować prostą aplikację, która wyświetla odcinki podcastu Laravel News i pozwala użytkownikom je odtwarzać, za pomocą odtwarzacza, który może płynnie kontynuować odtwarzanie na wszystkich załadunkach stron.
Konfiguracja Livewire, Volt i Folio Aby rozpocząć, musimy utworzyć nową aplikację Laravel i zainstalować Livewire, Volt, Folio
i Sushi (aby utworzyć fałszywe dane).
laravel new
composer require livewire/livewire:^3.0@beta livewire/volt:^1.0@beta laravel/folio:^1.0@beta calebporzio/sushi
Livewire v3, Volt i Folio są nadal w wersji beta. Powinny być dość stabilne, ale używaj ich na własne ryzyko.
Po wymaganiu pakietów musimy uruchomić php artisan volt:install
i php artisan folio:install
. Spowoduje to rusztowanie niektórych folderów i dostawców usług, których potrzebują Volt i Folio.
Model Episode
W przypadku danych atrapy stworzę model sushi. Sushi to pakiet napisany przez Caleba Pozio, który pozwala tworzyć modele Eloquent, które wysyłają zapytania do swoich danych z tablicy zapisanej bezpośrednio w pliku modelu. Działa to znakomicie, gdy tworzysz przykładowe aplikacje lub masz dane, które nie muszą się często zmieniać.
Utwórz model, a następnie usuń cechę HasFactory
i zastąp ją cechąSushi
. Dodałem szczegóły 4 najnowszych odcinków Laravel News Podcast jako dane dla tego przykładu.
Nie będę wchodził w szczegóły, jak to wszystko działa, ponieważ nie o to chodzi w artykule, i prawdopodobnie użyjesz prawdziwego modelu eloquent, jeśli miałbyś zbudować własny odtwarzacz podcastów.
<?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',
],
// ...
];
}
Widok
układu Będziemy potrzebować pliku układu, aby załadować Tailwind, dodać logo i dodać podstawowe stylizacje. Ponieważ Livewire i Alpine automatycznie wstrzykują teraz swoje skrypty i style, nie musimy nawet ładować ich w układzie! Utworzymy układ jako anonimowy komponent Blade w .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>
Strona
z listą odcinków Po pierwsze, potrzebujemy strony, aby wyświetlić wszystkie odcinki podcastu.
Korzystając z Folio, możemy łatwo utworzyć nową stronę w resources/views/pages
katalogu, a Laravel automatycznie utworzy trasę dla tej strony. Chcemy, aby nasza trasa była /episodes
, abyśmy mogli biecphp artisan make:folio episodes/index
. Spowoduje to utworzenie pustego widoku w formacie resources/views/pages/episodes/index.blade.php
.
Na tej stronie wstawimy komponent układu, a następnie zapętlimy wszystkie odcinki podcastu. Volt zapewnia funkcje namespaced dla większości funkcji Livewire. Tutaj otworzymy zwykłe <?php ?>
tagi otwierania i zamykania. Wewnątrz nich użyjemy computed
funkcji do utworzenia zmiennej$episodes
, która uruchamia zapytanie w celu uzyskania wszystkich modeli odcinków ($episodes = computed(fn () => Episode::get());
). Możemy uzyskać dostęp do obliczonej właściwości w szablonie za pomocą $this->episodes
.
Stworzyłem $formatDuration
również zmienną, która jest funkcją formatowania właściwości każdego odcinka duration_in_seconds
do czytelnego formatu. Możemy wywołać tę funkcję w szablonie za pomocą $this->formatDuration($episode->duration_in_seconds)
.
Musimy również owinąć dynamiczną funkcjonalność na stronie w dyrektywie@volt
, aby zarejestrować ją jako "anonimowy komponent Livewire" na stronie 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>
Odtwarzacz
odcinków Stamtąd musimy dodać trochę interaktywności. Chcę dodać odtwarzacz odcinków, abyśmy mogli słuchać odcinków z listy odcinków. Może to być zwykły komponent Blade, który renderujemy w pliku układu.
<!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>
Możemy utworzyć ten komponent, dodając resources/views/components/episode-player.blade.php
plik. Wewnątrz komponentu dodamy <audio>
element z kodem alpejskim do przechowywania aktywnego odcinka oraz funkcję, która aktualizuje aktywny odcinek i uruchamia dźwięk. Pokażemy graczowi tylko wtedy, gdy aktywny odcinek jest ustawiony, a do opakowania dodamy ładne przejście zanikania.
<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>
Jeśli ponownie załadujemy stronę, nie zobaczymy żadnych zmian. To dlatego, że nie dodaliśmy sposobu odtwarzania odcinków. Będziemy używać zdarzeń do komunikowania się z naszymi komponentami Livewire do gracza. Najpierw w odtwarzaczu dodamyx-on:play-episode.window="play($event.detail)"
, aby nasłuchiwać play-episode
zdarzenia w oknie, a następnie wywołać play
funkcję.
<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>
Następnie na stronie dodamy słuchacza kliknięć na episodes/index
przyciskach odtwarzania dla każdego odcinka. Przyciski wyślą play-episode
zdarzenie, które zostanie odebrane przez gracza odcinka i tam obsłużone.
<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>
Strona
szczegółów odcinka Następnie chcę dodać stronę szczegółów odcinka, aby wyświetlić notatki z każdego odcinka i inne szczegóły.
Folio ma kilka całkiem fajnych konwencji wiązania modelu trasy w nazwach plików. Aby utworzyć równoważną trasę dla /episodes/{episode:id}
, utwórz stronę w formacie resources/views/pages/episodes/[Episode].blade.php
. Aby użyć parametru route innego niż klucz podstawowy, można użyć składni [Model:some_other_key].blade.php
w nazwie pliku. Chcę użyć numeru odcinka w adresie URL, więc utworzymy plik pod adresem resources/views/pages/episodes/[Episode:number].blade.php
.
Folio automatycznie odpyta modele odcinków o numer, który przekazujemy w adresie URL i udostępni jako zmienną $episode
w naszym <?php ?>
kodzie. Następnie możemy przekonwertować to na właściwość Livewire za pomocą funkcji Voltastate
.
Na tej stronie umieścimy również przycisk odtwarzania, aby użytkownicy mogli odtwarzać odcinek, wyświetlając jego szczegóły.
<?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>
Teraz musimy połączyć się ze stroną szczegółów ze strony indeksu. Wróćmy na episodes/index
stronę, zawińmy każdy odcinek <h2>
w znacznik kotwicy.
@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
Tryb
SPA Jesteśmy prawie na miejscu. Aplikacja wygląda całkiem nieźle i działa dobrze, ale jest jeden problem. Jeśli użytkownik słucha odcinka i przejdzie na inną stronę, odtwarzacz odcinków utraci aktywny stan odcinka i zniknie.
Na szczęście Livewire ma teraz dyrektywę wire:navigate
@persist
, która pomoże w rozwiązaniu tych problemów!
W naszym pliku układu zawińmy logo i odtwarzacz odcinków w @persist
bloki. Livewire wykryje to i pominie ponowne renderowanie tych bloków, gdy zmienimy strony.
<!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>
Na koniec musimy dodać wire:navigate
atrybut do wszystkich linków za pośrednictwem aplikacji. Na przykład:
<a
href="/episodes/{{ $episode->number }}"
class="transition hover:text-[#FF2D20]"
wire:navigate
>
<h2>
No. {{ $episode->number }} -
{{ $episode->title }}
</h2>
</a>
Gdy używasz atrybutuwire:navigate
, za kulisami Livewire pobierze zawartość nowej strony za pomocą AJAX, a następnie magicznie zamieni zawartość w przeglądarce bez ponownego ładowania całej strony. To sprawia, że ładowanie strony jest niesamowicie szybkie i umożliwia działanie funkcji takich jak persist! Włącza funkcje, które wcześniej można było osiągnąć tylko poprzez zbudowanie SPA.
Wniosek:
To była naprawdę fajna aplikacja demonstracyjna do zbudowania podczas nauki Volt i Folio. Przesłałem tutaj aplikację demonstracyjną i @bosunski stworzyłem phpsandbox, jeśli chcesz zobaczyć pełny kod źródłowy lub wypróbować go sam!
Co myślisz? Czy Livewire v3 + Volt + Folio to teraz najprostszy stos do tworzenia aplikacji Laravel? Myślę, że jest to naprawdę fajne i może wydawać się bardziej znajome dla ludzi, którzy są przyzwyczajeni do tworzenia aplikacji w frameworkach JavaScript, takich jak Next.js i Nuxt.js. Miło jest również mieć cały kod strony kolokowany - stylizację (przez Tailwind), JS (przez Alpine) i kod backendu w jednym pliku. Wyślij mi swoje przemyślenia na Twitterze!