主题
操作
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="...">以下是所有可用的按键修饰符列表:
| 修饰符 | 按键 |
|---|---|
.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 后缀:
blade
<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 事件文档。
处理第三方事件
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"。
作为额外的便利,你可以通过作为参数提供给操作的相应模型 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 操作。这在两种情况下很有用:
- 当你想要执行不需要服务器通信的简单 UI 更新时
- 当你想要在发出服务器请求之前使用 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当用户点击心形按钮时,会发生以下顺序:
- 触发"bookmark" JavaScript 操作
- 通过在客户端切换
$wire.bookmarked立即更新心形图标 - 调用
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 属性将在 true 和 false 之间切换。
$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 处理器内引用的方法。要防止用户调用不打算从客户端调用的方法,你应该将它们标记为 protected 或 private。这样做,你将该敏感方法的可见性限制为组件的类及其子类,确保它们不能从客户端调用。
考虑我们之前讨论的 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 的。
要解决这个问题,我们可以将方法标记为 protected 或 private。一旦方法被标记为 protected 或 private,如果用户尝试调用它,将抛出错误:
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();
}
};