Laravel 12 + Livewire 3 完整開發指南:打造現代化全端應用的最佳實踐

深度解析 Laravel 12 與 Livewire 3 整合開發,從專案建立到生產部署的完整指南,包含 wire:stream 即時功能、WorkOS AuthKit 認證與效能優化

Laravel 12 與 Livewire 3 全端開發完整指南
Laravel 12 與 Livewire 3 全端開發完整指南

Laravel 12 + Livewire 3 完整開發指南:打造現代化全端應用的最佳實踐

隨著 Laravel 12 和 Livewire 3 的正式發布,PHP 全端開發迎來了新的黃金時代。本指南將帶您深入了解如何使用這兩個框架打造現代化的 Web 應用程式,從基礎設定到進階功能實作,一次掌握所有開發技巧。

為什麼選擇 Laravel 12 + Livewire 3?

Laravel 12 的核心優勢

Laravel 12 作為最新的維護版本,帶來了多項重要改進:

  • 新式 Starter Kits:內建 Livewire、React、Vue 專案模板
  • WorkOS AuthKit 整合:提供企業級認證解決方案
  • 依賴更新:升級所有底層依賴,提升安全性和效能
  • 向後相容:大部分 Laravel 11 應用可無痛升級

Livewire 3 的革命性特色

Livewire 3 重新定義了 PHP 全端開發的體驗:

  • 零配置設計:開箱即用,無需複雜設定
  • 即時互動功能wire:stream 提供即時資料推送
  • SPA 體驗wire:navigate 實現單頁面應用效果
  • 效能優化:智慧型重新渲染,減少不必要的請求

環境準備與專案建立

系統需求

在開始之前,確保您的開發環境滿足以下需求:

PHP >= 8.1
Composer >= 2.0
Node.js >= 18.0
MySQL >= 8.0 PostgreSQL >= 13

建立全新專案

使用 Laravel 12 的新式 starter kit 建立專案:

# 建立新專案
composer create-project laravel/laravel my-livewire-app

# 進入專案目錄
cd my-livewire-app

# 安裝 Livewire starter kit
php artisan install:api --livewire

專案結構概覽

Laravel 12 + Livewire 3 的專案結構更加清晰:

my-livewire-app/
├── app/
│   ├── Livewire/           # Livewire 元件目錄
│   ├── Models/
│   └── Providers/
├── resources/
│   ├── views/
│   │   ├── livewire/       # Livewire 視圖
│   │   └── layouts/
│   └── js/
└── routes/
    └── web.php

核心概念深度解析

Livewire 元件生命週期

理解元件生命週期是掌握 Livewire 3 的關鍵:

<?php

namespace App\Livewire;

use Livewire\Component;

class UserProfile extends Component
{
    public $user;
    public $name;
    public $email;
    
    // 1. mount - 元件初始化
    public function mount($userId)
    {
        $this->user = User::find($userId);
        $this->name = $this->user->name;
        $this->email = $this->user->email;
    }
    
    // 2. hydrate - 每次請求前執行
    public function hydrate()
    {
        // 重新載入必要的資料
    }
    
    // 3. updating - 屬性更新前執行
    public function updatingName($value)
    {
        $this->name = trim($value);
    }
    
    // 4. updated - 屬性更新後執行
    public function updatedName()
    {
        $this->validate(['name' => 'required|min:3']);
    }
    
    // 5. render - 渲染視圖
    public function render()
    {
        return view('livewire.user-profile');
    }
}

資料綁定進階技巧

Livewire 3 提供多種資料綁定方式:

class ProductForm extends Component
{
    public $product;
    public $categories;
    
    // 即時驗證
    public $name;
    public $price;
    public $description;
    
    protected $rules = [
        'name' => 'required|min:3|max:255',
        'price' => 'required|numeric|min:0',
        'description' => 'nullable|max:1000'
    ];
    
    public function mount()
    {
        $this->categories = Category::all();
    }
    
