Skip to content

属性

属性用于在 Livewire 组件内部存储和管理数据。它们被定义为组件类的公共属性,可以在服务器端和客户端进行访问和修改。

初始化属性

你可以在组件的 mount() 方法中设置属性的初始值。

考虑以下示例:

php
<?php // resources/views/components/⚡todos.blade.php

use Illuminate\Support\Facades\Auth;
use Livewire\Component;

new class extends Component
{
    public $todos = [];

    public $todo = '';

    public function mount()
    {
        $this->todos = Auth::user()->todos; // [tl! highlight]
    }

    // ...
};

在这个示例中,我们定义了一个空的 todos 数组,并使用已认证用户的现有待办事项进行初始化。现在,当组件首次渲染时,数据库中所有现有的待办事项都会展示给用户。

批量赋值

有时在 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( // [tl! highlight]
            $post->only('title', 'description'), // [tl! highlight]
        ); // [tl! highlight]
    }

    // ...
};

因为 $post->only(...) 基于你传入的名称返回一个模型属性和值的关联数组,所以 $title$description 属性将被初始设置为数据库中 $post 模型的 titledescription,而无需逐个单独设置。

数据绑定

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..."> <!-- [tl! highlight] -->

    <button wire:click="add">Add Todo</button>

    <ul>
        @foreach ($todos as $todo)
            <li>{{ $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'); // [tl! highlight]
    }

    // ...
};

在上面的示例中,当用户点击 "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'); // [tl! highlight]
    }

    // ...
};

上面的示例拉取了单个值,但 pull() 也可以用于重置和检索(作为键值对)所有或部分属性:

php
// 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 相互转换,使它们成为 Livewire 组件中属性的理想选择。

Livewire 支持以下原始属性类型:ArrayStringIntegerFloatBooleanNull

php
new class 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 类型:

TypeFull Class Name
BackedEnumBackedEnum
CollectionIlluminate\Support\Collection
Eloquent CollectionIlluminate\Database\Eloquent\Collection
ModelIlluminate\Database\Eloquent\Model
DateTimeDateTime
CarbonCarbon\Carbon
StringableIlluminate\Support\Stringable

Eloquent 集合和模型

当在 Livewire 属性中存储 Eloquent 集合和模型时,额外的查询约束(如 select(...))将不会在后续请求中重新应用。

查看 Eloquent 约束在请求之间不会保留获取更多详细信息

以下是将属性设置为这些各种类型的快速示例:

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 在浏览器中使用,你可以从 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 输入字段的实时字符计数:

blade
<div>
    <input type="text" wire:model="todo">

    Todo character length: <h2 x-text="$wire.todo.length"></h2>
</div>

当用户在字段中输入时,正在编写的当前待办事项的字符长度将在页面上显示并实时更新,所有这些都无需向服务器发送网络请求。

如果你愿意,可以使用更明确的 .get() 方法来实现同样的效果:

blade
<div>
    <input type="text" wire:model="todo">

    Todo character length: <h2 x-text="$wire.get('todo').length"></h2>
</div>

操作属性

同样,你可以使用 $wire 在 JavaScript 中操作 Livewire 组件属性。

例如,让我们向 todos 组件添加一个"清除"按钮,允许用户仅使用 JavaScript 重置输入字段:

blade
<div>
    <input type="text" wire:model="todo">

    <button x-on:click="$wire.todo = ''">Clear</button>
</div>

用户点击"清除"后,输入将被重置为空字符串,而无需向服务器发送网络请求。

在后续请求中,$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 作为组件上的公共属性存储,它可以像 titlecontent 属性一样在客户端被操作。

我们没有编写带有 wire:model="id" 的输入并不重要。恶意用户可以轻松地使用其浏览器 DevTools 将视图更改为以下内容:

blade
<form wire:submit="update">
    <input type="text" wire:model="id"> <!-- [tl! highlight] -->
    <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); // [tl! highlight]

    $post->update(...);
}

如果恶意用户修改了 $id 属性,添加的授权将捕获它并抛出错误。

锁定属性

Livewire 还允许你"锁定"属性以防止属性在客户端被修改。你可以使用 #[Locked] 属性"锁定"属性以防止客户端操作:

php
use Livewire\Attributes\Locked;
use Livewire\Component;

new class extends Component
{
    #[Locked] // [tl! highlight]
    public $id;

    // ...
};

现在,如果用户尝试在前端修改 $id,将抛出错误。

通过使用 #[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; // [tl! highlight]
    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 理解的格式。这种格式可能会向浏览器暴露有关应用程序的信息,包括属性的名称和类名。

例如,假设你有一个 Livewire 组件,它定义了一个名为 $post 的公共属性。此属性包含数据库中 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": "App\Models\Post", // [tl! remove]
    "class": "post", // [tl! add]
    "key": 1,
    "relationships": []
}

Eloquent 约束在请求之间不会保留

通常,Livewire 能够在请求之间保留和重新创建服务器端属性;但是,在某些情况下,在请求之间保留值是不可能的。

例如,当将 Eloquent 集合存储为 Livewire 属性时,额外的查询约束(如 select(...))将不会在后续请求中重新应用。

为了演示,考虑以下带有 select() 约束应用于 Todos Eloquent 集合的 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']) // [tl! highlight]
            ->get();
    }
};

当此组件最初加载时,$todos 属性将被设置为用户待办事项的 Eloquent 集合;但是,数据库中每一行只有 titlecontent 字段会被查询并加载到每个模型中。

当 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] // [tl! highlight]
    public function todos()
    {
        return Auth::user()
            ->todos()
            ->select(['title', 'content'])
            ->get();
    }
};

以下是如何从 Blade 视图访问这些 todos:

blade
<ul>
    @foreach ($this->todos as $todo)
        <li>{{ $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() // [tl! highlight:3]
    {
        $this->todos->each->complete();
    }
};

你可能想知道为什么不直接在需要的地方调用 $this->todos() 作为方法?为什么首先要使用 #[Computed]

原因是计算属性具有性能优势,因为它们在单个请求期间首次使用后会自动缓存。这意味着你可以在组件中自由访问 $this->todos,并确保实际方法只会被调用一次,这样你就不会在同一个请求中多次运行昂贵的查询。

有关更多信息,请访问计算属性文档