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">
    <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>
        </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)
    {
        $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)
    {
        $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>
        </div>
    @endforeach
</div>

在此示例中,delete() 方法在接收提供的 $postId 参数之前,先接收通过 Laravel 服务容器解析的 PostRepository 实例。

事件监听器

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 事件文档

处理第三方事件

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="...">

    <!-- 深度嵌套在此组件内: -->
    <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>

<!-- 在组件外部页面的某处分发: -->
<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>
</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 中使用 Alpine 的文档

确认操作

当允许用户执行危险操作(例如从数据库删除帖子)时,你可能想向他们显示确认警告以验证他们是否希望执行该操作。

Livewire 通过提供一个名为 wire:confirm 的简单指令使这变得容易:

blade
<button
    type="button"
    wire:click="delete"
    wire:confirm="Are you sure you want to delete this post?"
>
    Delete post
</button>

wire:confirm 被添加到包含 Livewire 操作的元素时,当用户尝试触发该操作时,他们将看到一个包含提供消息的确认对话框。他们可以按"OK"确认操作,或按"Cancel"或按 escape 键取消。

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

从 Alpine 调用操作

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

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

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"。

对于 JavaScript 消费的操作使用 #[Json]

对于主要由 JavaScript 消费的操作,考虑使用 #[Json] 属性。它通过 promise 解析/拒绝返回数据,自动处理带有 promise 拒绝的验证错误,并跳过重新渲染以获得更好的性能。

使用 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">
        {{-- 空心收藏图标... --}}
        <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>

        {{-- 实心收藏图标... --}}
        <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() 方法将更改保存到数据库

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

基于类的组件需要 @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 调用

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');
    }
};
blade
<div>
    <!-- ... -->

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

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

在此示例中,当 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]
    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();
    }
};

你也可以使用 .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>

因为跟踪是异步发生的,用户的点击不会被网络请求延迟。

何时不使用异步操作

异步操作和状态变更不能混合

如果异步操作修改了反映在 UI 中的组件状态,永远不要使用它们。 因为异步操作并行运行,你可能会遇到不可预测的竞争条件,其中组件的状态在多个同时请求中分叉。

考虑这个危险的示例:

php
// 警告:此代码片段演示了不应该做的事情...

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

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

new class extends Component {
    public $count = 0;

    #[Async] // 不要这样做!
    public function increment()
    {
        $this->count++; // 在异步操作中修改状态
    }

    // ...
};

如果用户快速点击递增按钮,多个异步请求将同时触发。每个请求都以相同的初始 $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 处理器。在这些场景中,用户仍然可以从浏览器的 DevTools 触发操作。

以下是三个容易遗漏的 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);

        $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"按钮;但是,任何用户都可以从浏览器的 DevTools 在组件上调用 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) {
            abort(403);
        }

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

        $post->delete();
    }
};

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

保持危险方法为 protected 或 private

Livewire 组件中的每个公共方法都可以从客户端调用。即使你没有在 wire:click 处理器中引用的方法。为了防止用户调用不打算从客户端调用的方法,你应该将它们标记为 protectedprivate。这样做会限制该敏感方法的可见性仅限于组件的类及其子类,确保它们不能从客户端调用。

考虑我们之前讨论的 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)
    {
        if (! Auth::user()->isAdmin) {
            abort(403);
        }

        $this->delete($id);
    }

    public function delete($postId)
    {
        $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() 的专用方法中。即使此方法没有在我们的模板中任何地方引用,如果用户得知其存在,他们将能够从浏览器的 DevTools 调用它,因为它是 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)
    {
        $post = Post::find($postId);

        $post->delete();
    }
};

另请参阅

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