Skip to content
全部文档

操作

Livewire 操作(actions)是组件上的方法,可由点击按钮或提交表单等前端交互触发。它们带来了能直接从浏览器调用 PHP 方法的开发体验,让你专注于应用逻辑,而不必纠缠于连接前后端的样板代码。

下面来看一个调用 save 操作的基本示例:

php
<?php // resources/views/components/post/⚡create.blade.php

use Livewire\Component;
use App\Models\Post;

new class extends Component {
    public $title = '';

    public $content = '';

    public function save()
    {
        Post::create([
            'title' => $this->title,
            'content' => $this->content,
        ]);

        return redirect()->to('/posts');
    }
};
?>

<form wire:submit="save"> <!-- [tl! highlight] -->
    <input type="text" wire:model="title">

    <textarea wire:model="content"></textarea>

    <button type="submit">Save</button>
</form>

在上例中,当用户点击「Save」提交表单时,wire:submit 会拦截 submit 事件,并在服务器上调用 save() 操作。

本质上,操作让你轻松把用户交互映射到服务端功能,而不必手动提交和处理 AJAX 请求。

传递参数

Livewire 允许你从 Blade 模板向组件中的操作传递参数,从而在调用操作时从前端提供额外数据或状态。

例如,假设你有一个允许用户删除文章的 ShowPosts 组件。你可以把文章 ID 作为参数传给 Livewire 组件中的 delete() 操作,然后由该操作取出对应文章并从数据库删除:

php
<?php // resources/views/components/post/⚡index.blade.php

use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Post;

new class extends Component {
    #[Computed]
    public function posts()
    {
        return Auth::user()->posts;
    }

    public function delete($id)
    {
        $post = Post::findOrFail($id);

        $this->authorize('delete', $post);

        $post->delete();
    }
};
blade
<div>
    @foreach ($this->posts as $post)
        <div wire:key="{{ $post->id }}">
            <h1>{{ $post->title }}</h1>
            <span>{{ $post->content }}</span>

            <button wire:click="delete({{ $post->id }})">Delete</button> <!-- [tl! highlight] -->
        </div>
    @endforeach
</div>

对于 ID 为 2 的文章,上面 Blade 模板中的「Delete」按钮在浏览器中会渲染为:

blade
<button wire:click="delete(2)">Delete</button>

点击该按钮时会调用 delete() 方法,并传入值为「2」的 $id

WARNING

不要信任操作参数

操作参数应像 HTTP 请求输入一样对待,也就是说参数值不可信任。在数据库中更新实体之前,你应始终授权确认所有权。

更多信息请参阅关于安全注意事项与最佳实践的文档。

作为额外便利,你可以根据传给操作的模型 ID 自动解析 Eloquent 模型。这与路由模型绑定非常相似。入门做法是:用模型类为操作参数加上类型提示,对应模型会自动从数据库取出并传给操作,而不是传入 ID:

php
<?php // resources/views/components/post/⚡index.blade.php

use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Post;

new class extends Component {
    #[Computed]
    public function posts()
    {
        return Auth::user()->posts;
    }

    public function delete(Post $post) // [tl! highlight]
    {
        $this->authorize('delete', $post);

        $post->delete();
    }
};

依赖注入

你可以在操作签名中通过类型提示,利用 Laravel 的依赖注入系统。Livewire 与 Laravel 会自动从容器解析操作的依赖:

php
<?php // resources/views/components/post/⚡index.blade.php

use Illuminate\Support\Facades\Auth;
use App\Repositories\PostRepository;
use Livewire\Attributes\Computed;
use Livewire\Component;

new class extends Component {
    #[Computed]
    public function posts()
    {
        return Auth::user()->posts;
    }

    public function delete(PostRepository $posts, $postId) // [tl! highlight]
    {
        $posts->deletePost($postId);
    }
};
blade
<div>
    @foreach ($this->posts as $post)
        <div wire:key="{{ $post->id }}">
            <h1>{{ $post->title }}</h1>
            <span>{{ $post->content }}</span>

            <button wire:click="delete({{ $post->id }})">Delete</button> <!-- [tl! highlight] -->
        </div>
    @endforeach
</div>

在本例中,delete() 方法会先通过 Laravel 服务容器解析得到 PostRepository 实例,再接收传入的 $postId 参数。