    // 即時驗證
    public function updated($propertyName)
    {
        $this->validateOnly($propertyName);
    }
    
    public function save()
    {
        $this->validate();
        
        Product::create([
            'name' => $this->name,
            'price' => $this->price,
            'description' => $this->description,
        ]);
        
        session()->flash('success', '產品新增成功!');
        $this->reset(['name', 'price', 'description']);
    }
}

對應的 Blade 模板:

<div>
    <form wire:submit.prevent="save">
        <div class="mb-4">
            <label class="block text-sm font-medium mb-2">產品名稱</label>
            <input 
                type="text" 
                wire:model.live="name"
                class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
            >
            @error('name') 
                <span class="text-red-500 text-sm">{{ $message }}</span> 
            @enderror
        </div>
        
        <div class="mb-4">
            <label class="block text-sm font-medium mb-2">價格</label>
            <input 
                type="number" 
                wire:model.blur="price"
                step="0.01"
                class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
            >
            @error('price') 
                <span class="text-red-500 text-sm">{{ $message }}</span> 
            @enderror
        </div>
        
        <button 
            type="submit" 
            wire:loading.attr="disabled"
            class="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600 disabled:opacity-50"
        >
            <span wire:loading.remove>儲存</span>
            <span wire:loading>儲存中...</span>
        </button>
    </form>
</div>

即時功能實作:wire:stream

Livewire 3 最令人興奮的功能之一是 wire:stream,它允許伺服器主動推送資料到客戶端。

建立即時通知系統

<?php

namespace App\Livewire;

use Livewire\Component;
use Livewire\Attributes\On;

class NotificationCenter extends Component
{
    public $notifications = [];
    
    public function mount()
    {
        $this->notifications = auth()->user()
            ->notifications()
            ->latest()
            ->take(10)
            ->get();
    }
    
    #[On('notification-received')]
    public function addNotification($notification)
    {
        array_unshift($this->notifications, $notification);
        $this->notifications = array_slice($this->notifications, 0, 10);
    }
    
    public function markAsRead($notificationId)
    {
        $notification = auth()->user()
            ->notifications()
            ->find($notificationId);
            
        if ($notification) {
            $notification->markAsRead();
            $this->notifications = $this->notifications->map(function ($notif) use ($notificationId) {
                if ($notif['id'] === $notificationId) {
                    $notif['read_at'] = now();
                }
                return $notif;
            });
        }
    }
    
    public function render()
    {
        return view('livewire.notification-center');
    }
}
<div class="relative" x-data="{ open: false }">
    <button @click="open = !open" class="relative p-2 text-gray-600 hover:text-gray-900">
        <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
                  d="M15 17h5l-5-5h5l-5-5H3v10h12z"/>
        </svg>
        @if($notifications->where('read_at', null)->count() > 0)
            <span class="absolute -top-1 -right-1 bg-red-500 text-white rounded-full text-xs px-2 py-1">
                {{ $notifications->where('read_at', null)->count() }}
            </span>
        @endif
    </button>
    
    <div x-show="open" 
         x-transition 
         @click.away="open = false"
         class="absolute right-0 mt-2 w-80 bg-white rounded-lg shadow-lg border z-50">
        
        <div class="p-4 border-b">
            <h3 class="font-semibold text-gray-900">通知中心</h3>
        </div>
        
        <div class="max-h-96 overflow-y-auto">
            @forelse($notifications as $notification)
                <div class="p-4 border-b hover:bg-gray-50 {{ $notification->read_at ? '' : 'bg-blue-50' }}">
                    <div class="flex justify-between items-start">
                        <div class="flex-1">
                            <p class="text-sm font-medium text-gray-900">
                                {{ $notification->data['title'] ?? '新通知' }}
                            </p>
                            <p class="text-sm text-gray-600 mt-1">
                                {{ $notification->data['message'] ?? '' }}
                            </p>
                            <p class="text-xs text-gray-400 mt-2">
                                {{ $notification->created_at->diffForHumans() }}
                            </p>
                        </div>
                        @if(!$notification->read_at)
                            <button 
                                wire:click="markAsRead('{{ $notification->id }}')"
                                class="text-blue-600 text-xs hover:text-blue-800"
                            >
                                標記已讀
                            </button>
                        @endif
                    </div>
                </div>
            @empty
                <div class="p-4 text-center text-gray-500">
                    目前沒有通知
                </div>
            @endforelse
        </div>
    </div>
