Middle: питання на співбесіді з теми «Події, Alpine і навігація»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
Події Livewire - це події браузера, тож їх бачать і Alpine, і будь-який JavaScript на сторінці.
З PHP у JavaScript. Подія, відправлена з компонента, доходить у браузер:
$this->dispatch('notify', message: 'Збережено', type: 'success');
{{-- Alpine-компонент сповіщень у макеті --}}
<div x-data="{ items: [] }"
x-on:notify.window="items.push($event.detail)">
...
</div>
// звичайний JavaScript
Livewire.on('notify', ({ message, type }) => showToast(message, type));
З JavaScript у Livewire. Компонент з #[On('cart-updated')] отримає подію, відправлену будь-звідки:
<button x-on:click="$dispatch('cart-updated', { count: 3 })">...</button>
Livewire.dispatch('cart-updated', { count: 3 });
Livewire.dispatchTo('cart-badge', 'cart-updated', { count: 3 });
У скрипті компонента (Livewire 4: тег <script> у шаблоні, this - це $wire):
<script>
this.$on('post-created', () => { /* ... */ });
this.$dispatchSelf('refresh');
</script>
Події браузера на корені компонента - wire:назва-події:
<div wire:custom-event="handle">
<button x-on:click="$dispatch('custom-event')">...</button> {{-- спливе до кореня --}}
</div>
<div wire:custom-event.window="handle"> {{-- подія з будь-якого місця сторінки --}}
Безпека - головне. Слухач #[On] - це метод, який будь-хто може викликати з консолі браузера з довільними параметрами: Livewire.dispatch('order-paid', { orderId: 5 }). Тож слухачі мають перевіряти права й дані так само, як дії. Подія order-paid, що сама по собі змінює статус замовлення, - діра.
Прибирання слухачів. Livewire.on() повертає функцію для відписки. В Alpine-компонентах, що живуть із wire:navigate, слухачі треба знімати в destroy(), інакше після кожного переходу вони накопичуватимуться:
Alpine.data('cartBadge', () => ({
off: null,
init() { this.off = Livewire.on('cart-updated', ({ count }) => (this.count = count)); },
destroy() { this.off?.(); },
}));
Трансляція з сервера (Echo) - слухачі виду #[On('echo:orders,OrderShipped')] отримують події WebSocket від Laravel Reverb чи Pusher без власного JavaScript.
При SPA-переході через wire:navigate вміст <body> замінюється новим. Але деякі елементи не повинні перестворюватися: аудіо- чи відеоплеєр, що грає, чат-віджет, бічна панель з прокручуванням.
@persist позначає елемент, який Livewire переносить на нову сторінку замість заміни:
{{-- resources/views/layouts/app.blade.php --}}
<body>
<main>{{ $slot }}</main>
@persist('player')
<audio src="{{ $episode->file }}" controls></audio>
@endpersist
@livewireScripts
</body>
Якщо на новій сторінці є @persist з тією самою назвою, Livewire бере існуючий DOM-елемент зі старої сторінки й ставить на місце нового. Відтворення не переривається, зберігаються стан Alpine, обробники подій і значення полів.
Умови:
- працює лише з
wire:navigate- при звичайному переході чи оновленні сторінки все завантажується заново; - елемент з тією самою назвою має бути на обох сторінках. Найпростіше - розмістити його в спільному макеті;
- розміщувати поза Livewire-компонентами сторінки, зазвичай прямо в макеті. Вміст, який має оновлюватися з сервера, не варто «заморожувати».
Прокрутка всередині збереженого елемента (довга бічна навігація) - wire:navigate:scroll:
@persist('sidebar')
<aside class="overflow-y-auto" wire:navigate:scroll>...</aside>
@endpersist
У Livewire 3 для цього був wire:scroll - при оновленні до v4 його треба перейменувати.
Підводні камені:
- збережений елемент не оновлюється. Якщо в ньому показано «Активний пункт меню» чи лічильник, після переходу вони лишаться старими. Активне посилання в збереженій навігації позначається через
data-current/wire:current- Livewire оновлює їх при навігації; - назва має бути унікальною й стабільною - різні елементи з однією назвою на різних сторінках підмінять один одного;
- персональні дані в збереженому елементі (наприклад, після виходу з облікового запису через
wire:navigate) переживуть перехід - вихід краще робити повним перезавантаженням сторінки.
Сторонні бібліотеки (редактори тексту, вибір дати, карти, графіки) самі змінюють DOM: додають елементи, класи, атрибути. Після наступного запиту Livewire морфить DOM за новим HTML із сервера - і затирає все, що побудувала бібліотека.
wire:ignore каже Livewire не чіпати вміст елемента при оновленнях:
<div wire:ignore>
<div x-data x-init="initEditor($el)"></div>
</div>
wire:ignore.self - ігнорувати зміни лише атрибутів самого елемента, а вміст оновлювати як звичайно.
Типовий шаблон інтеграції через Alpine:
<div wire:ignore
x-data="{ value: $wire.entangle('content') }"
x-init="
const editor = new Editor($refs.editor, { content: value });
editor.on('change', () => $wire.content = editor.getHTML());
">
<div x-ref="editor"></div>
</div>
Краще без entangle - напряму через $wire:
<div wire:ignore
x-data
x-init="
const picker = flatpickr($refs.input, {
defaultDate: $wire.date,
onChange: (dates, str) => $wire.date = str,
});
">
<input x-ref="input" type="text">
</div>
Що варто врахувати:
- напрям «сервер → бібліотека»: якщо сервер змінив значення (скинули форму), бібліотека під
wire:ignoreпро це не знає. Потрібна подія ($this->dispatch('editor-reset')) чи$wire.$watch('date', ...)в Alpine; - частота запитів:
$wire.content = ...лише змінює значення в браузері, на сервер воно піде з наступною дією.$wire.set('content', value)надсилає запит одразу - для редактора тексту це запит на кожне натискання клавіші; - знищення: бібліотеки, що підписуються на
window/document, мають прибиратися вdestroy()Alpine-компонента - інакше зwire:navigateслухачі накопичуються; wire:key, якщо таких полів кілька в циклі, - щоб при зміні списку бібліотека не опинилася «прив'язаною» до чужого рядка;- власне поле з
wire:model: обгортку можна оформити Blade-компонентом зx-modelable, тоді в шаблонах вистачить<x-date-picker wire:model="date" />.
Альтернатива - бібліотеки, створені для Livewire/Alpine (Flux, Alpine-плагіни), які вже вміють жити з морфінгом.
Livewire дає два способи виконати JavaScript, і вони вирішують різні задачі.
1. #[Js] - метод, що виконується в браузері без запиту. PHP-метод повертає рядок JavaScript; при виклику з шаблону сервер не задіюється:
use Livewire\Attributes\Js;
new class extends Component {
public string $title = '';
public string $content = '';
#[Js]
public function clearForm(): string
{
return <<<'JS'
$wire.title = ''
$wire.content = ''
JS;
}
};
<button type="button" wire:click="clearForm">Очистити</button>
Очищення полів відбувається миттєво. Код JavaScript потрапляє в сторінку під час рендеру, тож на клік не потрібен мережевий запит.
2. $this->js() - виконати JavaScript після дії на сервері:
public function save(): void
{
$this->form->save();
$this->js("document.getElementById('comment-form').reset()");
$this->js('$wire.$refresh()');
}
Код ставиться в чергу й виконується, коли відповідь прийде в браузер.
3. Скрипти в шаблоні (Livewire 4): тег <script> у шаблоні однофайлового чи багатофайлового компонента виконується з this = $wire, без обгортки @script:
<script>
this.$js.focusSearch = () => this.$refs.search.focus();
</script>
<input wire:ref="search">
<button type="button" wire:click="$js.focusSearch">Пошук</button>
Такі скрипти віддаються окремими кешованими файлами.
Коли що:
- суто клієнтська дія (очистити, згорнути, сфокусуватися) -
#[Js]чи$jsу скрипті компонента; - реакція на результат серверної дії (прокрутити до нового коментаря, закрити вікно, сповіщення) -
$this->js()або подія, яку слухає JavaScript; - переважно клієнтська логіка - Alpine, а не JavaScript, згенерований у PHP.
Пастки:
- не вставляйте дані користувача в рядок JavaScript -
$this->js("alert('{$this->name}')")з ім'ям'); stealCookies(); ('виконає чужий код. Дані передавайте через подію з параметрами чиJs::from($value)для безпечного екранування; - CSP: з увімкненим
csp_safeскладні вирази JavaScript у директивах обмежені - перевірте сумісність, якщо сайт використовує суворий Content Security Policy; #[Js]-метод не має доступу до PHP-стану на момент кліку - лише до того, що є в$wireу браузері.