事件监听器

Livewire 支持多种事件监听器,让你能响应各类用户交互:

监听器说明
wire:click元素被点击时触发
wire:submit表单提交时触发
wire:keydown按下按键时触发
wire:keyup松开按键时触发
wire:mouseenter鼠标进入元素时触发
wire:*wire: 后的任意文本都会作为监听器的事件名

由于 wire: 后的事件名可以是任意内容,Livewire 支持你需要监听的任何浏览器事件。例如,要监听 transitionend,可以使用 wire:transitionend

监听特定按键

你可以使用 Livewire 提供的便捷别名,将按键事件监听缩小到特定按键或按键组合。

例如,用户在搜索框输入后按下 Enter 时执行搜索,可以使用 wire:keydown.enter

blade
<input wire:model="query" wire:keydown.enter="searchPosts">

你可以在第一个别名后再串联更多按键别名,以监听按键组合。例如,若只想在按住 Shift 时监听 Enter,可以这样写:

blade
<input wire:keydown.shift.enter="...">

以下是全部可用的按键修饰符:

修饰符按键
.shiftShift
.enterEnter
.spaceSpace
.ctrlCtrl
.cmdCmd
.metaMac 上为 Cmd,Windows 上为 Windows 键
.altAlt
.up上方向键
.down下方向键
.left左方向键
.right右方向键
.escapeEscape
.tabTab
.caps-lockCaps Lock
.equal等号,=
.period句点,.
.slash正斜杠,/

事件处理修饰符

Livewire 还提供了实用的修饰符,让常见的事件处理任务变得简单。

例如,若需要在事件监听器中调用 event.preventDefault(),可以在事件名后加上 .prevent

blade
<input wire:keydown.prevent="...">

以下是全部可用的事件监听修饰符及其作用:

修饰符作用
.prevent等同于调用 .preventDefault()
.stop等同于调用 .stopPropagation()
.windowwindow 对象上监听事件
.outside仅监听元素「外部」的点击
.documentdocument 对象上监听事件
.once确保监听器只调用一次
.debounce默认将处理函数防抖 250ms
.debounce.100ms按指定时长对处理函数防抖
.throttle节流处理函数,最少每 250ms 调用一次
.throttle.100ms按自定义时长节流处理函数
.self仅当事件源是本元素(而非子元素)时才调用监听器
.camel将事件名转为驼峰式(wire:custom-event →「customEvent」)
.dot将事件名转为点号表示(wire:custom-event →「custom.event」)
.passivewire:touchstart.passive 不会阻塞滚动性能
.capture在「捕获」阶段监听事件

由于 wire: 底层使用 Alpinex-on 指令,这些修饰符由 Alpine 提供。关于何时应使用这些修饰符,请参阅 Alpine Events 文档

处理第三方事件

Livewire 也支持监听第三方库触发的自定义事件。

例如,假设你在项目中使用 Trix 富文本编辑器,并希望监听 trix-change 事件以获取编辑器内容。可以使用 wire:trix-change 指令实现:

blade
<form wire:submit="save">
    <!-- ... -->

    <trix-editor
        wire:trix-change="setPostContent($event.target.value)"
    ></trix-editor>

    <!-- ... -->
</form>

在本例中,每当触发 trix-change 事件时都会调用 setPostContent 操作,用 Trix 编辑器的当前值更新 Livewire 组件中的 content 属性。

INFO

你可以用 $event 访问事件对象

在 Livewire 事件处理函数中,可通过 $event 访问事件对象,便于引用事件相关信息。例如,可通过 $event.target 访问触发该事件的元素。

WARNING

上面的 Trix 示例代码并不完整,仅用于演示事件监听器。若原样使用,每次按键都会发起网络请求。更高效的实现可以是:

blade
<trix-editor
   x-on:trix-change="$wire.content = $event.target.value"
></trix-editor>

监听派发的自定义事件

如果你的应用从 Alpine 派发自定义事件,也可以用 Livewire 监听它们:

blade
<div wire:custom-event="...">

    <!-- Deeply nested within this component: -->
    <button x-on:click="$dispatch('custom-event')">...</button>

</div>

在上例中点击按钮时,会派发 custom-event 事件并向上冒泡到 Livewire 组件根节点,由 wire:custom-event 捕获并调用给定操作。