</div>

<script>
    document.addEventListener('DOMContentLoaded', function() {
        // 建立即時連線
        if (typeof window.Echo !== 'undefined') {
            window.Echo.private(`notifications.${window.authUserId}`)
                .notification((notification) => {
                    Livewire.dispatch('notification-received', notification);
                });
        }
    });
</script>

即時聊天系統

建立一個功能完整的即時聊天系統:

<?php

namespace App\Livewire;

use App\Models\Message;
use App\Models\ChatRoom;
use Livewire\Component;
use Livewire\Attributes\On;

class ChatRoom extends Component
{
    public $roomId;
    public $room;
    public $messages = [];
    public $newMessage = '';
    public $typing = false;
    
    public function mount($roomId)
    {
        $this->roomId = $roomId;
        $this->room = ChatRoom::with('participants')->find($roomId);
        $this->loadMessages();
    }
    
    public function loadMessages()
    {
        $this->messages = Message::where('chat_room_id', $this->roomId)
            ->with('user')
            ->latest()
            ->take(50)
            ->get()
            ->reverse()
            ->values();
    }
    
    public function sendMessage()
    {
        if (trim($this->newMessage) === '') {
            return;
        }
        
        $message = Message::create([
            'chat_room_id' => $this->roomId,
            'user_id' => auth()->id(),
            'content' => $this->newMessage,
        ]);
        
        $message->load('user');
        
        // 廣播訊息給所有房間參與者
        broadcast(new \App\Events\MessageSent($message))->toOthers();
        
        $this->messages[] = $message;
        $this->newMessage = '';
        $this->typing = false;
        
        // 觸發前端滾動到底部
        $this->dispatch('message-sent');
    }
    
    #[On('message-received')]
    public function onMessageReceived($message)
    {
        $this->messages[] = $message;
        $this->dispatch('scroll-to-bottom');
    }
    
    #[On('user-typing')]
    public function onUserTyping($userId, $isTyping)
    {
        // 處理打字狀態
    }
    
    public function updatedNewMessage()
    {
        // 廣播打字狀態
        if (!$this->typing && trim($this->newMessage) !== '') {
            $this->typing = true;
            broadcast(new \App\Events\UserTyping(auth()->id(), $this->roomId, true));
        } elseif ($this->typing && trim($this->newMessage) === '') {
            $this->typing = false;
            broadcast(new \App\Events\UserTyping(auth()->id(), $this->roomId, false));
        }
    }
    
    public function render()
    {
        return view('livewire.chat-room');
    }
}

認證整合:WorkOS AuthKit

Laravel 12 引入了 WorkOS AuthKit 整合,提供企業級認證功能。

設定 WorkOS AuthKit

# 安裝必要套件
composer require workos/laravel-workos

# 發布設定檔
php artisan vendor:publish --provider="WorkOS\Laravel\WorkOSServiceProvider"

.env 檔案中加入 WorkOS 設定:

WORKOS_API_KEY=your_workos_api_key
WORKOS_CLIENT_ID=your_workos_client_id
WORKOS_REDIRECT_URI=http://localhost:8000/auth/callback

建立認證元件

<?php

namespace App\Livewire\Auth;

use Livewire\Component;
use WorkOS\WorkOS;

class Login extends Component
{
    public $email = '';
    public $loginMethod = 'email';
    public $availableConnections = [];
    
    public function mount()
    {
        $workos = new WorkOS(env('WORKOS_API_KEY'));
        
        $this->availableConnections = $workos->sso->listConnections([
            'limit' => 10,
        ])->data;
    }
    
