操作
Livewire 操作(actions)是组件上的方法,可由点击按钮或提交表单等前端交互触发。它们带来了能直接从浏览器调用 PHP 方法的开发体验,让你专注于应用逻辑,而不必纠缠于连接前后端的样板代码。
下面来看一个调用 save 操作的基本示例:
<?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 // 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();
}
};<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」按钮在浏览器中会渲染为:
<button wire:click="delete(2)">Delete</button>点击该按钮时会调用 delete() 方法,并传入值为「2」的 $id。
作为额外便利,你可以根据传给操作的模型 ID 自动解析 Eloquent 模型。这与路由模型绑定非常相似。入门做法是:用模型类为操作参数加上类型提示,对应模型会自动从数据库取出并传给操作,而不是传入 ID:
<?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 // 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);
}
};<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:
<input wire:model="query" wire:keydown.enter="searchPosts">你可以在第一个别名后再串联更多按键别名,以监听按键组合。例如,若只想在按住 Shift 时监听 Enter,可以这样写:
<input wire:keydown.shift.enter="...">以下是全部可用的按键修饰符:
| 修饰符 | 按键 |
|---|---|
.shift | Shift |
.enter | Enter |
.space | Space |
.ctrl | Ctrl |
.cmd | Cmd |
.meta | Mac 上为 Cmd,Windows 上为 Windows 键 |
.alt | Alt |
.up | 上方向键 |
.down | 下方向键 |
.left | 左方向键 |
.right | 右方向键 |
.escape | Escape |
.tab | Tab |
.caps-lock | Caps Lock |
.equal | 等号,= |
.period | 句点,. |
.slash | 正斜杠,/ |
事件处理修饰符
Livewire 还提供了实用的修饰符,让常见的事件处理任务变得简单。
例如,若需要在事件监听器中调用 event.preventDefault(),可以在事件名后加上 .prevent:
<input wire:keydown.prevent="...">以下是全部可用的事件监听修饰符及其作用:
| 修饰符 | 作用 |
|---|---|
.prevent | 等同于调用 .preventDefault() |
.stop | 等同于调用 .stopPropagation() |
.window | 在 window 对象上监听事件 |
.outside | 仅监听元素「外部」的点击 |
.document | 在 document 对象上监听事件 |
.once | 确保监听器只调用一次 |
.debounce | 默认将处理函数防抖 250ms |
.debounce.100ms | 按指定时长对处理函数防抖 |
.throttle | 节流处理函数,最少每 250ms 调用一次 |
.throttle.100ms | 按自定义时长节流处理函数 |
.self | 仅当事件源是本元素(而非子元素)时才调用监听器 |
.camel | 将事件名转为驼峰式(wire:custom-event →「customEvent」) |
.dot | 将事件名转为点号表示(wire:custom-event →「custom.event」) |
.passive | wire:touchstart.passive 不会阻塞滚动性能 |
.capture | 在「捕获」阶段监听事件 |
由于 wire: 底层使用 Alpine 的 x-on 指令,这些修饰符由 Alpine 提供。关于何时应使用这些修饰符,请参阅 Alpine Events 文档。
处理第三方事件
Livewire 也支持监听第三方库触发的自定义事件。
例如,假设你在项目中使用 Trix 富文本编辑器,并希望监听 trix-change 事件以获取编辑器内容。可以使用 wire:trix-change 指令实现:
<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 示例代码并不完整,仅用于演示事件监听器。若原样使用,每次按键都会发起网络请求。更高效的实现可以是:
<trix-editor
x-on:trix-change="$wire.content = $event.target.value"
></trix-editor>监听派发的自定义事件
如果你的应用从 Alpine 派发自定义事件,也可以用 Livewire 监听它们:
<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 修饰符即可:
<div wire:custom-event.window="...">
<!-- ... -->
</div>
<!-- Dispatched somewhere on the page outside the component: -->
<button x-on:click="$dispatch('custom-event')">...</button>表单提交时禁用输入
回顾我们之前讨论过的 CreatePost 示例:
<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」按钮下方显示加载提示的简短示例:
<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 属性来设置加载状态样式:
<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 操作:
<button type="button" wire:click="$refresh">...</button>触发 $refresh 操作时,Livewire 会与服务器往返一次并重新渲染组件,但不调用任何方法。
需要注意的是,组件中任何待处理的数据更新(例如 wire:model 绑定)会在刷新时应用到服务器端。
你也可以在 Livewire 组件中用 AlpineJS 触发组件刷新:
<button type="button" x-on:click="$wire.$refresh()">...</button>更多内容请参阅在 Livewire 中使用 Alpine 的文档。
确认操作
当允许用户执行危险操作(例如从数据库删除文章)时,你可能希望先弹出确认提示,以核实他们确实要执行该操作。
Livewire 通过简单的 wire:confirm 指令让这件事变得容易:
<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 方法会被调用:
<button x-on:click="$wire.save()">Save Post</button>或者,举一个更复杂的例子:你可以用 Alpine 的 x-intersect 工具,在某个元素出现在页面上时触发 Livewire 的 incrementViewCount() 操作:
<div x-intersect="$wire.incrementViewCount()">...</div>传递参数
你传给 $wire 方法的任何参数也会传给 PHP 类方法。例如,考虑以下 Livewire 操作:
public function addTodo($todo)
{
$this->todos[] = $todo;
}在组件的 Blade 模板中,你可以通过 Alpine 调用该操作,并提供应传给操作的参数:
<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 组件:
use App\Models\Post;
public function getPostCount()
{
return Post::count();
}使用 $wire,可以调用该操作并解析其返回值:
<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 操作。这在两种场景下很有用:
- 当你想执行不需要与服务器通信的简单 UI 更新时
- 当你想在发起服务器请求前先用 JavaScript 乐观更新 UI 时
要定义 JavaScript 操作,可在组件的 <script> 标签内使用 $js() 函数。
下面是一个收藏文章的示例:先用 JavaScript 操作乐观更新 UI,再发起服务器请求。该 JavaScript 操作会立刻显示实心书签图标,然后请求将收藏持久化到数据库:
<?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());
}
};<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>用户点击心形按钮时,会发生如下顺序:
- 触发「bookmark」JavaScript 操作
- 通过在客户端切换 `$wire.bookmarked`,心形图标立即更新
- 调用 `bookmarkPost()` 方法将更改保存到数据库
这样既能立刻给出视觉反馈,又能确保收藏状态被正确持久化。
WARNING
基于类的组件需要 @@script 包裹
上面的示例使用裸 <script> 标签,适用于单文件与多文件组件。若使用基于类的组件,必须用 @@script 指令包裹 script 标签:
@@script
<script>
this.$js.bookmark = () => { /* ... */ }
</script>
@@endscript这能确保你的 JavaScript 正确限定在该组件作用域内。
从 Alpine 调用
你可以使用 $wire 对象直接从 Alpine 调用 JavaScript 操作。例如,可用 $wire 调用 bookmark JavaScript 操作:
<button x-on:click="$wire.$js.bookmark()">Bookmark</button>从 PHP 调用
也可以通过 PHP 的 js() 方法调用 JavaScript 操作:
<?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]
}
};<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 魔法变量允许你从子组件访问父组件属性并调用父组件操作:
<button wire:click="$parent.removePost({{ $post->id }})">Remove</button>在上例中,若父组件有 removePost() 操作,子组件可在 Blade 模板中用 $parent.removePost() 直接调用。
$set
$set 魔法操作允许你直接从 Blade 模板更新 Livewire 组件中的属性。使用 $set 时,传入要更新的属性和新值作为参数:
<button wire:click="$set('query', '')">Reset Search</button>在本例中,点击按钮会发起网络请求,将组件中的 $query 属性设为 ''。
$refresh
$refresh 操作会触发 Livewire 组件重新渲染。在不改变任何属性值而需要更新组件视图时很有用:
<button wire:click="$refresh">Refresh</button>点击按钮后组件会重新渲染,让你看到视图中的最新变化。
$toggle
$toggle 操作用于切换 Livewire 组件中布尔属性的值:
<button wire:click="$toggle('sortAsc')">
Sort {{ $sortAsc ? 'Descending' : 'Ascending' }}
</button>在本例中,点击按钮后组件中的 $sortAsc 属性会在 true 与 false 之间切换。
$dispatch
$dispatch 操作允许你直接在浏览器中派发 Livewire 事件。下面是一个点击后会派发 post-deleted 事件的按钮示例:
<button type="submit" wire:click="$dispatch('post-deleted')">Delete Post</button>$event
$event 可在 wire:click 这类事件监听器中使用。它让你能访问实际触发的 JavaScript 事件,从而引用触发元素及其他相关信息:
<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 魔法操作:
<button x-on:click="$wire.$refresh()">Refresh</button>跳过重新渲染
有时组件中的某个操作在调用后不会产生会改变已渲染 Blade 模板的副作用。此时,可在操作方法上方添加 #[Renderless] 属性,以跳过 Livewire 生命周期中的 render 部分。
下面以 ShowPost 组件为例:当用户滚动到文章底部时记录「浏览次数」:
<?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();
}
};<div>
<h1>{{ $post->title }}</h1>
<p>{{ $post->content }}</p>
<div wire:intersect="incrementViewCount"></div>
</div>上例使用 wire:intersect,在元素进入视口时调用操作(通常用于检测用户滚动到页面更下方的元素)。
可以看到,用户滚动到文章底部时会调用 incrementViewCount()。由于操作加了 #[Renderless],浏览会被记录,但模板不会重新渲染,页面也不会受影响。
若你不想使用方法属性,或需要按条件跳过渲染,可在组件操作中调用 skipRender() 方法:
<?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 修饰符直接在元素上跳过渲染:
<button type="button" wire:click.renderless="incrementViewCount">使用 async 并行执行
默认情况下,Livewire 会对同一组件内的操作串行化,以确保状态更新可预测。若某个操作正在进行,后续操作会排队等待其完成。这能防止竞态并保持组件状态一致,但有时你希望操作立即运行、无需等待——即并行而非顺序执行。
#[Async] 属性和 wire:click.async 修饰符会告诉 Livewire 并行执行操作,绕过正常的请求队列。
使用 async 修饰符
给事件监听器加上 .async 修饰符,即可让任意操作变为异步:
<button wire:click.async="logActivity">Track Event</button>点击该按钮时,即使有其他请求正在进行,logActivity 也会立即触发。它不会阻塞后续请求,其他请求也不会阻塞它。
使用 Async 属性
或者,你可以用 #[Async] 属性将方法标记为异步。这样无论从何处调用,该操作都是异步的:
<?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);
}
// ...
};<div wire:intersect="logActivity">
<!-- ... -->
</div>在本例中,当元素进入视口时,会异步调用 logActivity(),且不会阻塞其他进行中的请求。
何时使用异步操作
异步操作适用于「即发即忘」、结果不影响页面显示的场景。常见用例包括:
- 分析与日志: 跟踪用户行为、页面浏览或交互
- 后台操作: 触发任务、发送通知或更新外部服务
- 仅供 JavaScript 使用的结果: 通过
await $wire.getData()获取仅由 JavaScript 消费的数据
下面是一个跟踪用户点击外部链接的示例:
<?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(),
]);
}
// ...
};<a href="{{ $url }}" target="_blank" wire:click.async="trackClick">
Visit External Site
</a>由于跟踪是异步进行的,用户的点击不会被网络请求拖慢。
何时不要使用异步操作
WARNING
异步操作与状态变更不宜混用
若操作会修改并反映在 UI 中的组件状态,切勿使用异步操作。 因为异步操作并行运行,可能出现不可预测的竞态,导致组件状态在多个并发请求间不一致。
请看这个危险示例:
// 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
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');
}
// ...
};<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 修饰符可在更新期间保持当前滚动位置:
<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 // 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();
}
};<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 // 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 // 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();
}
};<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 // 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 处理程序中引用它们。为防止用户调用本不应可从客户端调用的方法,应将其标记为 protected 或 private。这样会把敏感方法的可见性限制在组件类及其子类内,确保无法从客户端调用。
回顾之前讨论的 BrowsePosts 示例:用户可查看应用中的全部文章,但只有管理员可删除。在始终在服务端授权一节中,我们通过添加服务端授权使操作变得安全。现在假设为简化代码,我们把实际删除文章的逻辑重构到一个独立方法中:
// 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();
}
};<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。
要解决这一点,可将该方法标记为 protected 或 private。一旦标记为 protected 或 private,用户尝试调用时就会抛出错误:
<?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
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 — 通过按钮点击触发操作
- 验证 — 在处理操作前验证数据