談談 Livewire 的 Json Attribute
Livewire 4 在前幾天終於正式發佈啦!這次同樣帶來了翻天覆的的改變 😆,最大的亮點莫過於 SFC(Single-file Component) 了。現在你可以將前後端的邏輯都寫在同一個檔案中,讓開發體驗更接近現代化的前端框架。
<?php
use Livewire\Component;
new class extends Component
{
// 這裡可以寫後端邏輯
};
?>
<div>
{{-- 這裡寫前端模板 --}}
</div>如果你還是習慣將前後端邏輯分成兩個檔案,Livewire 4 依然支援 MFC(Multi-file Component)的模式。
雖然 Livewire 4 正式版最近才上線,但早在去年還在 Beta 階段的時候,我就已經將自己的部落格升級到 4 了,並幫忙回報一些 Bug 🐛。正式版除了原先 Beta 版有的新功能,還加入了一些 Beta 版沒有的新功能,其中最讓我眼前一亮的,就是新的 #[Json] Attribute。
下面是官方文件示範如何使用 #[Json] Attribute 實作常見的文章搜尋功能:
<?php
declare(strict_types=1);
use App\Models\Post;
use Illuminate\Database\Eloquent\Collection;
use Livewire\Attributes\Json;
use Livewire\Component;
new class extends Component
{
// 將這個方法標註為 json endpoint
// 你可以在前端使用 $wire.search() 呼叫這個方法,並取得回傳值
// [!code highlight:1]
#[Json]
public function search(string $query): Collection
{
// json endpoint 的回傳值會被轉為前端可以直接使用的 json 格式
return Post::search($query)
->take(10)
->get();
}
};
?>
@script
<script>
// 定義一個 alpine component
Alpine.data('searchComponent', () => ({
query: '',
posts: [],
onInput() {
// [!code highlight:5]
// 呼叫後端的 search() 方法取得文章搜尋結果
$wire.search(this.query)
// 回傳的結果為 Promise
// 可以使用 then() 或是 await 取得 JSON 格式的資料
.then(data => this.posts = data)
}
}));
</script>
@endscript
{{-- 使用剛剛定義的 alpine component --}}
<div x-data="searchComponent">
<input
type="text"
x-model="query"
x-on:input.debounce="onInput"
placeholder="搜尋文章..."
>
<ul>
{{-- 使用迴圈顯示文章搜尋結果 --}}
<template x-for="post in posts" :key="post.id">
<li x-text="post.title"></li>
</template>
</ul>
</div>簡單來說,#[Json] Attribute 可以將 Livewire 組件中的方法變為 JSON 端點(JSON Endpoint)。當你在前端透過 $wire 調用此方法時,Livewire 會直接回傳用 Promise 包裝好的 JSON 格式資料,而且不會觸發組件的重新渲染(Re-rendering)流程。
有點像是 SvelteKit 的 Remote Function 功能。前端可以呼叫後端的方法並取得資料。
如果你的方法中有使用到 Validation 的話,那麼在 Validation 失敗的時候,Livewire 也會回傳 JSON 格式的錯誤訊息。
{
"status": 422, // HTTP 狀態碼 (validation 失敗會回傳 422)
"body": null, // 原始回應的內容 (validation 失敗會回傳 null)
"json": null, // JSON 格式的回應 (validation 失敗會回傳 null)
"errors": {...} // Validation 的錯誤訊息
}你可以使用 catch() 來接收錯誤訊息並進行對應的處理。
$wire.save()
.then((data) => {
// 處理成功回應
console.log(data);
})
.catch((e) => {
if (e.status === 422) {
// 處理 validation 失敗
console.log(e.errors);
}
});可以看到掛上 #[Json] Attribute 後,前端與後端交互方式就會變成我們很熟悉的模式 - 使用 JSON 格式的資料進行交互。
提升 Livewire 效能的方式,就是不使用 Livewire!?
#[Json] Attribute 之所以讓我眼前一亮,是因為近年來我在關注 Livewire 相關新聞時,有注意到一個關於優化 Livewire 的趨勢。實際上,隨著 Livewire 的普及,關於它的優缺點與效能優化的討論也越來越多。例如這篇在 Reddit 上關於 Livewire 優缺點的討論中,有一位開發者的建議獲得了廣泛認同。
他提到,如果你的畫面上有一個區塊需要根據狀態頻繁切換顯示,盡量不要這樣寫:
@if ($isEnabled)
<div>content here</div>
@endif;因為這會觸發 Livewire 組件的更新頁面流程。他建議使用 Alpine.js 的 x-show,效能會好得多:
<div x-cloak x-show="$wire.isEnabled">
content here
</div>Laravel 的開發者關係工程師 Josh Cirre,在看到這個討論串後有製作一部影片分享了自己的看法。
他提到,任何工具都有適合與不適合的場景,連 Livewire 也不例外。雖然 Livewire 3 透過合併同一頁面上的多個組件的請求,大幅度減少向伺服器發送請求的次數,但是如果連細微的 UI 互動都要依賴伺服器端的狀態,那麼這種使用的數量一多,就很難不影響應用的效能。
在影片的留言區中,Filament 的開發者關係工程師 Alex Six 提到,他們為了提升 Filament 4 的效能花了很多心力,而核心的策略就是:盡可能的使用 Alpine.js。

無獨有偶,Laravel Cloud 的核心開發者 Ryan Chandler,也在去年的 Wire Live 議程中分享如何在開發 Livewire 應用時充分利用 Alpine.js 執行前端渲染來提升效能。
這些效能的改善的建議背後都有一個共同點,那就是:
盡量避免 Livewire 組件重新渲染頁面。
原因很簡單,一旦觸發 Livewire 組件的更新頁面流程,伺服器回應的 Payload 就會包含一大段更新後的 HTML:
{
"components": [
{
"snapshot": "...",
"effects": {
"returns": [null],
// [!code highlight:1]
"html": "一大片更新後的 HTML..."
}
}
],
"assets": []
}當互動頻率高或 DOM 結構複雜時,相較於單純的 JSON 資料,這種包含大量 HTML 的 Payload 顯然會造成較大的效能負擔。
魚與熊掌可以兼得,#[Json] 的妙用
官方文件對於使用 #[Json] 的建議如下:
- 實作搜尋建議 (Autocomplete)。
- 為前端圖表或第三方套件加載動態數據。
- 任何只需要數據而不需要更新 HTML 結構的互動。
但我感覺用途遠不只有這些,有一種比較極端的用法,任何需要跟後端取得資料的地方,都使用 #[JSON] Attribute,前端只跟後端拿簡易的 JSON 格式資料,並使用 Alpine.js 根據資料渲染頁面。藉由不觸發 Livewire 重新渲染,把效能最大化。這樣的使用方式就好像我們熟悉的純 API + 前端框架的模式一般。
但仔細一想,這樣好像就喪失了 Livewire 最大的魅力,也就是用 Laravel 的方式去開發前端,既不用煩惱前端該如何與後端進行資料交換,也不用煩惱該如何更新前端頁面。
我覺得 #[Json] Attribute 出現的意義,就是它能讓我們在 Livewire 組件內定義「輕量級的 API 端點」。讓我們在不需要重新渲染頁面的前提下,更方便的與後端進行資料上的交換,同時也不需要特地去寫一支獨立的 API Controller。
總結來說,#[Json] 讓 Livewire 組件能更靈活的的在「伺服器端渲染」與「客戶端渲染」之間切換,是開發高效能 Livewire 應用的新利器。