學習了怎麼用 Laravel Boost 幫我們撰寫後端 API,並且可以加上規格文件和自動化測試,確保我們撰寫的內容符合想法之後。
接著我們來看看,如果要生成前端互動的元件該怎麼進行。
在很久以前的專案,網頁前後端是不做區分的,撰寫網頁後端的人也會包含到前端行為的設計和撰寫。隨著網頁前端行為越來越複雜,後來前後端分離的做法漸漸變成主流,網頁前端的行為由前端工程師處理,,並且使用專業的框架,比方說 Vue 或者 React 進行撰寫。後端的部分則專注處理資料面和邏輯面的部分,並將處理好的資料透過 API 提供給前端。
這個做法在大多數的專案上都非常合適,不過大家在開發過程中也逐漸發現,面對一些前端邏輯相對單純的需求,如果一樣使用前後端分離的做法,那麼可能就沒有享受到前端框架所能提供的好處,反而因為將專案拆分成前後端兩個專案,以及引入不同的框架撰寫,導致後續維護上的人力成本偏高。
面對這種類型的專案,Laravel 提供了 Livewire 這個框架,可以讓 PHP 工程師不需要去碰觸前端框架,也能寫出前端互動的功能。
Livewire 是一個 Laravel 的全端框架,讓你用 PHP 直接寫互動式的前端功能,而不需要大量依賴 JavaScript。
核心概念如下:
如果使用的是 Livewire Volt 的話,這兩個元件會寫在同一個檔案內。
由於在 Day 02:使用 php.new 設置開發環境,搭配 Laravel Starter Kit 建立專案 生成專案時,我們已經使用了 Livewire 版本的 starter kit,所以這時候不需要再進行安裝,直接使用即可。
我們來做一個簡單的範例,做一個點擊後會 +1 的按鈕。跟昨天一樣,我們先試著生成規格書
幫我生成一個點擊後會 +1 的 Livewire 元件 的規格書,並放在 docs/ 內
生成規格書如下
# Livewire 元件規格:點擊後 +1 計數器(Increment Counter)
本文件定義一個以 Livewire 3(搭配 Volt 單檔元件風格)實作的「點擊後 +1」計數器元件之規格,供前後端、設計、與測試參考。除特別註明外,本文檔不限定實際檔名與路徑,但範例以 `resources/views/livewire/counter.blade.php`(Volt 單檔元件)呈現。
- 技術棧:Laravel 12、Livewire 3、Livewire Volt、Tailwind CSS v4、Flux UI Free(可選)
- 語言:繁體中文
## 目標與敘述
- 提供一個簡單的互動元件,顯示目前數值 `count`,並在使用者點擊按鈕後使 `count` 增加 1。
- 元件需在不重新整理頁面的情況下即時更新(Livewire 互動)。
- 元件應易於在任何 Blade 頁面中重複使用。
## 使用情境(Use Cases)
1. 使用者看到一個數字(預設 0),按下「+1」按鈕後,數字立即變成 1、2、3…。
2. 可自訂初始值(例如來自父層傳入或資料庫取得)。
3. 可限制最大值(選用)。
## 功能需求(Functional Requirements)
- 顯示目前計數值 `count`。
- 提供按鈕或可點擊元素,觸發 `increment()` 行為,使 `count = count + 1`。
- 提供屬性/參數:
- `initial`(number,選填,預設 0):初始值。
- `max`(number,選填,預設 null):若設定,則 `count` 不得超過 `max`。
- 若已達 `max`,應禁用按鈕或提供視覺狀態提示。
## 非功能需求(Non-Functional Requirements)
- 可存取性(a11y):
- 按鈕需有可理解的 `aria-label`(例:`aria-label="increase counter"`)。
- 透過鍵盤(Enter/Space)可觸發。
- 若禁用狀態,使用 `disabled` 屬性,並有對應的樣式提示顏色。
- 響應式:在不同裝置尺寸下均可正常顯示。
- 效能:避免不必要的重新渲染;使用 Livewire 預設即可。
- i18n:文案可由父層覆寫或使用簡單字串常數,預設顯示「+1」。
## 互動詳述
- Action:`increment()`
- 條件:若 `max` 為 null 或 `count < max`。
- 結果:`count++` 並更新 UI。
- 邊界:若 `count` 在呼叫前已等於/超過 `max`,則不遞增且可視化為禁用。
## 介面與樣式
- 預設使用 Tailwind v4。
- 可選:使用 Flux UI Free 的 `<flux:button/>` 作為按鈕元件,以符合專案 UI 風格。
- 建議樣式:
- 計數顯示:大字體、字重中等。
- 按鈕:主要色系,禁用時降低對比。
## 屬性與狀態
- 狀態(State):
- `public int $count`(或 `public float $count` 視需求,但本元件以 `int` 為準)。
- 參數(Props):
- `public int $initial = 0;`
- `public ?int $max = null;`
- 初始化:`mount($initial = 0, $max = null)` 中設定 `$this->count = (int) $initial; $this->max = $max !== null ? (int) $max : null;`
## 事件(可選)
- `counter-incremented`:每次成功 +1 後可透過 `$this->dispatch('counter-incremented', count: $this->count)` 對外發布事件,供父層監聽。
## 錯誤處理
- 若 `max` 非數字或小於 `initial`,以就近原則:
- 將 `max` 設為 `null` 或
- 規格化:`max = max(initial, max)`。
- 本元件不進行伺服器端資料儲存;如需持久化,須由父層或外部行為處理。
⋯⋯
## 整合建議
- 將元件置於常見版面 layout 中,如 `components.layouts.app`。
- 在需要的頁面插入元件,並視需求傳入 `initial` 與 `max`。
- 若需記錄使用者點擊次數,可在父層監聽 `counter-incremented` 事件並觸發後續行為(例如 API 紀錄)。
## 開發注意事項
- Livewire 3 事件請使用 `$this->dispatch()`(非舊版的 `emit`)。
- 需要即時更新時,輸入綁定使用 `wire:model.live`;本元件僅有按鈕行為,無輸入欄位。
- Tailwind v4 請使用 `@import "tailwindcss";` 的引入方式,避免 v3 的 `@tailwind` 寫法。
---
以上為「點擊後 +1」Livewire 元件的完整規格。
確認需求無誤之後,我們根據這份規格書生成自動化測試。這邊的自動化測試會通過呼叫 Livewire Component 的 PHP 類別,來確定行為符合預期。
根據以下規格書生成自動化測試
docs/livewire-increment-counter.md
順利的話就會生成對應的自動化測試
<?php
declare(strict_types=1);
use Livewire\Volt\Volt as LivewireVolt;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
it('defaults to 0 and increments twice to 2', function () {
LivewireVolt::test('counter')
->assertSet('count', 0)
->call('increment')
->assertSet('count', 1)
->call('increment')
->assertSet('count', 2)
->assertSee('2');
});
it('respects initial prop', function () {
LivewireVolt::test('counter', ['initial' => 5])
->assertSet('count', 5)
->assertSee('5');
});
it('stops at max and does not increment beyond', function () {
LivewireVolt::test('counter', ['initial' => 5, 'max' => 6])
->assertSet('count', 5)
->call('increment')
->assertSet('count', 6)
->call('increment')
->assertSet('count', 6);
});
it('dispatches counter-incremented event on success', function () {
LivewireVolt::test('counter')
->call('increment')
->assertDispatched('counter-incremented', ['count' => 1]);
});
初步確認自動化測試符合我們的預期之後,我們就可以開始讓 AI 幫我們寫程式了
根據以下規格書撰寫對應的程式,並通過所有測試
docs/livewire-increment-counter.md
接著我們就會看到生成的 Livewire Volt 類別 resources/views/livewire/counter.blade.php
<?php
use Livewire\Volt\Component;
new class extends Component {
public int $count = 0;
public int $initial = 0;
public ?int $max = null;
public function mount(int $initial = 0, ?int $max = null): void
{
$this->initial = $initial;
$this->count = $initial;
$this->max = $max;
}
public function increment(): void
{
if ($this->max !== null && $this->count >= $this->max) {
return;
}
$this->count++;
$this->dispatch('counter-incremented', ['count' => $this->count]);
}
};
?>
<div class="inline-flex items-center gap-3">
<span class="text-2xl font-medium" aria-live="polite">{{ $count }}</span>
<button
type="button"
wire:click="increment"
aria-label="increase counter"
@if($max !== null && $count >= $max) disabled @endif
class="px-3 py-2 rounded bg-blue-600 text-white disabled:bg-gray-400"
>
+1
</button>
</div>
這邊的 wire:click
就是 livewire 設定的前端行為,如果被點擊了之後,就會呼叫 increment()
這個函數,並更新前端的 $count
變數。
不過寫好之後,我們發現 AI 沒有撰寫對應的路由,所以我們沒有任何地方可以看到這個元件。
沒關係,我們可以再和 AI 溝通一下
撰寫一個路由包含 counter.blade.php 以測試前端行為
AI 寫好了之後,我們發現竟然還是出現了錯誤
Call to a member function initials() on null
依照之前的經驗,這類錯誤通常是出現在取得使用者時,由於我們還沒有登入,所以取出的使用者是 null,如果程式沒有檢查就往下進行,就會出現類似這樣的錯誤訊息。如果要手動修正的話,我們可以一層層找 counter.blade.php 是在哪邊呼叫,並確認在這些地方我們有專門做未登入時應該要顯示的畫面。
如果我們沒有時間這樣除錯的話,也沒關係,由於 Laravel Boost 有專門的 MCP Server 可以讓 AI 知道我們的專案情況,所以我們不需要將所有程式碼複製進 AI 讓他找錯誤,只需要直接提供錯誤內容讓 AI 修正就好
出現錯誤
Call to a member function initials() on null
AI 會自動進入程式內找出錯誤的地方,並試著修正這些問題
1. Reproduce and identify where auth()->user() is used without guard.
2. Wrap all UI segments that access auth()->user() with @auth (or use optional helpers), ensuring no guest path calls methods on null.
3. Verify the layout component renders for guests on /counter without errors.
4. Run relevant tests (auth tests and counter feature test) to ensure no regressions.
5. Run Pint to ensure style is consistent.
6. Summarize changes and provide the counter demo URL for retest.
修正完錯誤之後,我們就可以看到按鈕的 Livewire Component 了!
我們沒有安裝任何前端框架,也沒有撰寫對應的 JavaScript,就做好了一個互動的元件!
今天的部分就到這邊,我們明天見!