【Laravel實戰】15分鐘快速掌握Livewire的屬性觀念
大家好,我是哥布林工程師,今天來聊Livewire的核心觀念篇當中,非常重要的一個部分,那就是組件的屬性
組件公開屬性
首先你需要知道的是Livewire 組件是透過組件類別裡的公開屬性來存取資料,像是下面例子的 $message 屬性
//app/Http/Livewire/HelloWorld.php
class HelloWorld extends Component
{
public $message = 'Hello World!';
…
}
Livewire 類別的所有公開屬性將自動的能夠在視圖內去取用,一般不需要你自己去將它們手動傳入到視圖中。有一種狀況就是假如屬性並非公開屬性,但又有必要傳到視圖內,這時候就可以透過 view()的第二參數來傳入
//app/Http/Livewire/HelloWorld.php
class HelloWorld extends Component
{
public $message = 'Hello World!';
}
//resources/views/livewire/helloWorld.blade.php
<div>
<h1>{{ $message }}</h1>
<!-- Will output "Hello World!" -->
</div>
那關於公開屬性有一些細節你需要去注意:
- 屬性名稱請避開Livewire的保留字,比如:rules以及messages
- 存在 public 屬性的資料將會對前端JS開放,因此請勿儲存敏感的資料
- 屬性只能夠儲存JS所支持的資料型態,比如(string.int.array.boolean),又或者是以下的PHP型別(Stringable, Collection, DateTime, Model, EloquentCollection)
屬性如果沒有設為公開 比如 protected 或者是 private 將不會在 Livewire 更新的時候保存 所以請避免在這些屬性裡頭去儲存狀態
屬性初始化
接著來聊聊屬性初始化的做法,你能夠透過組件的 mount() 來進行屬性的初始化,比如下面這個例子:
//app/Http/Livewire/HelloWorld.php
class HelloWorld extends Component
{
public $message;
public function mount()
{
$this->message = 'Hello World!';
}
}
假如你覺得逐一的進行屬性賦值太醜,你也可以改用 $this->fill() 來做,比如下面這個例子:
public function mount()
{
$this->fill(['message' => 'Hello World!']);
}
除此之外,Livewire也提供了 $this->reset() 來幫助你重設公開屬性的值。這在你執行完某些操作需要重置屬性值的時候很有用
public $search = '';
public $isActive = true;
public function resetFilters()
{
$this->reset('search');
//重置 search 屬性
$this->reset(['search', 'isActive']);
//重置 search 和 isActive 屬性
}
資料綁定
假如你用過前端框架像是 Vue 或是 Angular,你應該就已經熟悉MVVM的觀念了
然而如果你不是太理解的話,資料綁定就是 Livewire 能夠綁定組件的公開屬性到某些 HTML 元素的屬性上。當屬性被變更時,這些元素的屬性也會跟著異動,彷彿彼此之間有心電感應一般,比如下面的這個例子:
//app/Http/Livewire/HelloWorld.php
class HelloWorld extends Component
{
public $message;
}
//resources/views/livewire/helloWorld.blade.php
<div>
<input wire:model="message" type="text">
<h1>{{ $message }}</h1>
</div>
像這個例子,當使用者在輸入項輸入內容就會很神奇的發現下面的H1標籤內容跟著異動,一切就像是魔法一樣,這就是資料綁定的效果
它的原理是,Livewire將會監聽元素的輸入事件。當觸發時,就會發出一個 AJAX 請求,從伺服器取得新資料之後再重新渲染該組件視圖
要知道的是你能夠加入 wire:model 到任何會派發 input 事件的元素。哪怕是自定義元素甚至是第三方 JS 函式庫都可以,一般能使用 wire:model 的元素包含以下這些輸入項:
<input type="text">
<input type="radio">
<input type="checkbox">
<select>
<textarea>
綁定巢狀資料
接下來,我們聊聊綁定巢狀資料的這個話題
Livewire 支持綁定陣列裡頭的巢狀資料,主要是透過點語法的方式,下面是一個例子:
<input type="text" wire:model="parent.message">
你甚至可以在組件中直接綁定到模型屬性,但請注意這個功能只支持 PHP 7.4以上版本,如果版本不足的話可是會出現語法錯誤的唷
假如一個 Eloquent 模型被作為公開屬性儲存在 Livewire 組件,你能夠直接綁定它,這是一個例子
視圖有直接綁定到模型的 "title" 以及 "content" 屬性,Livewire 將會為你搞定模型的解構與重構等工作
//app/Http/Livewire/PostForm.php
use App\Post;
class PostForm extends Component
{
public Post $post;
protected $rules = [
'post.title' => 'required|string|min:6',
'post.content' => 'required|string|max:500',
];
public function save()
{
$this->validate();
$this->post->save();
}
}
//resources/views/livewire/postForm.blade.php
<form wire:submit.prevent="save">
<input type="text" wire:model="post.title">
<textarea wire:model="post.content"></textarea>
<button type="submit">Save</button>
</form>
這裡可以注意到一個名為 $rules 的屬性。要在 wire:model 使用 Route Model Binding ,你必須定義每一個需要取用模型屬性的驗證規則,否則將會跳出錯誤,甚至你還可以在 Eloquent 集合裡去綁定模型
除此之外公開屬性為 Eloquent Model 的屬性,考量到安全因素,有想要對前端公開的欄位都必須要列在 $rules 屬性內,否則前端是得不到值的
//app/Http/Livewire/PostForm.php
use App\Post;
class PostForm extends Component
{
public $posts;
protected $rules = [
'posts.*.title' => 'required|string|min:6',
'posts.*.content' => 'required|string|max:500',
];
public function mount()
{
$this->posts = auth()->user()->posts;
}
public function save()
{
$this->validate();
foreach ($this->posts as $post) {
$post->save();
}
}
}
//resources/views/livewire/postForm.blade.php
<form wire:submit.prevent="save">
@foreach ($posts as $index => $post)
<div wire:key="post-field-{{ $post->id }}">
<input type="text" wire:model="posts.{{ $index }}.title">
<textarea wire:model="posts.{{ $index }}.content"></textarea>
</div>
@endforeach
<button type="submit">Save</button>
</form>
取消延遲輸入
如果你有取消延遲輸入 Debouncing Input 的需求,沒問題!
什麼是延遲輸入呢?
預設 Livewire 會採用一個 150ms 的文字輸入延遲以避免當使用者在輸入文字時產生過多的網頁請求,因為使用者每打一個字元就會產生一次請求
假如你想要覆寫此預設行為,Livewire 提供你一個名為 "debounce" 的修飾字。比如你想要將輸入項的延遲時間改為0.5秒,可以加入這個修飾字像這樣:
<input type="text" wire:model.debounce.500ms="name">
Lazy Update
另外,Livewire也是有支持 Lazy Update , 我個人稱之為懶惰更新,它的目的也是為了降低服務器的請求量,效果比取消延遲輸入更好一些
預設 Livewire會在每一個輸入事件觸發之後發出請求,這對於像下拉選單這種輸入項不至於發出過快或過多的請求沒啥問題, 然而對於文字輸入項的話就沒有必要這麼頻繁了
所以針對文字輸入項比如 textfield 使用 lazy 修飾字來限定監聽 change 事件
<input type="text" wire:model.lazy="message">
現在的 $message 屬性將只會在使用者點到其他的輸入項導致觸發 change事件時才會被更新
延遲更新
除了可以延遲輸入以外,你還可以做延遲更新!它的目的同樣是為了降低服務器的請求量,效果是剛談過的這幾個作法當中最好的一個
假如你不需要資料被同步的更新,而只需要跟著下一次的網頁請求再做更新的話。Livewire 提供了 .defer 修飾字來滿足你的需求 下面是一個例子:
<input type="text" wire:model.defer="query">
<button wire:click="search">Search</button>
當使用者輸入內容到文字輸入項將不會有任何網路請求被發出,儘管使用者點到其他輸入項來觸發 change 事件也一樣。
但當使用者按下 Search 按鈕,Livewire將發出一個網路請求,同時包含新的查詢狀態.以及 Search 行為,這將大幅度的縮減不必要的網路使用量
計算屬性
Livewire 提供一個用來取用動態屬性的API。透過計算屬性, Vue 也有相同概念的機制。這特別適合用於取用來自於資料庫或者是快取的內容,請看這個範例
//app/Http/Livewire/ShowPost.php
class ShowPost extends Component
{
// Computed Property
public function getPostProperty()
{
return Post::find($this->postId);
}
}
加入了getPostProperty()之後,現在你可以從組件類別或視圖中去取用 $this->post
//app/Http/Livewire/ShowPost.php
class ShowPost extends Component
{
public $postId;
public function getPostProperty()
{
return Post::find($this->postId);
}
public function deletePost()
{
$this->post->delete();
}
}
<div>
<h1>{{ $this->post->title }}</h1>
...
<button wire:click="deletePost">Delete Post</button>
</div>
根據官方文件的記載,計算屬性會作為單獨的 Livewire 請求生命週期來進行快取。也就是說,假如你在Blade視圖呼叫 $this->post 五次之後,它將不會再對資料庫進行查詢而是從快取中取出
問題排除
Livewire\Exceptions\CorruptComponentPayloadException
當公開屬性為陣列時出現以下問題
Livewire encountered corrupt data when trying to hydrate the [checkout] component. Ensure that the [name, id, data] of the Livewire component wasn't tampered with between requests.
可能原因
當屬性為公開時,Livewire就會和前端javascript來進行資料交換,而陣列格式的資料,其key在javascript並不支持 "named",只支持 "numeric" 也就是數字型態。換句話說,key會被轉成數字型態並重新排序。這時就可能造成這樣的問題
可能解決方法
除非是需要雙向綁定的屬性,否則請避免使用 公開屬性。建議把出問題的屬性改為 protected 甚至是 private,視圖用到的話可以在 render() 裡頭回傳 view() 時,以第二參數的方式傳給視圖,如下例所示:
//app\Http\Livewire\MemberPage.php
public function render()
{
$data = ['key' => 'value'];
return view('member-page',compact('data'));
}



