属性
属性存储和管理 Livewire 组件中的数据。它们被定义为组件类上的公共属性,并且可以在服务器和客户端访问和修改。
初始化属性
您可以在组件的 mount()
方法中为属性设置初始值。
考虑以下示例
<?php namespace App\Livewire; use Illuminate\Support\Facades\Auth;use Livewire\Component; class TodoList extends Component{ public $todos = []; public $todo = ''; public function mount() { $this->todos = Auth::user()->todos; } // ...}
在此示例中,我们定义了一个空的 todos
数组,并使用已认证用户的现有待办事项对其进行初始化。现在,当组件首次渲染时,数据库中所有现有的待办事项都会显示给用户。
批量分配
有时在 mount()
方法中初始化许多属性会感觉很冗长。为了解决这个问题,Livewire 提供了一种便捷的方法,可以通过 fill()
方法一次分配多个属性。通过传递属性名称及其各自值的关联数组,您可以同时设置多个属性,并减少 mount()
中重复的代码行。
例如
<?php namespace App\Livewire; use Livewire\Component;use App\Models\Post; class UpdatePost extends Component{ public $post; public $title; public $description; public function mount(Post $post) { $this->post = $post; $this->fill( $post->only('title', 'description'), ); } // ...}
由于 $post->only(...)
根据您传入的名称返回模型属性和值的关联数组,因此 $title
和 $description
属性将最初设置为数据库中 $post
模型的 title
和 description
,而无需单独设置每个属性。
数据绑定
Livewire 通过 wire:model
HTML 属性支持双向数据绑定。这使您可以轻松地在组件属性和 HTML 输入之间同步数据,从而保持用户界面和组件状态同步。
让我们使用 wire:model
指令将 TodoList
组件中的 $todo
属性绑定到一个基本输入元素
<?php namespace App\Livewire; use Livewire\Component; class TodoList extends Component{ public $todos = []; public $todo = ''; public function add() { $this->todos[] = $this->todo; $this->todo = ''; } // ...}
<div> <input type="text" wire:model="todo" placeholder="Todo..."> <button wire:click="add">Add Todo</button> <ul> @foreach ($todos as $todo) <li>{{ $todo }}</li> @endforeach </ul></div>
在上面的示例中,当单击“添加待办事项”按钮时,文本输入的值将与服务器上的 $todo
属性同步。
这只是 wire:model
的皮毛。有关数据绑定的更深入信息,请查看我们的表单文档。
重置属性
有时,您可能需要在用户执行操作后将属性重置为其初始状态。在这些情况下,Livewire 提供了一个 reset()
方法,该方法接受一个或多个属性名称,并将其值重置为其初始状态。
在下面的示例中,我们可以在单击“添加待办事项”按钮后使用 $this->reset()
重置 todo
字段,从而避免代码重复
<?php namespace App\Livewire; use Livewire\Component; class ManageTodos extends Component{ public $todos = []; public $todo = ''; public function addTodo() { $this->todos[] = $this->todo; $this->reset('todo'); } // ...}
在上面的示例中,在用户单击“添加待办事项”后,保存刚刚添加的待办事项的输入字段将被清除,允许用户编写新的待办事项。
reset()
不适用于在 mount()
中设置的值
reset()
会将属性重置为调用 mount()
方法之前的状态。如果您在 mount()
中将属性初始化为不同的值,则需要手动重置属性。
拉取属性
或者,您可以使用 pull()
方法在一个操作中重置和检索值。
以下是上面相同的示例,但使用 pull()
进行了简化
<?php namespace App\Livewire; use Livewire\Component; class ManageTodos extends Component{ public $todos = []; public $todo = ''; public function addTodo() { $this->todos[] = $this->pull('todo'); } // ...}
上面的示例拉取单个值,但 pull()
也可用于重置和检索(作为键值对)所有或部分属性
// The same as $this->all() and $this->reset();$this->pull(); // The same as $this->only(...) and $this->reset(...);$this->pull(['title', 'content']);
支持的属性类型
Livewire 支持有限的一组属性类型,因为它采用独特的方法来管理服务器请求之间的组件数据。
Livewire 组件中的每个属性在请求之间序列化或“脱水”为 JSON,然后在下一个请求中从 JSON “补水”回 PHP。
这种双向转换过程有一定的限制,限制了 Livewire 可以处理的属性类型。
原始类型
Livewire 支持原始类型,例如字符串、整数等。这些类型可以轻松地转换为 JSON 和从 JSON 转换,使其非常适合用作 Livewire 组件中的属性。
Livewire 支持以下原始属性类型:Array
、String
、Integer
、Float
、Boolean
和 Null
。
class TodoList extends Component{ public $todos = []; // Array public $todo = ''; // String public $maxTodos = 10; // Integer public $showTodos = false; // Boolean public $todoFilter; // Null}
常见的 PHP 类型
除了原始类型外,Livewire 还支持 Laravel 应用程序中使用的常见 PHP 对象类型。但是,需要注意的是,这些类型将在每次请求中脱水为 JSON,并补水回 PHP。这意味着该属性可能无法保留运行时值,例如闭包。此外,有关对象的信息(例如类名)可能会暴露给 JavaScript。
支持的 PHP 类型
类型 | 完整类名 |
---|---|
BackedEnum | BackedEnum |
集合 | Illuminate\Support\Collection |
Eloquent 集合 | Illuminate\Database\Eloquent\Collection |
模型 | Illuminate\Database\Model |
日期时间 | 日期时间 |
Carbon | Carbon\Carbon |
Stringable | Illuminate\Support\Stringable |
在 Livewire 属性中存储 Eloquent 集合和模型时,后续请求将不会重新应用附加查询约束,例如 select(...)。
有关更多详细信息,请参阅 Eloquent 约束不会在请求之间保留
以下是设置各种类型的属性的快速示例
public function mount(){ $this->todos = collect([]); // Collection $this->todos = Todos::all(); // Eloquent Collection $this->todo = Todos::first(); // Model $this->date = new DateTime('now'); // DateTime $this->date = new Carbon('now'); // Carbon $this->todo = str(''); // Stringable}
支持自定义类型
Livewire 允许你的应用程序通过两种强大的机制来支持自定义类型
- 可连接对象
- 合成器
可连接对象简单易用,适用于大多数应用程序,因此我们将在下面探讨它们。如果你是一位高级用户或希望获得更多灵活性的包作者,合成器是实现这一目标的方法。
可连接对象
可连接对象是你应用程序中实现 `Wireable` 接口的任何类。
例如,假设你在应用程序中有一个 `Customer` 对象,其中包含有关客户的主要数据
class Customer{ protected $name; protected $age; public function __construct($name, $age) { $this->name = $name; $this->age = $age; }}
尝试将此类的实例设置为 Livewire 组件属性将导致一个错误,提示你 `Customer` 属性类型不受支持
class ShowCustomer extends Component{ public Customer $customer; public function mount() { $this->customer = new Customer('Caleb', 29); }}
但是,你可以通过实现 `Wireable` 接口并在类中添加 `toLivewire()` 和 `fromLivewire()` 方法来解决此问题。这些方法告诉 Livewire 如何将此类型的属性转换为 JSON,然后再转换回来
use Livewire\Wireable; class Customer implements Wireable{ protected $name; protected $age; public function __construct($name, $age) { $this->name = $name; $this->age = $age; } public function toLivewire() { return [ 'name' => $this->name, 'age' => $this->age, ]; } public static function fromLivewire($value) { $name = $value['name']; $age = $value['age']; return new static($name, $age); }}
现在,你可以在 Livewire 组件上自由设置 `Customer` 对象,Livewire 将知道如何将这些对象转换为 JSON,然后再转换为 PHP。
如前所述,如果你想更全面、更有效地支持类型,Livewire 提供了合成器,这是其用于处理不同属性类型的先进内部机制。 了解有关合成器的更多信息。
从 JavaScript 访问属性
由于 Livewire 属性也可以通过 JavaScript 在浏览器中使用,因此你可以从 AlpineJS 访问和操作它们的 JavaScript 表示形式。
Alpine 是一个轻量级的 JavaScript 库,与 Livewire 一起提供。Alpine 提供了一种在 Livewire 组件中构建轻量级交互的方法,而无需进行完整的服务器往返。
在内部,Livewire 的前端建立在 Alpine 之上。事实上,每个 Livewire 组件实际上都是底层的 Alpine 组件。这意味着你可以在 Livewire 组件中自由使用 Alpine。
本页的其余部分假定你基本熟悉 Alpine。如果你不熟悉 Alpine,请参阅 Alpine 文档。
访问属性
Livewire 向 Alpine 公开了一个神奇的 `$wire` 对象。你可以在 Livewire 组件内的任何 Alpine 表达式中访问 `$wire` 对象。
`$wire` 对象可以像 Livewire 组件的 JavaScript 版本一样对待。它具有与组件的 PHP 版本相同的所有属性和方法,但还包含一些专用方法,用于在模板中执行特定函数。
例如,我们可以使用 $wire
来显示 todo
输入字段的实时字符数
<div> <input type="text" wire:model="todo"> Todo character length: <h2 x-text="$wire.todo.length"></h2></div>
当用户在字段中输入时,正在编写的当前待办事项的字符长度将显示在页面上并实时更新,所有这些都不需要向服务器发送网络请求。
如果你愿意,可以使用更明确的 .get()
方法来完成相同的事情
<div> <input type="text" wire:model="todo"> Todo character length: <h2 x-text="$wire.get('todo').length"></h2></div>
操作属性
同样,你可以使用 $wire
在 JavaScript 中操作 Livewire 组件属性。
例如,让我们向 TodoList
组件添加一个“清除”按钮,以允许用户仅使用 JavaScript 重置输入字段
<div> <input type="text" wire:model="todo"> <button x-on:click="$wire.todo = ''">Clear</button></div>
用户单击“清除”后,输入将重置为空字符串,而无需向服务器发送网络请求。
在后续请求中,$todo
的服务器端值将被更新并同步。
如果你愿意,还可以使用更明确的 .set()
方法来设置客户端属性。但是,你应该注意,默认情况下使用 .set()
会立即触发网络请求并将状态与服务器同步。如果需要,这是一个非常好的 API
<button x-on:click="$wire.set('todo', '')">Clear</button>
为了在不向服务器发送网络请求的情况下更新属性,你可以传递第三个布尔参数。这将延迟网络请求,并且在后续请求中,状态将在服务器端同步
<button x-on:click="$wire.set('todo', '', false)">Clear</button>
安全问题
虽然 Livewire 属性是一个强大的功能,但在使用它们之前,你应该了解一些安全注意事项。
简而言之,始终将公共属性视为用户输入——就像它们是从传统端点请求的输入一样。鉴于此,在将属性持久保存到数据库之前,必须对其进行验证和授权——就像在控制器中处理请求输入时所做的那样。
不要信任属性值
为了演示忽视授权和验证属性如何在你的应用程序中引入安全漏洞,以下 UpdatePost
组件容易受到攻击
<?php namespace App\Livewire; use Livewire\Component;use App\Models\Post; class UpdatePost extends Component{ public $id; public $title; public $content; public function mount(Post $post) { $this->id = $post->id; $this->title = $post->title; $this->content = $post->content; } public function update() { $post = Post::findOrFail($this->id); $post->update([ 'title' => $this->title, 'content' => $this->content, ]); session()->flash('message', 'Post updated successfully!'); } public function render() { return view('livewire.update-post'); }}
<form wire:submit="update"> <input type="text" wire:model="title"> <input type="text" wire:model="content"> <button type="submit">Update</button></form>
乍一看,这个组件可能看起来完全没问题。但是,让我们了解攻击者如何使用该组件在你的应用程序中执行未经授权的操作。
因为我们将帖子的 id
作为组件上的公共属性存储,所以它可以在客户端上进行操作,就像 title
和 content
属性一样。
我们没有使用 wire:model="id"
编写输入并不重要。恶意用户可以使用其浏览器 DevTools 轻松地将视图更改为以下内容
<form wire:submit="update"> <input type="text" wire:model="id"> <input type="text" wire:model="title"> <input type="text" wire:model="content"> <button type="submit">Update</button></form>
现在,恶意用户可以将 id
输入更新为其他帖子模型的 ID。当提交表单并调用 update()
时,Post::findOrFail()
将返回并更新用户不是所有者的帖子。
为了防止这种攻击,我们可以使用以下一种或两种策略
- 授权输入
- 锁定属性以防止更新
授权输入
因为 $id
可以通过类似 wire:model
的客户端操作,就像在控制器中一样,我们可以使用 Laravel 授权 来确保当前用户可以更新帖子
public function update(){ $post = Post::findOrFail($this->id); $this->authorize('update', $post); $post->update(...);}
如果恶意用户改变了 $id
属性,添加的授权将捕获它并抛出一个错误。
锁定属性
Livewire 还允许你“锁定”属性以防止属性在客户端被修改。你可以使用 #[Locked]
属性“锁定”一个属性以防止客户端操作
use Livewire\Attributes\Locked;use Livewire\Component; class UpdatePost extends Component{ #[Locked] public $id; // ...}
现在,如果用户尝试在前端修改 $id
,将会抛出一个错误。
通过使用 #[Locked]
,你可以假设此属性未在组件类外部的任何地方被操作。
有关锁定属性的更多信息,请参阅锁定属性文档。
Eloquent 模型和锁定
当一个 Eloquent 模型被分配给 Livewire 组件属性时,Livewire 将自动锁定该属性并确保 ID 不被更改,这样你就可以免受此类攻击
<?php namespace App\Livewire; use Livewire\Component;use App\Models\Post; class UpdatePost extends Component{ public Post $post; public $title; public $content; public function mount(Post $post) { $this->post = $post; $this->title = $post->title; $this->content = $post->content; } public function update() { $this->post->update([ 'title' => $this->title, 'content' => $this->content, ]); session()->flash('message', 'Post updated successfully!'); } public function render() { return view('livewire.update-post'); }}
属性向浏览器公开系统信息
另一件需要记住的重要事情是,Livewire 属性在发送到浏览器之前会被序列化或“脱水”。这意味着它们的值被转换为可以通过网络发送并被 JavaScript 理解的格式。此格式可以向浏览器公开有关你的应用程序的信息,包括你的属性的名称和类名。
例如,假设你有一个 Livewire 组件,它定义了一个名为 $post
的公共属性。此属性包含来自你的数据库的 Post
模型的一个实例。在这种情况下,通过网络发送的此属性的脱水值可能如下所示
{ "type": "model", "class": "App\Models\Post", "key": 1, "relationships": []}
如你所见,$post
属性的脱水值包括模型的类名 (App\Models\Post
) 以及 ID 和任何已急切加载的关系。
如果你不想公开模型的类名,可以使用服务提供程序中的 Laravel 的“morphMap”功能为模型类名分配一个别名
<?php namespace App\Providers; use Illuminate\Support\ServiceProvider;use Illuminate\Database\Eloquent\Relations\Relation; class AppServiceProvider extends ServiceProvider{ public function boot() { Relation::morphMap([ 'post' => 'App\Models\Post', ]); }}
现在,当 Eloquent 模型“脱水”(序列化)时,不会公开原始类名,只会公开“post”别名
{ "type": "model",- "class": "App\Models\Post", + "class": "post", "key": 1, "relationships": [] }
Eloquent 约束不会在请求之间保留
通常情况下,Livewire 能够在请求之间保留和重新创建服务器端属性;但是,在某些情况下,在请求之间保留值是不可能的。
例如,当将 Eloquent 集合存储为 Livewire 属性时,附加查询约束(如 select(...)
)不会在后续请求中重新应用。
为了演示,请考虑以下 ShowTodos
组件,其中对 Todos
Eloquent 集合应用了 select()
约束
<?php namespace App\Livewire; use Illuminate\Support\Facades\Auth;use Livewire\Component; class ShowTodos extends Component{ public $todos; public function mount() { $this->todos = Auth::user() ->todos() ->select(['title', 'content']) ->get(); } public function render() { return view('livewire.show-todos'); }}
当最初加载此组件时,$todos
属性将被设置为用户待办事项的 Eloquent 集合;但是,数据库中每行的 title
和 content
字段将被查询并加载到每个模型中。
当 Livewire 在后续请求中将此属性的 JSON 水化回 PHP 时,select 约束将丢失。
为了确保 Eloquent 查询的完整性,我们建议你使用 计算属性,而不是属性。
计算属性是组件中的方法,用 #[Computed]
属性标记。它们可以作为动态属性访问,这些属性不存储为组件状态的一部分,而是动态求值。
下面是使用计算属性重新编写的上述示例
<?php namespace App\Livewire; use Illuminate\Support\Facades\Auth;use Livewire\Attributes\Computed;use Livewire\Component; class ShowTodos extends Component{ #[Computed] public function todos() { return Auth::user() ->todos() ->select(['title', 'content']) ->get(); } public function render() { return view('livewire.show-todos'); }}
下面是访问 Blade 视图中这些 todos 的方法
<ul> @foreach ($this->todos as $todo) <li>{{ $todo }}</li> @endforeach</ul>
请注意,在视图中,你只能像这样访问 $this
对象上的计算属性:$this->todos
。
你还可以从类内部访问 $todos
。例如,如果你有一个 markAllAsComplete()
操作
<?php namespace App\Livewire; use Illuminate\Support\Facades\Auth;use Livewire\Attributes\Computed;use Livewire\Component; class ShowTodos extends Component{ #[Computed] public function todos() { return Auth::user() ->todos() ->select(['title', 'content']) ->get(); } public function markAllComplete() { $this->todos->each->complete(); } public function render() { return view('livewire.show-todos'); }}
你可能会想,为什么不直接在需要的地方将 $this->todos()
作为方法调用?为什么要首先使用 #[Computed]
?
原因是计算属性具有性能优势,因为它们在单个请求期间首次使用后会自动缓存。这意味着你可以在组件中自由访问 $this->todos
,并确信实际方法只会被调用一次,这样你就不会在同一个请求中多次运行昂贵的查询。
有关更多信息,请访问计算属性文档。