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
最佳實踐總結
開發原則
- 元件職責單一化:每個 Livewire 元件應該專注於單一功能
- 合理使用快取:對於不常變動的資料使用快取機制
- 優化資料庫查詢:避免 N+1 查詢,使用 Eager Loading
- 前端互動增強:結合 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 全端開發帶來了革命性的變化。透過本指南的深度解析,您已經掌握了:
- 現代化開發流程:從專案建立到生產部署的完整流程
- 即時功能實作:利用
wire:stream
建立即時互動應用 - 企業級認證:WorkOS AuthKit 的整合與應用
- 效能優化技巧:快取策略與查詢優化最佳實踐
- 測試與部署:確保程式品質的完整測試策略
隨著 Web 技術的持續演進,Laravel 12 + Livewire 3 的組合將繼續是構建現代 Web 應用的首選解決方案。無論您是正在開發企業級應用、電商平台,還是即時協作工具,這個技術堆疊都能提供您所需的所有工具和功能。
現在就開始您的 Laravel 12 + Livewire 3 開發之旅,體驗 PHP 全端開發的全新境界!