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