若要监听应用中其他位置派发的事件,需要等事件冒泡到 window 对象后再在那里监听。好在 Livewire 让这件事很简单:给任意事件监听器加上 .window 修饰符即可:

blade
<div wire:custom-event.window="...">
    <!-- ... -->
</div>

<!-- Dispatched somewhere on the page outside the component: -->
<button x-on:click="$dispatch('custom-event')">...</button>

表单提交时禁用输入

回顾我们之前讨论过的 CreatePost 示例:

blade
<form wire:submit="save">
    <input wire:model="title">

    <textarea wire:model="content"></textarea>

    <button type="submit">Save</button>
</form>

用户点击「Save」时,会向服务器发送网络请求,以调用 Livewire 组件上的 save() 操作。

但假设用户在较慢的网络上填写该表单。用户点击「Save」后起初没有反应,因为网络请求比平时更久。他们可能怀疑提交失败,并在第一个请求仍在处理时再次点击「Save」按钮。

这种情况下,同一操作会同时有两个请求在处理。

为防止这种情况,在处理 wire:submit 操作期间,Livewire 会自动禁用 <form> 元素内的提交按钮和所有表单输入,确保表单不会被意外提交两次。

为进一步减轻慢速网络下用户的困惑,展示加载指示(例如轻微的背景色变化或 SVG 动画)通常很有帮助。

Livewire 提供了 wire:loading 指令,可轻松在页面任意位置显示和隐藏加载指示。下面是一个用 wire:loading 在「Save」按钮下方显示加载提示的简短示例:

blade
<form wire:submit="save">
    <textarea wire:model="content"></textarea>

    <button type="submit">Save</button>

    <span wire:loading>Saving...</span> <!-- [tl! highlight] -->
</form>

或者,你可以直接用 Tailwind 和 Livewire 自动添加的 data-loading 属性来设置加载状态样式:

blade
<form wire:submit="save">
    <textarea wire:model="content"></textarea>

    <button type="submit" class="data-loading:opacity-50">Save</button>

    <span class="not-data-loading:hidden">Saving...</span>
</form>

多数情况下,使用 data-loading 选择器比 wire:loading 更简单、更灵活。了解更多关于加载状态 →

刷新组件

有时你可能只想简单地「刷新」组件。例如,若组件在检查数据库中某项状态,你可能希望向用户展示一个按钮,让他们刷新显示结果。

你可以在通常引用组件方法的任何地方,使用 Livewire 简单的 $refresh 操作:

blade
<button type="button" wire:click="$refresh">...</button>

触发 $refresh 操作时,Livewire 会与服务器往返一次并重新渲染组件,但不调用任何方法。

需要注意的是,组件中任何待处理的数据更新(例如 wire:model 绑定)会在刷新时应用到服务器端。

你也可以在 Livewire 组件中用 AlpineJS 触发组件刷新:

blade
<button type="button" x-on:click="$wire.$refresh()">...</button>

确认操作

当允许用户执行危险操作(例如从数据库删除文章)时,你可能希望先弹出确认提示,以核实他们确实要执行该操作。

Livewire 通过简单的 wire:confirm 指令让这件事变得容易:

blade
<button
    type="button"
    wire:click="delete"
    wire:confirm="Are you sure you want to delete this post?"
>
    Delete post <!-- [tl! highlight:-2,1] -->
</button>

当把 wire:confirm 加到包含 Livewire 操作的元素上时,用户尝试触发该操作会看到带有所提供文案的确认对话框。他们可以按「OK」确认操作,或按「Cancel」/Escape 键取消。

更多信息请访问 wire:confirm 文档页

从 Alpine 调用操作

Livewire 与 Alpine 无缝集成。实际上,每个 Livewire 组件在底层也是一个 Alpine 组件。这意味着你可以在组件中充分利用 Alpine,添加由 JavaScript 驱动的客户端交互。

为了让这种搭配更强大,Livewire 向 Alpine 暴露了魔法对象 $wire,可将其视为 PHP 组件的 JavaScript 表示。除了通过 $wire 访问和修改公共属性外,你还可以调用操作。在 $wire 对象上调用操作时,后端 Livewire 组件上对应的 PHP 方法会被调用:

blade
<button x-on:click="$wire.save()">Save Post</button>