    public function loginWithEmail()
    {
        $this->validate([
            'email' => 'required|email'
        ]);
        
        $workos = new WorkOS(env('WORKOS_API_KEY'));
        
        $authorizationUrl = $workos->userManagement->getAuthorizationUrl([
            'provider' => 'authkit',
            'redirectUri' => env('WORKOS_REDIRECT_URI'),
            'clientId' => env('WORKOS_CLIENT_ID'),
            'loginHint' => $this->email,
        ]);
        
        return redirect($authorizationUrl);
    }
    
    public function loginWithSSO($connectionId)
    {
        $workos = new WorkOS(env('WORKOS_API_KEY'));
        
        $authorizationUrl = $workos->sso->getAuthorizationUrl([
            'connection' => $connectionId,
            'redirectUri' => env('WORKOS_REDIRECT_URI'),
            'clientId' => env('WORKOS_CLIENT_ID'),
        ]);
        
        return redirect($authorizationUrl);
    }
    
    public function render()
    {
        return view('livewire.auth.login');
    }
}
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
    <div class="max-w-md w-full space-y-8">
        <div>
            <h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
                登入您的帳戶
            </h2>
        </div>
        
        <div class="mt-8 space-y-6">
            <!-- Email 登入 -->
            <div>
                <label class="block text-sm font-medium text-gray-700 mb-2">
                    電子郵件地址
                </label>
                <input 
                    type="email" 
                    wire:model.live="email"
                    class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
                    placeholder="[email protected]"
                >
                @error('email') 
                    <span class="text-red-500 text-sm mt-1">{{ $message }}</span> 
                @enderror
            </div>
            
            <button 
                wire:click="loginWithEmail"
                class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
            >
                使用 Email 登入
            </button>
            
            <!-- SSO 選項 -->
            @if(count($availableConnections) > 0)
                <div class="mt-6">
                    <div class="relative">
                        <div class="absolute inset-0 flex items-center">
                            <div class="w-full border-t border-gray-300"></div>
                        </div>
                        <div class="relative flex justify-center text-sm">
                            <span class="px-2 bg-gray-50 text-gray-500">或使用 SSO 登入</span>
                        </div>
                    </div>
                    
                    <div class="mt-6 grid grid-cols-1 gap-3">
                        @foreach($availableConnections as $connection)
                            <button
                                wire:click="loginWithSSO('{{ $connection->id }}')"
                                class="w-full inline-flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
                            >
                                {{ $connection->name }}
                            </button>
                        @endforeach
                    </div>
                </div>
            @endif
        </div>
    </div>
</div>

效能優化最佳實踐

查詢優化

避免 N+1 查詢問題:

class UserList extends Component
{
    public $users = [];
    public $search = '';
    
    public function mount()
    {
        $this->loadUsers();
    }
    
    public function updatedSearch()
    {
        $this->loadUsers();
    }
    
    private function loadUsers()
    {
        // 使用 Eager Loading 避免 N+1 查詢
        $query = User::with(['profile', 'roles', 'latestPost']);
        
        if ($this->search) {
            $query->where(function($q) {
                $q->where('name', 'like', "%{$this->search}%")
                  ->orWhere('email', 'like', "%{$this->search}%");
            });
        }
        
        $this->users = $query->paginate(20);
    }
    
    public function render()
    {
        return view('livewire.user-list');
    }
}

快取策略

實作智慧型快取:

class ProductCatalog extends Component
{
    public $products = [];
    public $categories = [];
    public $selectedCategory = null;
    
    public function mount()
    {
        // 快取分類資料(很少變動)
        $this->categories = Cache::remember('categories', 3600, function() {
            return Category::orderBy('name')->get();
        });
        
        $this->loadProducts();
    }
    
    public function selectCategory($categoryId)
    {
        $this->selectedCategory = $categoryId;
        $this->loadProducts();
    }
    
