專案

一般

配置概況

動作

文件 #5

進行中
JH

模組 4:Laravel 13 + Livewire 3 進階整合(8–10 小時)

文件 #5: 模組 4:Laravel 13 + Livewire 3 進階整合(8–10 小時)

是由 Jeffery Hsu3 天 前加入. 於 2 天 前更新.

狀態:
已解決
優先權:
正常
被分派者:
-
開始日期:
2026-04-26
完成日期:
完成比例:

0%

預估工時:

概述

  1. Full Page Components 與 Navigation(wire:navigate 實現 SPA-like 體驗)。
  2. 巢狀 Components(Nested Components)與事件傳遞。
  3. 授權與安全性(Policies、Gates、Livewire 安全最佳實務)。
  4. Laravel 13 新功能整合:
    • Laravel AI SDK(文字生成、Embeddings、向量搜尋、Agents)。
    • Passkey 無密碼登入。
    • Semantic / Vector Search 結合 Eloquent。
    • JSON:API Resources 輸出。
    • Cache::touch()、Reverb(WebSocket)資料庫驅動。
  5. Pagination + Livewire Tables。
  6. 實作:帶即時搜尋與 AI 輔助的 CRUD 系統。

JH 是由 Jeffery Hsu3 天 前更新 動作 #1

  • 追蹤標籤功能 變更為 文件

JH 是由 Jeffery Hsu2 天 前更新 動作 #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>

驗證作法

  1. 驗證 CRUD 與分頁: 開啟這個表格頁面,試著在搜尋框輸入文字,停頓 0.3 秒後,表格會不閃爍地過濾出資料。點擊下方的分頁按鈕,網址列會自動變成 ?page=2,體驗極度順暢。
  2. 驗證 Reverb (雙開瀏覽器測試):
    • 在左半邊螢幕開啟 Chrome 瀏覽這個列表。
    • 在右半邊螢幕開啟 Firefox (或無痕視窗) 登入系統並新增一篇文章(這會觸發 PostCreated 事件)。
    • 你會看到左半邊的 Chrome 畫面自動跳出新文章的通知,或是表格自動多出了一筆資料,期間你完全沒有觸碰鍵盤滑鼠!

恭喜!到這裡,模組 4 的所有進階整合已經大功告成。從 SPA 路由、AI 向量搜尋、Passkey 到 WebSockets,這些都是打造現代頂級 Web App 的必備武器。

接下來的 模組 5,我們將進入實戰開發與部署。如果你覺得吸收得差不多了,隨時可以告訴我繼續前進!

動作

匯出至 PDF Atom