或者,举一个更复杂的例子:你可以用 Alpine 的 x-intersect 工具,在某个元素出现在页面上时触发 Livewire 的 incrementViewCount() 操作:

blade
<div x-intersect="$wire.incrementViewCount()">...</div>

传递参数

你传给 $wire 方法的任何参数也会传给 PHP 类方法。例如,考虑以下 Livewire 操作:

php
public function addTodo($todo)
{
    $this->todos[] = $todo;
}

在组件的 Blade 模板中,你可以通过 Alpine 调用该操作,并提供应传给操作的参数:

blade
<div x-data="{ todo: '' }">
    <input type="text" x-model="todo">

    <button x-on:click="$wire.addTodo(todo)">Add Todo</button>
</div>

若用户在文本输入框中输入「Take out the trash」并按下「Add Todo」按钮,会触发 addTodo() 方法,且 $todo 参数值为「Take out the trash」。

接收返回值

更强大的是,调用的 $wire 操作在网络请求处理期间会返回一个 promise。收到服务器响应后,promise 会以后端操作的返回值 resolve。

例如,考虑具有以下操作的 Livewire 组件:

php
use App\Models\Post;

public function getPostCount()
{
    return Post::count();
}

使用 $wire,可以调用该操作并解析其返回值:

blade
<span x-init="$el.innerHTML = await $wire.getPostCount()"></span>

在本例中,若 getPostCount() 返回「10」,<span> 标签中也会包含「10」。

TIP

供 JavaScript 消费的操作请使用 #[Json]

对于主要由 JavaScript 消费的操作,可考虑使用 #[Json] 属性。它通过 promise 的 resolve/reject 返回数据,用 promise reject 自动处理验证错误,并跳过重新渲染以提升性能。

使用 Livewire 并不要求掌握 Alpine;但它是非常强大的工具,了解 Alpine 会提升你使用 Livewire 的体验与效率。

JavaScript 操作

Livewire 允许你定义完全在客户端运行、无需发起服务器请求的 JavaScript 操作。这在两种场景下很有用:

  1. 当你想执行不需要与服务器通信的简单 UI 更新时
  2. 当你想在发起服务器请求前先用 JavaScript 乐观更新 UI 时

要定义 JavaScript 操作,可在组件的 <script> 标签内使用 $js() 函数。

下面是一个收藏文章的示例:先用 JavaScript 操作乐观更新 UI,再发起服务器请求。该 JavaScript 操作会立刻显示实心书签图标,然后请求将收藏持久化到数据库:

php
<?php // resources/views/components/post/⚡show.blade.php

use Livewire\Component;
use App\Models\Post;

new class extends Component {
    public Post $post;

    public $bookmarked = false;

    public function mount()
    {
        $this->bookmarked = $this->post->bookmarkedBy(auth()->user());
    }

    public function bookmarkPost()
    {
        $this->post->bookmark(auth()->user());

        $this->bookmarked = $this->post->bookmarkedBy(auth()->user());
    }
};
blade
<div>
    <button wire:click="$js.bookmark" class="flex items-center gap-1">
        {{-- Outlined bookmark icon... --}}
        <svg wire:show="!bookmarked" wire:cloak xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
            <path stroke-linecap="round" stroke-linejoin="round" d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0 1 11.186 0Z" />
        </svg>

        {{-- Solid bookmark icon... --}}
        <svg wire:show="bookmarked" wire:cloak xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
            <path fill-rule="evenodd" d="M6.32 2.577a49.255 49.255 0 0 1 11.36 0c1.497.174 2.57 1.46 2.57 2.93V21a.75.75 0 0 1-1.085.67L12 18.089l-7.165 3.583A.75.75 0 0 1 3.75 21V5.507c0-1.47 1.073-2.756 2.57-2.93Z" clip-rule="evenodd" />
        </svg>
    </button>
</div>

<script>
    this.$js.bookmark = () => {
        $wire.bookmarked = !$wire.bookmarked

        $wire.bookmarkPost()
    }
</script>

用户点击心形按钮时,会发生如下顺序:

  1. 触发「bookmark」JavaScript 操作
  2. 通过在客户端切换 `$wire.bookmarked`,心形图标立即更新
  3. 调用 `bookmarkPost()` 方法将更改保存到数据库

