• Час читання ~9 хв
  • 28.07.2023

Вчора команда 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>
                                &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 автоматично запитає моделі епізодів для епізоду з номером, який ми передаємо в 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>
                            &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, а потім чарівним чином замінить вміст у вашому браузері, не виконуючи повного перезавантаження сторінки. Це робить завантаження сторінок неймовірно швидким і дозволяє працювати таким функціям, як persist! Це дозволяє функції, які раніше ви могли досягти, лише побудувавши SPA.

Висновок Це

був дійсно веселий демонстраційний додаток, який можна створити під час вивчення Вольта та Фоліо. Я завантажив демонстраційний додаток сюди і @bosunski створив phpsandbox, якщо ви хочете побачити повний вихідний код або спробувати його самостійно!

Як ти гадаєш? Чи є Livewire v3 + Volt + Folio найпростішим стеком для створення програм Laravel зараз? Я думаю, що це дійсно круто і може здатися більш знайомим людям, які звикли створювати додатки в JavaScript фреймворках.js таких як Next і Nuxt.js. Також приємно мати весь свій код для сторінки - стиль (через Tailwind), JS (через Alpine) та бекенд-код в одному файлі. Надішліть мені свої думки у Twitter!

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

Про автора CrazyBoy49z
WORK EXPERIENCE
Контакти
Ukraine, Lutsk
+380979856297