Livewire 3 і 4 — що змінилося
Усе, що змінилося між Livewire 3 і Livewire 4: нові можливості, які варто взяти, кожна ламка зміна з кодом «було / стало» і чек-лист самого апгрейду. Включно з трьома змінами, які проходять ваші тести і ламають продакшен.
1. Коротко
Livewire 4 — великий реліз із невеликим шляхом міграції. Модель компонента не змінилася: PHP-клас, Blade-шаблон, стан на сервері, HTML по дроту. Змінилося те, де лежать компоненти, як вони пишуться і яку частину сторінки зачіпає одне оновлення.
Що ви отримуєте
- Однофайлові компоненти — клас і розмітка в одному файлі.
- Острови — ділянки з незалежним рендерингом, без розбиття на дочірні компоненти.
- Слоти — батько передає розмітку в нащадка, обчислюється в контексті батька.
- Оптимістичний UI —
wire:show,wire:text,wire:bindзмінюють DOM без round trip. - Drag and drop —
wire:sort, без зовнішньої бібліотеки. - Скоуплений CSS — стилі компонента, що не протікають.
Що підставить
- Незакриті теги
<livewire:…>мовчки не рендеряться. - Модифікатори
wire:modelзмінили сенс — поля перестають синхронізуватися. - Ендпоінт оновлень переїхав на
/livewire-{hash}/— ламаються інфраструктурні правила. - Модифікатори
wire:transitionприбрано. - Volt увійшов у ядро.
2. Порівняння пліч-о-пліч
| Область | Livewire 3 | Livewire 4 |
|---|---|---|
| Файли компонента | app/Livewire/Counter.php + resources/views/livewire/counter.blade.php |
resources/views/components/⚡counter.blade.php (один файл) |
| Команда створення | php artisan livewire:make Counter |
php artisan make:livewire counter |
| Маршрутизація | Route::get('/x', Dashboard::class) |
Route::livewire('/x', 'pages::dashboard') |
| Частковий рендеринг | Розбиття на дочірні компоненти | Ділянки @island |
| Слоти | Не підтримуються | {{ $slot }}, <wire:slot name="…"> |
| Drag and drop | SortableJS плюс обв’язка | wire:sort |
| Миттєвий UI | Alpine.js вручну | wire:show, wire:text, wire:bind |
| CSS компонента | Глобальний стайлшит | Скоуплений <style> у компоненті |
| Паралельні дії | Недоступні | #[Async], wire:click.async |
| Ендпоінт оновлень | /livewire/update |
/livewire-{hash}/update |
| JS-хуки | Livewire.hook('request'|'commit') |
interceptRequest(), interceptMessage() |
| Volt | Окремий пакет | Увійшов у ядро |
| Тестування | PHPUnit або Pest | Рекомендується Pest, тести можуть лежати поруч із компонентом |
3. Апгрейд за кроками
# 1. Підняти залежність
composer require livewire/livewire:^4.0
# 2. Очистити всі кеші
php artisan optimize:clear
# 3. Прогнати тести, перш ніж чіпати щось іще
php artisan testLaravel Shift автоматизує значну частину механічної роботи. Незалежно від того, користуєтеся ви ним чи ні, пройдіться потім по чек-листу нижче — мовчазні поломки інструмент відзначає найгірше.
Порядок робіт, який зазвичай проходить гладко
- Оновити пакет і очистити кеші.
- Полагодити перейменування в конфігу (розділ 4).
- Полагодити маршрути (розділ 5).
- Знайти в коді незакриті теги компонентів (розділ 6).
- Перевірити кожен
wire:modelіз модифікатором (розділ 7). - Оновити інфраструктурні правила під новий ендпоінт (розділ 8).
- Замінити прибрані модифікатори
wire:transition(розділ 9). - Мігрувати компоненти Volt, якщо він використовувався (розділ 12).
- Прогнати тести знову і пройти застосунок руками.
- І тільки потім братися за острови, слоти й однофайлові компоненти.
4. Перейменування в конфігурації
| Livewire 3 | Livewire 4 | Примітка |
|---|---|---|
layout | component_layout | Використовує namespace layouts:: |
lazy_placeholder | component_placeholder | — |
smart_wire_keys | smart_wire_keys | Умовчання змінилося з false на true |
| — | component_locations | Каталоги, де шукаються компоненти |
| — | component_namespaces | Іменовані корені, наприклад pages:: |
| — | make_command | Однофайлова чи класова генерація |
| — | csp_safe | Збірка під сувору CSP |
'component_layout' => 'layouts::app',
'component_placeholder' => 'livewire.placeholder',
'component_locations' => [
'resources/views/components',
'resources/views/livewire',
],
'component_namespaces' => [
'pages' => 'resources/views/pages',
],
// Продовжувати генерувати класові компоненти
'make_command' => [
'type' => 'class',
],5. Маршрутизація
use App\Livewire\Dashboard;
Route::get('/dashboard', Dashboard::class)
->middleware('auth')
->name('dashboard');use App\Livewire\Dashboard;
Route::livewire('/dashboard', Dashboard::class)
->middleware('auth')
->name('dashboard');
// Або за іменем компонента
Route::livewire('/dashboard', 'pages::dashboard')
->middleware('auth')
->name('dashboard');grep -rn "Route::get(.*::class" routes/.
6. Теги компонентів мають закриватися
<livewire:user-profile>
<livewire:order-list><livewire:user-profile />
<livewire:order-list /># Теги компонентів, які не самозакриті
grep -rn "<livewire:[^>]*[^/]>" resources/views/
# Форма через директиву @livewire() не зачеплена
grep -rn "@livewire(" resources/views/7. wire:model змінився двічі
7.1 Модифікатори керують синхронізацією на клієнті
<input wire:model.blur="title">
<select wire:model.change="status"><input wire:model.live.blur="title">
<select wire:model.live.change="status">7.2 Більше немає спливання від дочірніх елементів
<div wire:model="value">
<input type="text">
</div><div wire:model.deep="value">
<input type="text">
</div># Усі wire:model з модифікатором
grep -rn "wire:model\.[a-z]" resources/views/
# wire:model на не-input елементі — кандидат на проблему зі спливанням
grep -rn "<div[^>]*wire:model" resources/views/8. Ендпоінт оновлень переїхав
Усі URL Livewire тепер містять хеш: /livewire/ став /livewire-{hash}/.
Де перевірити
- Блоки
locationу nginx або Apache - Білі списки WAF і фаєрвола
- Правила обходу кешу CDN
- Rate limiter'и, прив’язані до шляху
- Правила маршрутизації балансувальника
- Моніторинг і uptime-перевірки
- Записи
connect-srcу CSP
location /livewire/ {
# …
}location ~ ^/livewire(-[a-z0-9]+)?/ {
# …
}9. wire:transition перероблено
<div wire:transition.opacity.duration.300ms>
…
</div><div wire:transition>
…
</div>::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 300ms;
}10. Зміни в PHP API
Стрімінг
$this->stream(to: '#container', content: 'Hello', replace: true);$this->stream('Hello', replace: true, el: '#container');Монтування компонента
mount($name, $params = [], $key = null)mount($name, $params = [], $key = null, $slots = [])Нові атрибути
| Атрибут | Призначення |
|---|---|
#[Async] | Виконати дію паралельно |
#[Renderless] | Пропустити перемальовування після дії |
#[Prop] | Оголосити властивість як props від батька |
#[Json] | Повернути дані прямо в JavaScript |
11. Зміни в JavaScript API
$wire.$js('showToast', { message: 'Збережено' });$wire.$js.showToast = { message: 'Збережено' };Хуки стали перехоплювачами
document.addEventListener('livewire:init', () => {
Livewire.hook('request', ({ options, fail }) => {
options.headers['X-Tenant'] = window.tenantId;
fail(({ status, preventDefault }) => {
if (status === 419) {
preventDefault();
window.location.reload();
}
});
});
});document.addEventListener('livewire:init', () => {
Livewire.interceptRequest(({ options, fail }) => {
options.headers['X-Tenant'] = window.tenantId;
fail(({ status, preventDefault }) => {
if (status === 419) {
preventDefault();
window.location.reload();
}
});
});
Livewire.interceptMessage(({ component, succeed }) => {
succeed(() => {
console.debug('компонент оновлено', component.name);
});
});
});{{-- Помилки валідації на клієнті --}}
<div x-show="$errors.has('email')" x-text="$errors.first('email')"></div>12. Volt увійшов у ядро
| Volt | Livewire 4 |
|---|---|
Livewire\Volt\Component | Livewire\Component |
Volt::route() | Route::livewire() |
Volt::test() | Livewire::test() |
| Сервіс-провайдер Volt | Видалити |
Пакет livewire/volt | Видалити залежність |
composer remove livewire/volt<?php
use Livewire\Volt\Component;
new class extends Component {
public string $title = '';
};
?>
<div>…</div><?php
use Livewire\Component;
new class extends Component {
public string $title = '';
};
?>
<div>…</div>13. Освоєння нових можливостей
1. Острови на найповільнішій сторінці
<div>
<div class="card">Виручка: {{ $this->revenue }}</div>
<div class="card">Завдань у черзі: {{ $this->queue }}</div>
<div class="card">Остання активність: …</div>
</div><div>
@island(name: 'revenue')
<div class="card">Виручка: {{ $this->revenue }}</div>
@endisland
@island(name: 'queue', poll: '5s')
<div class="card">Завдань у черзі: {{ $this->queue }}</div>
@endisland
@island(name: 'activity', lazy: true)
@placeholder
<div class="card skeleton animate-pulse h-48"></div>
@endplaceholder
<div class="card">Остання активність: …</div>
@endisland
</div>2. Замінити round trip клієнтськими директивами
<button wire:click="$toggle('showFilters')">Фільтри</button>
@if ($showFilters)
<div class="filters">…</div>
@endif<button wire:click="$toggle('showFilters')">Фільтри</button>
<div class="filters" wire:show="showFilters">…</div>3. Викинути SortableJS
<ul wire:sort="reorder">
@foreach ($tasks as $task)
<li wire:sort:item="{{ $task->id }}" wire:key="task-{{ $task->id }}">
<span wire:sort:handle>⠿</span> {{ $task->title }}
</li>
@endforeach
</ul>4. Схлопнути дочірні компоненти, створені лише заради рендеру
Якщо в нащадка немає власного стану і він не перевикористовується, острів у батькові легший — один компонент, один клас, один snapshot.
5. #[Renderless] для невидимої роботи
use Livewire\Attributes\Renderless;
#[Renderless]
public function trackView(): void
{
$this->post->increment('views');
}6. Переведення на однофайловий формат — останнім
Це косметика. Робіть принагідно, коли й так правите файл, а не окремим спринтом міграції.
14. Чек-лист апгрейду
До початку
- Тести проходять на Livewire 3.
- Окрема гілка і бекап бази, якщо оточення спільне.
- Хтось готовий пройти застосунок руками після — мовчазні поломки бачить лише людина.
Механічні правки
composer require livewire/livewire:^4.0php artisan optimize:clearlayout→component_layoutlazy_placeholder→component_placeholderRoute::get(…, Component::class)→Route::livewire(…)- Усі теги
<livewire:…>самозакриті wire:model.blur/.change→ додати.livewire:modelна обгортці → додати.deepwire:scroll→wire:navigate:scroll- Модифікатори
wire:transition→ у CSS - Порядок аргументів
$this->stream() $wire.$js('name', …)→$wire.$js.name = …Livewire.hook(…)→interceptRequest()/interceptMessage()- Класи, маршрути, тести і пакет Volt видалені
Інфраструктура
- Location-блоки nginx / Apache розширені під
/livewire-{hash}/ - Правила WAF, CDN і фаєрвола оновлені
- Rate limiter'и і моніторинг перенацілені
connect-srcу CSP переглянутий
Перевірка
- Тести зелені
- Усі форми надсилаються і валідуються
- Завантаження файлів працює, включно з індикатором прогресу
- Таблиці гортаються, сортуються і фільтруються
- Модальні вікна відкриваються і закриваються
- Сторонні віджети всередині
wire:ignoreініціалізуються - Консоль браузера чиста на основних сценаріях
- Запити оновлення повертають 200 на проді, а не 403 від WAF
Офіційні ресурси
- Upgrade guide: livewire.laravel.com/docs/upgrading
- Документація Livewire: livewire.laravel.com/docs