Skip to content

操作

Livewire 操作是组件中的方法,可以通过前端交互(如点击按钮或提交表单)来触发。它们提供了直接从浏览器调用 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 的简单 $refresh 操作来实现:

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

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

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

在内部,Livewire 使用"commit"这个名称来指代 Livewire 组件在服务器上更新的任何时刻。如果你更喜欢这个术语,可以使用 $commit 辅助函数代替 $refresh。两者是完全相同的。

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

你也可以在 Livewire 组件中使用 Alpine 来触发组件刷新:

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

通过阅读在 Livewire 中使用 Alpine 的文档了解更多信息。

确认操作

当允许用户执行危险操作(例如从数据库中删除文章)时,你可能想要向他们显示一个确认提示,以验证他们是否真的想要执行该操作。

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 操作的元素上时,当用户尝试触发该操作时,他们会看到一个包含所提供消息的确认对话框。他们可以按"确定"来确认操作,或按"取消"或按 Escape 键。

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

并行执行

默认情况下,同一组件内的操作是串行化的:如果一个操作正在执行中,后续操作会排队等待它完成。

添加 .async 修饰符允许操作并行运行而不是排队。这对于不想阻塞后续操作的"触发即忘"操作很有帮助。

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

有关何时以及如何安全使用异步操作的更多信息,请参阅并行执行章节

事件监听器

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: 在底层使用了 Alpine 的 x-on 指令,所以这些修饰符由 Alpine 提供给你。有关何时应该使用这些修饰符的更多信息,请查阅 Alpine 事件文档

处理第三方事件

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 属性。

你可以使用 $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"按钮。

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

为了防止这种情况,Livewire 会在处理 wire:submit 操作时自动禁用提交按钮和 <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>

wire:loading 是一个功能强大的特性,具有多种更强大的功能。查看完整的加载文档以获取更多信息

传递参数

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() 方法,并将 $id 传入,值为"2"。

不要信任操作参数

操作参数应该像 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() 方法在接收提供的 $postId 参数之前,会接收通过 Laravel 的服务容器解析的 PostRepository 实例。

从 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 工具在给定元素在页面上可见时触发 incrementViewCount() Livewire 操作:

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 会解析为后端操作返回的值。

例如,考虑一个具有以下操作的 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"。

使用 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
<script>
    $js.bookmark = () => {
        $wire.bookmarked = !$wire.bookmarked

        $wire.bookmarkPost()
    }
</script>
@endscript

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

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

这提供了即时的视觉反馈,同时确保书签状态被正确持久化。

从 Alpine 调用

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

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

从 PHP 调用

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

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
<script>
    $js.onPostSaved = () => {
        alert('Your post has been saved successfully!')
    }
</script>
@endscript

在这个示例中,当 save() 操作完成时,将运行 postSaved JavaScript 操作,触发警报对话框。

魔法操作

Livewire 提供了一组"魔法"操作,允许你在组件中执行常见任务而无需定义自定义方法。这些魔法操作可以在 Blade 模板中定义的事件监听器中使用。

$parent

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

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

在上面的示例中,如果父组件有 removePost() 操作,子组件可以使用 $parent.removePost() 直接从其 Blade 模板调用它。

$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() 操作。

从 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() 被异步调用,不会阻塞任何其他正在执行的请求。

何时使用 async 操作

Async 操作对于"触发即忘"的操作很有用,这些操作的结果不会影响页面上显示的内容。常见用例包括:

  • 分析和日志记录:跟踪用户行为、页面浏览或交互
  • 后台操作:触发作业、发送通知或更新外部服务
  • 仅 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>

因为跟踪是异步进行的,所以用户的点击不会被网络请求延迟。

何时不使用 async 操作

Async 操作和状态变更不能混用

如果操作会修改 UI 中反映的组件状态,则永远不要使用 async 操作。因为 async 操作并行运行,你可能会遇到不可预测的竞态条件,其中组件的状态在多个同时请求中发生分歧。

考虑这个危险的示例:

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。

经验法则:仅对执行纯副作用的操作使用 async——不会更改影响组件视图的任何属性的操作。

为 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(),向操作传递他们想要的任何参数。这意味着查看自己文章的用户可以通过将未拥有的文章 ID 传递给 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);

        $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();
    }
};

通过此更改,只有管理员可以从此组件删除文章。

保持危险方法受保护或私有

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();
    }
};