Skip to content
全部文档

属性

属性用于在 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']; // [tl! highlight]
    }

    // ...
};

本例中,我们定义了一个空的 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( // [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 wire:key="{{ $loop->index }}">{{ $todo }}</li>
        @endforeach
    </ul>
</div>

上例中,点击「Add Todo」按钮时,文本输入的值会与服务端的 $todo 属性同步。

这只是 wire:model 的冰山一角。关于数据绑定的更多内容,请参阅我们的表单文档

重置属性

有时,用户执行某个操作后,你需要把属性重置回初始状态。这时可以用 Livewire 的 reset() 方法:传入一个或多个属性名,就会把它们的值重置为初始状态。

下面的示例中,点击「Add Todo」后用 $this->reset() 重置 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」后,刚添加完待办的输入框会清空,方便继续写下一条。

WARNING

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 组件中的每个属性会在请求之间被序列化(或称「脱水」dehydrated)成 JSON,再在下一次请求时从 JSON「注水」hydrated 回 PHP。

这种双向转换存在一定限制,因而约束了 Livewire 可用的属性类型。

原始类型

Livewire 支持字符串、整数等原始类型。它们能方便地与 JSON 互转,很适合作为 Livewire 组件属性。

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

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 对象类型。但要注意:这些类型在每次请求时都会被 dehydrated 成 JSON,再 hydrated 回 PHP。因此属性可能无法保留闭包等运行时值,对象的类名等信息也可能暴露给 JavaScript。

支持的 PHP 类型:

类型完整类名
BackedEnumBackedEnum
CollectionIlluminate\Support\Collection
Eloquent CollectionIlluminate\Database\Eloquent\Collection
ModelIlluminate\Database\Eloquent\Model
DateTimeDateTime
CarbonCarbon\Carbon
StringableIlluminate\Support\Stringable

WARNING

Eloquent Collection 与 Model

把 Eloquent Collection 和 Model 存进 Livewire 属性时,请注意这些限制:

  • 查询约束不会保留诸如 select(...) 的额外查询约束不会在后续请求中重新应用。详见 Eloquent 约束不会在请求之间保留
  • 性能影响把大型 Eloquent collection 存为属性可能带来性能问题,因为组件每次 hydrate 时 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 在浏览器中使用,你可以用 AlpineJS 访问并操作它们的 JavaScript 表示。

Alpine 是随 Livewire 一起提供的轻量 JavaScript 库。它能让你在 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>

用户输入时,当前待办文本的字符长度会在页面上实时更新,且全程不会向服务端发送网络请求。

操作属性

同样,你也可以用 $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>

乍看之下这个组件似乎没问题。但我们来看看攻击者如何利用它在应用里做未授权的事。

因为我们把 post 的 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 输入改成另一篇 post 的 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] 后,你可以假定该属性没有在组件类之外被篡改过。

关于锁定属性的更多信息,请参阅 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 能理解的格式。这种格式可能向浏览器暴露应用信息,包括属性名和类名。

例如,假设组件有一个公共属性 $post,里面是数据库中的 Post 模型实例。此时经网络发送的脱水值可能类似:

json
{
    "type": "model",
    "class": "App\Models\Post",
    "key": 1,
    "relationships": []
}

可以看到,$post 的脱水值包含模型类名(App\Models\Post),以及 ID 和已 eager-load 的关联关系。

若不想暴露模型类名,可以在服务提供者里用 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 collection 存为 Livewire 属性时,诸如 select(...) 的额外查询约束不会在后续请求中重新应用。

下面用带 select() 约束的 show-todos 组件来演示,该约束作用于 Todos Eloquent collection:

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 collection;但数据库每行只会查询并加载 titlecontent 字段到各个模型。

当 Livewire 在后续请求中把该属性的 JSON hydrate 回 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 wire:key="{{ $loop->index }}">{{ $todo }}</li>
    @endforeach
</ul>

注意:在视图里只能通过 $this 对象访问计算属性,例如 $this->todos

也可以在类内部访问 $todos。例如有一个 markAllAsComplete() action:

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]

因为计算属性有性能优势:在单次请求中首次使用后会自动 memoize。这样你可以在组件里随意访问 $this->todos,并确信实际方法只会调用一次,不会在同一请求里多次执行昂贵查询。

更多信息请参阅计算属性文档

另见