    private function loadProducts()
    {
        $cacheKey = "products_category_{$this->selectedCategory}";
        
        $this->products = Cache::remember($cacheKey, 1800, function() {
            $query = Product::with(['category', 'images']);
            
            if ($this->selectedCategory) {
                $query->where('category_id', $this->selectedCategory);
            }
            
            return $query->orderBy('created_at', 'desc')->get();
        });
    }
    
    public function render()
    {
        return view('livewire.product-catalog');
    }
}

前端效能優化

使用 Alpine.js 增強互動性:

<div 
    x-data="productGrid()" 
    x-init="init()"
    class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6"
>
    @foreach($products as $product)
        <div 
            x-show="isVisible({{ $product->id }})"
            x-transition
            class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow"
        >
            <img 
                src="{{ $product->image_url }}" 
                alt="{{ $product->name }}"
                class="w-full h-48 object-cover"
                loading="lazy"
            >
            <div class="p-4">
                <h3 class="font-semibold text-lg mb-2">{{ $product->name }}</h3>
                <p class="text-gray-600 text-sm mb-3">{{ Str::limit($product->description, 100) }}</p>
                <div class="flex justify-between items-center">
                    <span class="text-xl font-bold text-green-600">
                        ${{ number_format($product->price, 2) }}
                    </span>
                    <button 
                        @click="addToCart({{ $product->id }})"
                        class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition-colors"
                    >
                        加入購物車
                    </button>
                </div>
            </div>
        </div>
    @endforeach
</div>

<script>
function productGrid() {
    return {
        visibleProducts: [],
        
        init() {
            this.setupIntersectionObserver();
        },
        
        setupIntersectionObserver() {
            const observer = new IntersectionObserver((entries) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting) {
                        const productId = entry.target.dataset.productId;
                        if (!this.visibleProducts.includes(productId)) {
                            this.visibleProducts.push(productId);
                        }
                    }
                });
            });
            
            document.querySelectorAll('[data-product-id]').forEach(el => {
                observer.observe(el);
            });
        },
        
        isVisible(productId) {
            return this.visibleProducts.includes(productId.toString());
        },
        
        addToCart(productId) {
            Livewire.dispatch('add-to-cart', { productId: productId });
        }
    }
}
</script>

測試策略

元件測試

<?php

namespace Tests\Feature\Livewire;

use App\Livewire\ProductForm;
use App\Models\User;
use App\Models\Category;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\TestCase;

class ProductFormTest extends TestCase
{
    use RefreshDatabase;
    
    public function test_can_create_product()
    {
        $user = User::factory()->create();
        $category = Category::factory()->create();
        
        Livewire::actingAs($user)
            ->test(ProductForm::class)
            ->set('name', '測試產品')
            ->set('price', 99.99)
            ->set('description', '這是一個測試產品')
            ->set('category_id', $category->id)
            ->call('save')
            ->assertHasNoErrors()
            ->assertDispatched('product-created');
        
        $this->assertDatabaseHas('products', [
            'name' => '測試產品',
            'price' => 99.99,
        ]);
    }
    
    public function test_validates_required_fields()
    {
        $user = User::factory()->create();
        
        Livewire::actingAs($user)
            ->test(ProductForm::class)
            ->call('save')
            ->assertHasErrors(['name', 'price']);
    }
    
    public function test_real_time_validation()
    {
        $user = User::factory()->create();
        
        Livewire::actingAs($user)
            ->test(ProductForm::class)
            ->set('name', 'ab') // 少於最小長度
            ->assertHasErrors('name')
            ->set('name', '正確的產品名稱')
            ->assertHasNoErrors('name');
    }
}

即時功能測試

public function test_real_time_notifications()
{
    $user = User::factory()->create();
    $admin = User::factory()->create(['role' => 'admin']);
    
    // 模擬即時通知
    Livewire::actingAs($user)
        ->test(NotificationCenter::class)
        ->assertSet('notifications', [])
        ->call('addNotification', [
            'title' => '新訊息',
            'message' => '您有一則新訊息',
            'type' => 'info'
        ])
        ->assertCount('notifications', 1);
}