这样既能立刻给出视觉反馈,又能确保收藏状态被正确持久化。

WARNING

基于类的组件需要 @@script 包裹

上面的示例使用裸 <script> 标签,适用于单文件与多文件组件。若使用基于类的组件,必须用 @@script 指令包裹 script 标签:

blade
@@script
<script>
    this.$js.bookmark = () => { /* ... */ }
</script>
@@endscript

这能确保你的 JavaScript 正确限定在该组件作用域内。

从 Alpine 调用

你可以使用 $wire 对象直接从 Alpine 调用 JavaScript 操作。例如,可用 $wire 调用 bookmark JavaScript 操作:

blade
<button x-on:click="$wire.$js.bookmark()">Bookmark</button>

从 PHP 调用

也可以通过 PHP 的 js() 方法调用 JavaScript 操作:

php
<?php // resources/views/components/post/⚡create.blade.php

use Livewire\Component;

new class extends Component {
    public $title = '';

    public function save()
    {
        // ...

        $this->js('onPostSaved'); // [tl! highlight]
    }
};
blade
<div>
    <!-- ... -->

    <button wire:click="save">Save</button>
</div>

<script>
    this.$js.onPostSaved = () => {
        alert('Your post has been saved successfully!')
    }
</script>

在本例中,save() 操作完成后会运行 postSaved JavaScript 操作,从而弹出 alert 对话框。

魔法操作

Livewire 提供一组「魔法」操作,让你无需定义自定义方法即可在组件中完成常见任务。这些魔法操作可在 Blade 模板中定义的事件监听器里使用。

$parent

$parent 魔法变量允许你从子组件访问父组件属性并调用父组件操作:

blade
<button wire:click="$parent.removePost({{ $post->id }})">Remove</button>

在上例中,若父组件有 removePost() 操作,子组件可在 Blade 模板中用 $parent.removePost() 直接调用。

$set

$set 魔法操作允许你直接从 Blade 模板更新 Livewire 组件中的属性。使用 $set 时,传入要更新的属性和新值作为参数:

blade
<button wire:click="$set('query', '')">Reset Search</button>

在本例中,点击按钮会发起网络请求,将组件中的 $query 属性设为 ''

$refresh

$refresh 操作会触发 Livewire 组件重新渲染。在不改变任何属性值而需要更新组件视图时很有用:

blade
<button wire:click="$refresh">Refresh</button>

点击按钮后组件会重新渲染,让你看到视图中的最新变化。

$toggle

$toggle 操作用于切换 Livewire 组件中布尔属性的值:

blade
<button wire:click="$toggle('sortAsc')">
    Sort {{ $sortAsc ? 'Descending' : 'Ascending' }}
</button>

在本例中,点击按钮后组件中的 $sortAsc 属性会在 truefalse 之间切换。

$dispatch

$dispatch 操作允许你直接在浏览器中派发 Livewire 事件。下面是一个点击后会派发 post-deleted 事件的按钮示例:

blade
<button type="submit" wire:click="$dispatch('post-deleted')">Delete Post</button>

$event

$event 可在 wire:click 这类事件监听器中使用。它让你能访问实际触发的 JavaScript 事件,从而引用触发元素及其他相关信息:

blade
<input type="text" wire:keydown.enter="search($event.target.value)">

用户在上面的输入框中输入并按下 Enter 时,输入内容会作为参数传给 search() 操作。

