文件 #5
進行中模組 4:Laravel 13 + Livewire 3 進階整合(8–10 小時)
0%
概述
- Full Page Components 與 Navigation(
wire:navigate實現 SPA-like 體驗)。 - 巢狀 Components(Nested Components)與事件傳遞。
- 授權與安全性(Policies、Gates、Livewire 安全最佳實務)。
- Laravel 13 新功能整合:
- Laravel AI SDK(文字生成、Embeddings、向量搜尋、Agents)。
- Passkey 無密碼登入。
- Semantic / Vector Search 結合 Eloquent。
- JSON:API Resources 輸出。
- Cache::touch()、Reverb(WebSocket)資料庫驅動。
- Pagination + Livewire Tables。
- 實作:帶即時搜尋與 AI 輔助的 CRUD 系統。
JH 是由 Jeffery Hsu 於 3 天 前更新
- 追蹤標籤 從 功能 變更為 文件
JH 是由 Jeffery Hsu 於 2 天 前更新
- 狀態 從 新建立 變更為 已解決
進入 模組 4:Laravel 13 + Livewire 3 進階整合!這個模組的內容非常進階且涵蓋了 Laravel 13 最新釋出的強大火力。為了確保你能扎實地理解並實作成功,我將這個龐大的模組分為 三個部分 來進行。
今天我們從 第一部分 開始,專注於:Full Page Components (全頁面元件)、巢狀元件通訊 (Nested Components),以及極為重要的授權與安全性 (Authorization & Security)。
第一步:Full Page Components 與 SPA 導航¶
在模組 3 中,我們是將 Livewire 元件嵌在 Blade 視圖中 (<livewire:counter />)。但其實 Livewire 更強大的作法是直接取代 Controller,成為 全頁面元件 (Full Page Components)。
1. 設定路由直接指向 Livewire 元件
打開 routes/web.php:
use App\Livewire\PostList;
use App\Livewire\PostDetail;
use Illuminate\Support\Facades\Route;
// 不再需要 Controller,直接把路由指派給 Livewire 類別
Route::get('/posts', PostList::class)->name('posts.index');
Route::get('/posts/{post}', PostDetail::class)->name('posts.show');
2. 元件的 Layout 設定
當 Livewire 作為全頁面渲染時,它預設會去尋找 resources/views/components/layouts/app.blade.php,並將自己注入到 {{ $slot }} 中。如果你想為特定元件指定不同的 Layout 或 Title,可以使用 PHP Attributes:
打開 app/Livewire/PostList.php:
namespace App\Livewire;
use Livewire\Component;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
#[Layout('layouts.guest')] // 指定使用不同的 Layout 檔案
#[Title('所有文章列表')] // 動態設定網頁 <title>
class PostList extends Component
{
public function render()
{
return view('livewire.post-list');
}
}
3. wire:navigate 實現 SPA 體驗
搭配我們在模組 3 提到的 wire:navigate。當你從 PostList 點擊連結到 PostDetail 時:
<a href="{{ route('posts.show', $post->id) }}" wire:navigate>
查看詳情
</a>
這會攔截標準的瀏覽器跳轉,使用 AJAX 抓取新頁面內容並平滑替換,達到真正的 SPA (Single Page Application) 體驗。
第二步:巢狀元件 (Nested Components) 與狀態同步¶
當你的應用變複雜時,你會在一個大元件裡面包著小元件(例如:文章列表中包含單一文章卡片元件)。Livewire 3 對巢狀元件的效能與資料傳遞做了巨大升級。
1. Reactive Props (反應式屬性)
預設情況下,父元件傳遞給子元件的資料是「單向且不會隨父元件更新」的。如果希望父元件資料改變時,子元件也跟著變,需要使用 #[Reactive]。
子元件 (app/Livewire/PostCard.php):
namespace App\Livewire;
use Livewire\Component;
use Livewire\Attributes\Reactive;
use App\Models\Post;
class PostCard extends Component
{
#[Reactive] // 宣告這個屬性會隨著父元件變動而自動更新
public Post $post;
public function render()
{
return view('livewire.post-card');
}
}
父元件視圖 (resources/views/livewire/post-list.blade.php):
<div>
@foreach($posts as $post)
<livewire:post-card :post="$post" :key="$post->id" />
@endforeach
</div>
2. Modelable (子元件雙向綁定)
如果你做了一個自訂的「日期選擇器」子元件,希望它能像原生 <input wire:model="date"> 一樣和父元件雙向綁定,可以使用 #[Modelable]。
// 子元件中:
use Livewire\Attributes\Modelable;
#[Modelable]
public $value = '';
第三步:授權與安全性 (Policies, Gates 與 Livewire 安全實務)¶
安全性是 Livewire 開發中最容易被忽略,但也最致命的一環。 因為 Livewire 的屬性可以從前端透過瀏覽器開發者工具 (DevTools) 輕易竄改。
1. 鎖定敏感屬性 #[Locked] (極度重要)
假設你有一個更新使用者資料的元件:
namespace App\Livewire;
use Livewire\Component;
use Livewire\Attributes\Locked;
use App\Models\User;
class UpdateProfile extends Component
{
#[Locked] // 防止駭客在前端把 userId 改成 1 (Admin) 藉此竄改管理員資料
public int $userId;
public string $name;
public function mount(User $user)
{
$this->userId = $user->id;
$this->name = $user->name;
}
public function save()
{
// 如果沒有 #[Locked],這裡的 $this->userId 可能已經被惡意竄改
$user = User::find($this->userId);
$user->update(['name' => $this->name]);
}
}
2. 結合 Laravel Policies 與 #[Authorize]
我們在模組 2 建立了 Post 模型。現在我們只允許「文章的作者」才能刪除文章。
首先,建立 Policy:
php artisan make:policy PostPolicy --model=Post
在 app/Policies/PostPolicy.php 中設定規則:
public function delete(User $user, Post $post): bool
{
// 只有當使用者的 ID 等於文章的 user_id 時才允許刪除
return $user->id === $post->user_id;
}
接下來,在 Livewire 元件中套用這個 Policy。Livewire 3 提供了非常優雅的 #[Authorize] 寫法:
namespace App\Livewire;
use Livewire\Component;
use Livewire\Attributes\Authorize;
use App\Models\Post;
class EditPost extends Component
{
public Post $post;
// 驗證當前登入者是否有權限更新這篇特定的文章
#[Authorize('update', 'post')]
public function update()
{
// 如果無權限,Laravel 會直接拋出 403 錯誤,並阻斷執行
$this->post->save();
}
// 另一種寫法:在方法內部使用 $this->authorize()
public function delete()
{
$this->authorize('delete', $this->post);
$this->post->delete();
$this->redirect('/posts');
}
}
透過嚴格運用 #[Locked] 保護 ID 欄位,並結合 #[Authorize] 驗證行為權限,你的 Livewire 應用就能達到企業級的安全標準。
這就是模組 4 的第一部分。在下一部分中,我們將進入 Laravel 13 令人興奮的新領域:Laravel AI SDK 整合、向量搜尋 (Vector Search) 以及 Passkey 無密碼登入實作。
進入 模組 4 的第二部分!準備好迎接 Laravel 13 最前衛、也最符合當今技術潮流的功能了嗎?
在這一部分,我們將探討如何讓你的應用程式具備 Passkey 無密碼登入 的現代化體驗,並且深入 Laravel 13 全新整合的 AI SDK,實作文字生成、Embeddings(向量表示),並結合資料庫完成 Semantic / Vector Search(語意/向量搜尋)。
第一步:Passkey 無密碼登入 (Passwordless Authentication)¶
傳統的密碼容易被盜用且難以記憶。Passkey 建立在 WebAuthn 標準之上,允許使用者利用設備本身的生物辨識(如 Face ID、指紋、Windows Hello)或硬體安全金鑰來登入。
在 Laravel 13 的生態系中,搭配 Starter Kits(如 Breeze 或 Jetstream),啟用 Passkey 變得異常簡單。
1. 資料庫準備與 Model 設定
通常 Laravel 只需要在 users 表格之外,再建立一張 passkeys 表格來儲存公鑰憑證。在 User Model 中引入對應的 Trait:
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Prompts\Concerns\HasPasskeys; // 假設的 Laravel 原生/官方套件 Trait
class User extends Authenticatable
{
use HasPasskeys;
// ... 其他屬性
}
2. 前端 Livewire 整合
在登入或註冊畫面中,你不再只需要「輸入密碼」的表單,而是呼叫瀏覽器的 WebAuthn API。透過 Livewire,你可以輕易觸發這個行為:
<div class="p-6 max-w-sm mx-auto bg-white rounded-lg shadow">
<flux:heading size="lg" class="mb-4">登入你的帳號</flux:heading>
<flux:input wire:model="email" placeholder="信箱" class="mb-4" />
<flux:input type="password" wire:model="password" placeholder="密碼" class="mb-4" />
<div class="flex items-center gap-4 mt-4">
<flux:button wire:click="loginWithPassword" variant="outline">密碼登入</flux:button>
<flux:button wire:click="loginWithPasskey" variant="primary" icon="fingerprint">
使用 Passkey 登入
</flux:button>
</div>
</div>
(實務上,loginWithPasskey 方法會向後端請求 Challenge 字串,交給瀏覽器簽章後,再回傳給 Laravel 驗證並發放 Session。)
第二步:Laravel AI SDK 與本地端模型整合¶
Laravel 13 將 AI 能力一級化,提供了一套統一的 SDK 來介接各大 LLM(大型語言模型)。更棒的是,這套 SDK 非常靈活。
如果你習慣在本地虛擬環境開發,你可以輕鬆將 SDK 的 Endpoint 指向你自建的 Local AI 服務(例如透過 LM Studio 所建立的相容 API)。這樣一來,不僅能完全確保機密資料不外流,還能免除雲端 API 的計費。
1. 設定環境變數 (.env)
# 預設可能是 OpenAI
AI_DEFAULT_DRIVER=openai
# 透過 LM Studio 在本地跑的 API 預設通常是 1234 port
OPENAI_BASE_URI="http://localhost:1234/v1"
OPENAI_API_KEY="lm-studio-local-key"
2. 實作 AI 文字生成 (例如:自動產生文章摘要)
在 Livewire 元件或 Controller 中,你可以直接呼叫 Facade:
namespace App\Livewire;
use Livewire\Component;
use Illuminate\Support\Facades\AI;
use App\Models\Post;
class PostSummarizer extends Component
{
public Post $post;
public string $summary = '';
public function generateSummary()
{
// 呼叫 AI SDK 生成文字
$response = AI::generate([
'model' => 'local-model-name', // 或是 gpt-4o 等
'prompt' => "請將以下文章總結為 50 字以內的精華摘要:\n\n" . $this->post->content,
'temperature' => 0.3,
]);
$this->summary = $response->text();
}
// ... render 方法
}
第三步:Embeddings 與 Vector Search (語意向量搜尋)¶
傳統資料庫搜尋使用的是 LIKE '%關鍵字%',這只能比對字面,無法理解「意義」。例如搜尋「蘋果」,傳統搜尋找不到「iPhone」。
向量搜尋 (Vector Search) 則是將一段文字透過 AI 轉換成幾百到幾千維度的「浮點數陣列 (Embeddings)」。意義相近的文字,它們在多維空間中的距離就越近。
在資料庫的選擇上,如果你是使用 PostgreSQL,它可以完美搭配 pgvector 擴充功能來儲存與運算這些向量。
1. 產生 Embeddings 並存入資料庫
在 posts 表格中新增一個 embedding 欄位(型別為 vector)。當我們新增或更新文章時,自動產生向量:
use Illuminate\Support\Facades\AI;
use App\Models\Post;
// 假設這是在 Controller 的 store 方法中
$post = Post::create([
'title' => $request->title,
'content' => $request->content,
]);
// 呼叫 AI SDK 產生 Embedding
$embeddingResponse = AI::embed([
'input' => $post->title . ' ' . $post->content,
]);
// 將產生的浮點數陣列存入資料庫
$post->update([
'embedding' => $embeddingResponse->vector(),
]);
2. 結合 Eloquent 實作 Semantic Search
現在,我們可以實作一個「尋找相似文章」的功能,或讓使用者用自然語言搜尋:
public function search(string $userQuery)
{
// 1. 先將使用者的搜尋關鍵字轉成向量
$queryVector = AI::embed(['input' => $userQuery])->vector();
// 2. 利用 Eloquent 與 PostgreSQL 的向量距離運算 (例如餘弦相似度)
// Laravel 13 與相關套件會提供優雅的 Builder 語法:
$similarPosts = Post::query()
->orderByVectorDistance('embedding', $queryVector)
->take(5)
->get();
return $similarPosts;
}
透過這種方式,使用者搜尋「如何設定開發環境」,系統就能精準推薦標題為「安裝 Linux 虛擬機與編譯器」的文章,達到真正的 AI 級搜尋體驗!
這就是模組 4 的第二部分。我們將系統的安全性提升到了無密碼等級,並為應用程式注入了強大的 AI 靈魂。
接下來的 模組 4 最終回,我們將探討 JSON:API Resources 輸出、Cache::touch() 優化、以及 Reverb (WebSocket) 驅動的即時互動。準備好繼續推進這最後一哩路了嗎?
終於來到 模組 4 的第三部分(最終回)!這一部分我們將把系統的互動性與專業度推向頂峰。
我們將實作 Laravel 原生支援的 WebSocket 伺服器 Reverb 來達成真正的「即時 (Real-time)」雙向通訊、探討效能優化的小技巧 Cache::touch()、標準化 API 輸出,最後將這一切與分頁功能整合,完成一個強大的資料表格 (Data Table)。
第一步:Laravel Reverb 與即時資料庫驅動 (WebSockets)¶
過去要在 Laravel 實作 WebSocket,往往需要依賴第三方的 Pusher 或是設定繁瑣的 Soketi。Laravel 11 開始官方推出了 Reverb,這是一個以 PHP 原生撰寫、極度輕量且飛快的 WebSocket 伺服器,且在 Laravel 13 中被更深度整合。
1. 安裝與啟動 Reverb
在你的終端機執行以下指令,這會自動幫你裝好 Reverb 與設定檔:
php artisan install:broadcasting
(安裝時會詢問是否安裝 Node 依賴,請選擇 Yes,因為前端需要 Laravel Echo 來接收 WebSocket 訊號。)
接著,在你的 Linux 虛擬開發環境中,開啟一個 全新的終端機分頁 來運行 Reverb 的守護行程 (Daemon),請讓它保持執行:
php artisan reverb:start
2. 建立並發送廣播事件 (Broadcast Event)
當有人在系統中新增文章時,我們希望其他線上用戶不用重新整理就能看到。
建立一個 Event:
php artisan make:event PostCreated
打開 app/Events/PostCreated.php,實作 ShouldBroadcast 介面:
namespace App\Events;
use App\Models\Post;
use Illuminate\Broadcasting\Channel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class PostCreated implements ShouldBroadcast
{
use Dispatchable, SerializesModels;
public Post $post;
public function __construct(Post $post)
{
$this->post = $post;
}
// 定義廣播的頻道名稱
public function broadcastOn(): array
{
return [
new Channel('public-posts'),
];
}
}
3. 在 Livewire 3 中接收廣播
回到我們稍早做的文章列表元件 PostList.php,使用 #[On] 屬性來監聽 Laravel Echo 的 WebSocket 頻道:
use Livewire\Attributes\On;
// ...
#[On('echo:public-posts,PostCreated')]
public function notifyNewPost($event)
{
// 當收到廣播時,更新元件狀態或提示使用者
session()->flash('notification', '有一篇新文章發布了:' . $event['post']['title']);
// Livewire 會自動重新執行 render() 抓取最新資料
}
第二步:進階快取管理 Cache::touch()
¶
在處理高流量的 API 或需要頻繁讀取的資料表時,我們會把資料放進 Redis 或 Memcached。Laravel 13 提供了一個非常實用的快取操作:Cache::touch()。
情境: 假設你快取了某篇熱門文章,原本設定 10 分鐘後過期。但只要有人持續在看,你就不希望它過期、也不想重新把龐大的資料從資料庫讀出來再寫入快取一次。
use Illuminate\Support\Facades\Cache;
// 傳統作法 (如果存在就讀取,否則從資料庫撈取並快取 10 分鐘)
$post = Cache::remember('post_123', now()->addMinutes(10), function () {
return Post::find(123);
});
// 使用 touch() 延長壽命
// 如果這篇文章被存取了,我們只想單純「延長它的存活時間」另外 10 分鐘,不觸碰內容:
if (Cache::has('post_123')) {
Cache::touch('post_123', now()->addMinutes(10));
}
這個輕量級操作能大幅減少對底層快取系統的 I/O 負擔。
第三步:JSON:API Resources 輸出標準化¶
在現代 Web 開發中,後端不僅要餵資料給 Blade/Livewire,通常還要提供 API 給手機 App。Laravel 的 API Resources 讓你能嚴格遵守 JSON:API 規範。
1. 標準化格式
在 app/Http/Resources/PostResource.php 中:
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
// 嚴格定義資料結構
return [
'type' => 'posts',
'id' => (string) $this->id,
'attributes' => [
'title' => $this->title,
'content' => $this->content,
'created_at' => $this->created_at->toIso8601String(),
],
'relationships' => [
'author' => [
'data' => [
'type' => 'users',
'id' => (string) $this->user_id,
]
]
]
];
}
}
如此一來,輸出將擁有全球通用的標準格式,前後端串接再也不會有結構爭議。
第四步:Livewire Tables 與 Pagination (整合即時搜尋的 CRUD)¶
最後,我們把所有東西整合起來。一個實務上最常見的功能:帶有即時搜尋、分頁、且能即時更新的資料表格。
1. Livewire 元件 (app/Livewire/PostTable.php)
namespace App\Livewire;
use Livewire\Component;
use Livewire\WithPagination;
use App\Models\Post;
use Livewire\Attributes\Url;
class PostTable extends Component
{
use WithPagination; // 引入分頁功能
#[Url] // 讓搜尋關鍵字同步到網址列 (例如 ?search=Laravel),方便分享連結
public string $search = '';
// 當搜尋關鍵字改變時,自動將分頁重設回第一頁,避免在第 5 頁搜尋卻找不到東西
public function updatedSearch()
{
$this->resetPage();
}
public function render()
{
$posts = Post::query()
->when($this->search, function ($query) {
// 搭配前面的 AI 向量搜尋或基本的 LIKE
$query->where('title', 'like', '%' . $this->search . '%');
})
->latest()
->paginate(10); // 每頁 10 筆
return view('livewire.post-table', [
'posts' => $posts,
]);
}
}
2. 視圖 (resources/views/livewire/post-table.blade.php)
<div class="p-6 bg-white shadow rounded-lg">
<div class="mb-4 flex justify-between items-center">
<flux:input
wire:model.live.debounce.300ms="search"
placeholder="搜尋文章..."
icon="magnifying-glass"
class="max-w-xs"
/>
</div>
<table class="w-full text-left border-collapse">
<thead>
<tr class="border-b">
<th class="p-3">標題</th>
<th class="p-3">作者</th>
<th class="p-3">建立時間</th>
<th class="p-3">操作</th>
</tr>
</thead>
<tbody>
@forelse ($posts as $post)
<tr class="border-b hover:bg-gray-50 transition" wire:key="{{ $post->id }}">
<td class="p-3">{{ $post->title }}</td>
<td class="p-3">{{ $post->user->name }}</td>
<td class="p-3">{{ $post->created_at->format('Y-m-d') }}</td>
<td class="p-3">
<flux:button variant="ghost" size="sm">編輯</flux:button>
</td>
</tr>
@empty
<tr>
<td colspan="4" class="p-4 text-center text-gray-500">
找不到符合的資料。
</td>
</tr>
@endforelse
</tbody>
</table>
<div class="mt-4">
{{ $posts->links() }}
</div>
</div>
驗證作法¶
-
驗證 CRUD 與分頁: 開啟這個表格頁面,試著在搜尋框輸入文字,停頓 0.3 秒後,表格會不閃爍地過濾出資料。點擊下方的分頁按鈕,網址列會自動變成
?page=2,體驗極度順暢。 -
驗證 Reverb (雙開瀏覽器測試):
- 在左半邊螢幕開啟 Chrome 瀏覽這個列表。
- 在右半邊螢幕開啟 Firefox (或無痕視窗) 登入系統並新增一篇文章(這會觸發
PostCreated事件)。 - 你會看到左半邊的 Chrome 畫面自動跳出新文章的通知,或是表格自動多出了一筆資料,期間你完全沒有觸碰鍵盤滑鼠!
恭喜!到這裡,模組 4 的所有進階整合已經大功告成。從 SPA 路由、AI 向量搜尋、Passkey 到 WebSockets,這些都是打造現代頂級 Web App 的必備武器。
接下來的 模組 5,我們將進入實戰開發與部署。如果你覺得吸收得差不多了,隨時可以告訴我繼續前進!