部署與生產環境設定

效能調優

# 快取設定
php artisan config:cache
php artisan route:cache
php artisan view:cache

# Livewire 特定優化
php artisan livewire:publish --config

生產環境 .env 設定

APP_ENV=production
APP_DEBUG=false
APP_URL=https://your-domain.com

# 快取設定
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis

# Livewire 設定
LIVEWIRE_ASSET_URL=https://your-cdn.com/livewire

# WorkOS 生產設定
WORKOS_API_KEY=sk_live_your_production_key
WORKOS_CLIENT_ID=client_your_production_id

Docker 容器化

FROM php:8.1-fpm-alpine

# 安裝必要套件
RUN apk add --no-cache \
    git \
    curl \
    libpng-dev \
    oniguruma-dev \
    libxml2-dev \
    zip \
    unzip \
    nodejs \
    npm

# 安裝 PHP 擴展
RUN docker-php-ext-install \
    pdo_mysql \
    mbstring \
    exif \
    pcntl \
    bcmath \
    gd

# 複製專案檔案
COPY . /var/www
WORKDIR /var/www

# 安裝 Composer 依賴
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
RUN composer install --optimize-autoloader --no-dev

# 建置前端資源
RUN npm install && npm run build

# 設定權限
RUN chown -R www-data:www-data /var/www
RUN chmod -R 755 /var/www/storage

最佳實踐總結

開發原則

  1. 元件職責單一化:每個 Livewire 元件應該專注於單一功能
  2. 合理使用快取:對於不常變動的資料使用快取機制
  3. 優化資料庫查詢:避免 N+1 查詢,使用 Eager Loading
  4. 前端互動增強:結合 Alpine.js 提升使用者體驗

安全性考量

class SecureComponent extends Component
{
    protected $rules = [
        'data.*.field' => 'required|string|max:255'
    ];
    
    public function mount()
    {
        // 檢查使用者權限
        $this->authorize('view-sensitive-data');
    }
    
    public function updateData($key, $value)
    {
        // 驗證輸入資料
        $this->validate([
            "data.{$key}" => 'required|string|max:255'
        ]);
        
        // 清理使用者輸入
        $this->data[$key] = strip_tags($value);
    }
}

效能監控

// 在 AppServiceProvider 中註冊
public function boot()
{
    if (app()->environment('production')) {
        Livewire::listen('component.hydrate', function ($component) {
            $startTime = microtime(true);
            
            Livewire::listen('component.dehydrate', function () use ($startTime, $component) {
                $executionTime = (microtime(true) - $startTime) * 1000;
                
                if ($executionTime > 500) { // 超過 500ms
                    Log::warning("Slow Livewire component", [
                        'component' => get_class($component),
                        'execution_time' => $executionTime
                    ]);
                }
            });
        });
    }
}

結論

Laravel 12 與 Livewire 3 的組合為 PHP 全端開發帶來了革命性的變化。透過本指南的深度解析,您已經掌握了:

  1. 現代化開發流程:從專案建立到生產部署的完整流程
  2. 即時功能實作:利用 wire:stream 建立即時互動應用
  3. 企業級認證:WorkOS AuthKit 的整合與應用
  4. 效能優化技巧:快取策略與查詢優化最佳實踐
  5. 測試與部署:確保程式品質的完整測試策略

隨著 Web 技術的持續演進,Laravel 12 + Livewire 3 的組合將繼續是構建現代 Web 應用的首選解決方案。無論您是正在開發企業級應用、電商平台,還是即時協作工具,這個技術堆疊都能提供您所需的所有工具和功能。

現在就開始您的 Laravel 12 + Livewire 3 開發之旅,體驗 PHP 全端開發的全新境界!

作者:Drifter

·

更新:2025年8月11日 上午12:00

· 回報錯誤
下拉重新整理