关于魔法操作的更多信息,请参阅 [Javascript 参考](/4.x/javascript#the-wire-object-1)

从 Alpine 使用魔法操作

你也可以通过 $wire 对象从 Alpine 调用魔法操作。例如,可用 $wire 调用 $refresh 魔法操作:

blade
<button x-on:click="$wire.$refresh()">Refresh</button>

跳过重新渲染

有时组件中的某个操作在调用后不会产生会改变已渲染 Blade 模板的副作用。此时,可在操作方法上方添加 #[Renderless] 属性,以跳过 Livewire 生命周期中的 render 部分。

下面以 ShowPost 组件为例:当用户滚动到文章底部时记录「浏览次数」:

php
<?php // resources/views/components/post/⚡show.blade.php

use Livewire\Attributes\Renderless;
use Livewire\Component;
use App\Models\Post;

new class extends Component {
    public Post $post;

    public function mount(Post $post)
    {
        $this->post = $post;
    }

    #[Renderless] // [tl! highlight]
    public function incrementViewCount()
    {
        $this->post->incrementViewCount();
    }
};
blade
<div>
    <h1>{{ $post->title }}</h1>
    <p>{{ $post->content }}</p>

    <div wire:intersect="incrementViewCount"></div>
</div>

上例使用 wire:intersect,在元素进入视口时调用操作(通常用于检测用户滚动到页面更下方的元素)。

可以看到,用户滚动到文章底部时会调用 incrementViewCount()。由于操作加了 #[Renderless],浏览会被记录,但模板不会重新渲染,页面也不会受影响。

若你不想使用方法属性,或需要按条件跳过渲染,可在组件操作中调用 skipRender() 方法:

php
<?php // resources/views/components/post/⚡show.blade.php

use Livewire\Component;
use App\Models\Post;

new class extends Component {
    public Post $post;

    public function mount(Post $post)
    {
        $this->post = $post;
    }

    public function incrementViewCount()
    {
        $this->post->incrementViewCount();

        $this->skipRender(); // [tl! highlight]
    }
};

你也可以用 .renderless 修饰符直接在元素上跳过渲染:

blade
<button type="button" wire:click.renderless="incrementViewCount">

使用 async 并行执行

默认情况下,Livewire 会对同一组件内的操作串行化,以确保状态更新可预测。若某个操作正在进行,后续操作会排队等待其完成。这能防止竞态并保持组件状态一致,但有时你希望操作立即运行、无需等待——即并行而非顺序执行。

#[Async] 属性和 wire:click.async 修饰符会告诉 Livewire 并行执行操作,绕过正常的请求队列。

使用 async 修饰符

给事件监听器加上 .async 修饰符,即可让任意操作变为异步:

blade
<button wire:click.async="logActivity">Track Event</button>

点击该按钮时,即使有其他请求正在进行,logActivity 也会立即触发。它不会阻塞后续请求,其他请求也不会阻塞它。

使用 Async 属性

或者,你可以用 #[Async] 属性将方法标记为异步。这样无论从何处调用,该操作都是异步的:

php
<?php // resources/views/components/post/⚡show.blade.php

use Livewire\Attributes\Async;
use Livewire\Component;

new class extends Component {
    public Post $post;

    #[Async]
    public function logActivity()
    {
        Activity::log('post-viewed', $this->post);
    }

    // ...
};
blade
<div wire:intersect="logActivity">
    <!-- ... -->
</div>

在本例中,当元素进入视口时,会异步调用 logActivity(),且不会阻塞其他进行中的请求。

何时使用异步操作

异步操作适用于「即发即忘」、结果不影响页面显示的场景。常见用例包括:

  • 分析与日志: 跟踪用户行为、页面浏览或交互
  • 后台操作: 触发任务、发送通知或更新外部服务
  • 仅供 JavaScript 使用的结果: 通过 await $wire.getData() 获取仅由 JavaScript 消费的数据

下面是一个跟踪用户点击外部链接的示例:

php
<?php

use Livewire\Attributes\Async;
use Livewire\Component;

new class extends Component {
    public $url;

    #[Async]
    public function trackClick()
    {
        Analytics::track('external-link-clicked', [
            'url' => $this->url,
            'user_id' => auth()->id(),
        ]);
    }

    // ...
};
blade
<a href="{{ $url }}" target="_blank" wire:click.async="trackClick">
    Visit External Site
</a>

由于跟踪是异步进行的,用户的点击不会被网络请求拖慢。

何时不要使用异步操作

WARNING

异步操作与状态变更不宜混用

若操作会修改并反映在 UI 中的组件状态,切勿使用异步操作。 因为异步操作并行运行,可能出现不可预测的竞态,导致组件状态在多个并发请求间不一致。

请看这个危险示例:

php
// Warning: This snippet demonstrates what NOT to do...

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

use Livewire\Attributes\Async;
use Livewire\Component;

new class extends Component {
    public $count = 0;

    #[Async] // Don't do this!
    public function increment()
    {
        $this->count++; // State mutation in an async action
    }

    // ...
};

若用户快速点击递增按钮,会同时发出多个异步请求。每个请求都以相同的初始 $count 值开始,导致更新丢失。你可能点了 5 次,计数器却只增加了 1。

经验法则: 仅对执行纯副作用的操作使用异步——即不改变任何影响组件视图之属性的操作。

为 JavaScript 获取数据

另一个合理用例是从服务器获取完全由 JavaScript 消费的数据,且不影响组件已渲染的状态:

php
<?php

use Livewire\Attributes\Async;
use Livewire\Component;

new class extends Component {
    #[Async]
    public function fetchSuggestions($query)
    {
        return Post::where('title', 'like', "%{$query}%")
            ->limit(5)
            ->pluck('title');
    }

    // ...
};
blade
<div x-data="{ suggestions: [] }">
    <input
        type="text"
        x-on:input.debounce="suggestions = await $wire.fetchSuggestions($event.target.value)"
    >

    <template x-for="suggestion in suggestions">
        <div x-text="suggestion"></div>
    </template>
</div>

由于建议列表仅存储在 Alpine 的 suggestions 数据中,从不进入 Livewire 组件状态,因此异步获取是安全的。

保持滚动位置

更新内容时,浏览器可能会跳到不同的滚动位置。.preserve-scroll 修饰符可在更新期间保持当前滚动位置:

blade
<button wire:click.preserve-scroll="loadMore">Load More</button>

<select wire:model.live.preserve-scroll="category">...</select>

这对无限滚动、筛选器和动态内容更新很有用——你不希望页面发生跳动。

安全注意事项

请记住,Livewire 组件中的任何公共方法都可以从客户端调用,即使没有关联的 wire:click 处理程序。在这些情况下,用户仍可从浏览器的开发者工具触发该操作。

下面是三个容易忽略的 Livewire 组件漏洞示例。每个示例先展示有漏洞的组件,再展示安全版本。作为练习,可先尝试找出第一个示例中的漏洞,再查看解决方案。

若你难以发现这些漏洞,并因此担心自己保障应用安全的能力,请记住:所有这些漏洞同样适用于使用请求与控制器的标准 Web 应用。若把组件方法当作控制器方法的代理、把其参数当作请求输入的代理,你就可以把已有的应用安全知识应用到 Livewire 代码中。

始终授权操作参数

与控制器请求输入一样,操作参数是任意用户输入,必须进行授权。

下面是一个 ShowPosts 组件:用户可在同一页查看自己的全部文章,并可通过文章的「Delete」按钮删除任意文章。

以下是该组件有漏洞的版本:

php
<?php // resources/views/components/post/⚡index.blade.php

use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Post;

new class extends Component {
    #[Computed]
    public function posts()
    {
        return Auth::user()->posts;
    }

    public function delete($id)
    {
        $post = Post::find($id);

        $post->delete();
    }
};
blade
<div>
    @foreach ($this->posts as $post)
        <div wire:key="{{ $post->id }}">
            <h1>{{ $post->title }}</h1>
            <span>{{ $post->content }}</span>

            <button wire:click="delete({{ $post->id }})">Delete</button>
        </div>
    @endforeach
</div>

请记住,恶意用户可直接从 JavaScript 控制台调用 delete(),并向操作传入任意参数。这意味着正在查看自己某篇文章的用户,可通过向 delete() 传入不属于自己的文章 ID 来删除他人的文章。

为防范这一点,我们需要授权确认用户拥有即将删除的文章:

php
<?php // resources/views/components/post/⚡index.blade.php

use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Post;

new class extends Component {
    #[Computed]
    public function posts()
    {
        return Auth::user()->posts;
    }

    public function delete($id)
    {
        $post = Post::find($id);

        $this->authorize('delete', $post); // [tl! highlight]

        $post->delete();
    }
};

始终在服务端授权

与标准 Laravel 控制器一样,任何用户都可以调用 Livewire 操作,即使 UI 中没有调用该操作的入口。

考虑下面的 BrowsePosts 组件:任何用户都可以查看应用中的全部文章,但只有管理员可以删除文章:

php
<?php // resources/views/components/post/⚡index.blade.php

use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Post;

new class extends Component {
    #[Computed]
    public function posts()
    {
        return Auth::user()->posts;
    }

    public function deletePost($id)
    {
        $post = Post::find($id);

        $post->delete();
    }
};
blade
<div>
    @foreach ($this->posts as $post)
        <div wire:key="{{ $post->id }}">
            <h1>{{ $post->title }}</h1>
            <span>{{ $post->content }}</span>

            @if (Auth::user()->isAdmin())
                <button wire:click="deletePost({{ $post->id }})">Delete</button>
            @endif
        </div>
    @endforeach
</div>

可以看到,只有管理员能看到「Delete」按钮;但任何用户都可以从浏览器开发者工具调用组件上的 deletePost()

要修补此漏洞,需要在服务端授权该操作,例如:

php
<?php // resources/views/components/post/⚡index.blade.php

use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Post;

new class extends Component {
    #[Computed]
    public function posts()
    {
        return Auth::user()->posts;
    }

    public function deletePost($id)
    {
        if (! Auth::user()->isAdmin) { // [tl! highlight:2]
            abort(403);
        }

        $post = Post::find($id);

        $post->delete();
    }
};

经过此更改,只有管理员能从该组件删除文章。

将危险方法设为 protected 或 private

Livewire 组件中的每个公共方法都可以从客户端调用,即使你从未在 wire:click 处理程序中引用它们。为防止用户调用本不应可从客户端调用的方法,应将其标记为 protectedprivate。这样会把敏感方法的可见性限制在组件类及其子类内,确保无法从客户端调用。

回顾之前讨论的 BrowsePosts 示例:用户可查看应用中的全部文章,但只有管理员可删除。在始终在服务端授权一节中,我们通过添加服务端授权使操作变得安全。现在假设为简化代码,我们把实际删除文章的逻辑重构到一个独立方法中:

php
// Warning: This snippet demonstrates what NOT to do...
<?php // resources/views/components/post/⚡index.blade.php

use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Post;

new class extends Component {
    #[Computed]
    public function posts()
    {
        return Auth::user()->posts;
    }

    public function deletePost($id)
    {
        if (! Auth::user()->isAdmin) {
            abort(403);
        }

        $this->delete($id); // [tl! highlight]
    }

    public function delete($postId)  // [tl! highlight:5]
    {
        $post = Post::find($postId);

        $post->delete();
    }
};
blade
<div>
    @foreach ($posts as $post)
        <div wire:key="{{ $post->id }}">
            <h1>{{ $post->title }}</h1>
            <span>{{ $post->content }}</span>

            <button wire:click="deletePost({{ $post->id }})">Delete</button>
        </div>
    @endforeach
</div>

可以看到,我们把删除文章的逻辑重构到了名为 delete() 的独立方法中。即使模板中从未引用该方法,若用户得知其存在,仍可从浏览器开发者工具调用它,因为它是 public

要解决这一点,可将该方法标记为 protectedprivate。一旦标记为 protectedprivate,用户尝试调用时就会抛出错误:

php
<?php // resources/views/components/post/⚡index.blade.php

use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Post;

new class extends Component {
    #[Computed]
    public function posts()
    {
        return Auth::user()->posts;
    }

    public function deletePost($id)
    {
        if (! Auth::user()->isAdmin) {
            abort(403);
        }

        $this->delete($id);
    }

    protected function delete($postId) // [tl! highlight]
    {
        $post = Post::find($postId);

        $post->delete();
    }
};

应用中间件

默认情况下,若认证与授权相关中间件已在首次页面加载请求中应用,Livewire 会在后续请求中重新应用这些中间件。

例如,假设你的组件加载在分配了 auth 中间件的路由中,且用户会话已结束。当用户再触发其他操作时,会重新应用 auth 中间件,用户会收到错误。

若希望将特定中间件应用到特定操作,可以使用 #[Middleware] 属性。例如,我们可以把 LogPostCreation 中间件应用到创建文章的操作:

php
<?php

namespace App\Livewire;

use App\Http\Middleware\LogPostCreation;
use Livewire\Component;

class CreatePost extends Component
{
    public $title;

    public $content;

    #[Middleware(LogPostCreation::class)] // [tl! highlight]
    public function save()
    {
        // Create the post...
    }

    // ...
}

现在,LogPostCreation 中间件只会应用到 createPost 操作,确保仅在用户创建新文章时记录该活动。

另见

  • 事件使用事件在组件间通信
  • 表单用操作处理表单提交
  • 加载状态在操作处理时显示反馈
  • wire:click通过按钮点击触发操作
  • 验证在处理操作前验证数据