Alpine.js
AlpineJS 是一个轻量级的 JavaScript 库,可以轻松为网页添加客户端交互功能。它最初是为了补充像 Livewire 这样的工具而构建的,当你需要在应用中分散地添加交互功能时,一个更偏向 JavaScript 的工具会非常有用。
Livewire 内置了 Alpine,因此无需在项目中单独安装。
学习使用 AlpineJS 的最佳途径是 Alpine 文档。
一个基本的 Alpine 组件
为给本文档的其他内容打下基础,下面展示一个最简单且最具代表性的 Alpine 组件示例。一个小型"计数器",它在页面上显示一个数字,并允许用户通过点击按钮来增加该数字:
<!-- 声明一个 JavaScript 数据对象... -->
<div x-data="{ count: 0 }">
<!-- 在元素内渲染当前的 "count" 值... -->
<h2 x-text="count"></h2>
<!-- 当点击事件触发时,将 "count" 值增加 "1"... -->
<button x-on:click="count++">+</button>
</div>
上面的 Alpine 组件可以无缝地在应用中任何 Livewire 组件内使用。Livewire 负责在 Livewire 组件更新时维护 Alpine 的状态。本质上,你可以像在非 Livewire 环境中使用 Alpine 一样,在 Livewire 中自由地使用 Alpine 组件。
在 Livewire 中使用 Alpine
让我们探索一个更贴近实际的示例:在 Livewire 组件内使用 Alpine 组件。
下面是一个简单的 Livewire 组件,展示数据库中 post 模型的详细信息。默认情况下,只显示文章的标题:
<div>
<h1>{{ $post->title }}</h1>
<div x-data="{ expanded: false }">
<button type="button" x-on:click="expanded = ! expanded">
<span x-show="! expanded">显示文章内容...</span>
<span x-show="expanded">隐藏文章内容...</span>
</button>
<div x-show="expanded">
{{ $post->content }}
</div>
</div>
</div>
通过使用 Alpine,我们可以隐藏文章内容,直到用户点击"显示文章内容..."按钮。此时,Alpine 的 expanded 属性将被设为 true,内容将显示在页面上,因为 x-show="expanded" 让 Alpine 控制了文章内容的可见性。
这是 Alpine 的闪光之处:在不触发 Livewire 服务端往返的情况下为应用添加交互功能。
使用 $wire 从 Alpine 控制 Livewire
作为 Livewire 开发者,最强大的功能之一就是 $wire。$wire 对象是一个魔法对象,可用于所有在 Livewire 内部使用的 Alpine 组件。
你可以将 $wire 视为从 JavaScript 通往 PHP 的大门。它允许你访问和修改 Livewire 组件的属性、调用 Livewire 组件的 method 等,所有这些都可以在 AlpineJS 内部完成。
访问 Livewire 属性
下面是一个在创建文章的表单中使用的简单"字符计数"工具示例。它会在用户输入时立即显示文章内容的字符数:
<form wire:submit="save">
<!-- ... -->
<input wire:model="content" type="text">
<small>
字符数:<span x-text="$wire.content.length"></span> <!-- [tl! highlight] -->
</small>
<button type="submit">保存</button>
</form>
如上所示,上述示例中使用了 x-text 让 Alpine 控制 <span> 元素的文本内容。x-text 接受任何 JavaScript 表达式,并会在依赖项更新时自动响应。因为我们使用 $wire.content 来访问 $content 的值,Alpine 会在 $wire.content 被 Livewire 更新时自动更新文本内容;在本例中,是通过 wire:model="content" 实现的。
修改 Livewire 属性
下面是一个在 Alpine 中使用 $wire 清除创建文章表单中"title"字段的示例:
<form wire:submit="save">
<input wire:model="title" type="text">
<button type="button" x-on:click="$wire.title = ''">清除</button> <!-- [tl! highlight] -->
<!-- ... -->
<button type="submit">保存</button>
</form>
当用户填写上述 Livewire 表单时,可以按"清除"按钮,标题字段将被清除,而无需发送 Livewire 网络请求。这个交互是"即时"的。
以下是实现这一功能的简要说明:
x-on:click告诉 Alpine 监听按钮上的点击事件- 点击时,Alpine 执行提供的 JS 表达式:
$wire.title = '' - 因为
$wire是一个代表 Livewire 组件的魔法对象,组件中的所有属性都可以直接从 JavaScript 访问或修改 $wire.title = ''将 Livewire 组件中的$title值设为空字符串- 所有 Livewire 工具(如
wire:model)都会立即响应此更改,而无需发送服务端往返 - 在下一次 Livewire 网络请求时,
$title属性将在后端更新为空字符串
调用 Livewire 方法
Alpine 也可以轻松地直接调用 $wire 上的任何 Livewire 方法/操作。
下面是一个使用 Alpine 监听输入框的"blur"事件并触发表单保存的示例。当用户按下"tab"键使当前元素失去焦点并聚焦到页面上的下一个元素时,浏览器会触发"blur"事件:
<form wire:submit="save">
<input wire:model="title" type="text" x-on:blur="$wire.save()"> <!-- [tl! highlight] -->
<!-- ... -->
<button type="submit">保存</button>
</form>
通常,在这种情况下你只需使用 wire:model.blur="title",但这个示例有助于演示如何使用 Alpine 实现同样的功能。
传递参数
你也可以通过简单地在 $wire 方法调用中传递参数来向 Livewire 方法传递参数。
假设有一个包含 deletePost() 方法的组件:
public function deletePost($postId)
{
$post = Post::find($postId);
// 验证用户可以删除...
auth()->user()->can('update', $post);
$post->delete();
}
现在,你可以从 Alpine 向 deletePost() 方法传递 $postId 参数:
<button type="button" x-on:click="$wire.deletePost(1)">
通常,像 $postId 这样的值会在 Blade 中生成。下面是使用 Blade 决定 Alpine 传递给 deletePost() 的 $postId 的示例:
@foreach ($posts as $post)
<button type="button" x-on:click="$wire.deletePost({{ $post->id }})">
删除 "{{ $post->title }}"
</button>
@endforeach
如果页面上有三篇文章,上面的 Blade 模板将在浏览器中渲染为类似下面的内容:
<button type="button" x-on:click="$wire.deletePost(1)">
删除 "The power of walking"
</button>
<button type="button" x-on:click="$wire.deletePost(2)">
删除 "How to record a song"
</button>
<button type="button" x-on:click="$wire.deletePost(3)">
删除 "Teach what you learn"
</button>
如你所见,我们使用 Blade 将不同的文章 ID 渲染到 Alpine 的 x-on:click 表达式中。
Blade 参数注意事项
这是一种非常强大的技术,但在阅读 Blade 模板时可能会令人困惑。乍一看很难分辨哪些部分是 Blade,哪些部分是 Alpine。因此,检查页面上渲染的 HTML 以确保渲染结果与预期一致是很有帮助的。
下面是一个常见的令人困惑的示例:
假设你的 Post 模型使用 UUID 作为索引(ID 是整数,而 UUID 是长字符串),而不是 ID。
如果像使用 ID 一样渲染以下内容,就会出现问题:
<!-- 警告:这是一个有问题的代码示例... -->
<button
type="button"
x-on:click="$wire.deletePost({{ $post->uuid }})"
>
上面的 Blade 模板将在 HTML 中渲染为:
<!-- 警告:这是一个有问题的代码示例... -->
<button
type="button"
x-on:click="$wire.deletePost(93c7b04c-c9a4-4524-aa7d-39196011b81a)"
>
注意到 UUID 字符串周围缺少引号了吗?当 Alpine 评估此表达式时,JavaScript 将抛出错误:"Uncaught SyntaxError: Invalid or unexpected token"。
要解决此问题,需要在 Blade 表达式周围添加引号,如下所示:
<button
type="button"
x-on:click="$wire.deletePost('{{ $post->uuid }}')"
>
现在,上面的模板将正确渲染,一切按预期工作:
<button
type="button"
x-on:click="$wire.deletePost('93c7b04c-c9a4-4524-aa7d-39196011b81a')"
>
刷新组件
你可以使用 $wire.$refresh() 轻松刷新 Livewire 组件(触发网络往返以重新渲染组件的 Blade 视图):
<button type="button" x-on:click="$wire.$refresh()">
使用 $wire.entangle 共享状态
在大多数情况下,$wire 已经足够满足从 Alpine 与 Livewire 状态交互的需求。不过,Livewire 还提供了一个 $wire.entangle() 工具,用于保持 Livewire 中的值与 Alpine 中的值同步。
为了演示,考虑这个下拉菜单示例,其 showDropdown 属性通过 $wire.entangle() 在 Livewire 和 Alpine 之间进行了绑定。通过使用绑定,我们现在可以从 Alpine 和 Livewire 两端控制下拉菜单的状态:
use Livewire\Component;
class PostDropdown extends Component
{
public $showDropdown = false;
public function archive()
{
// ...
$this->showDropdown = false;
}
public function delete()
{
// ...
$this->showDropdown = false;
}
}
<div x-data="{ open: $wire.entangle('showDropdown') }">
<button x-on:click="open = true">显示更多...</button>
<ul x-show="open" x-on:click.outside="open = false">
<li><button wire:click="archive">归档</button></li>
<li><button wire:click="delete">删除</button></li>
</ul>
</div>
用户现在可以立即通过 Alpine 切换下拉菜单,但当他们点击像"归档"这样的 Livewire 操作时,下拉菜单将被 Livewire 告知关闭。Alpine 和 Livewire 都可以操作各自的属性,另一端会自动更新。
默认情况下,状态更新会被延迟(在客户端更改,但不会立即发送到服务器),直到下一次 Livewire 请求。如果你需要在用户点击时立即更新服务器端状态,可以链式调用 .live 修饰符,如下所示:
<div x-data="{ open: $wire.entangle('showDropdown').live }">
...
</div>
:::tip 你可能不需要 $wire.entangle
在大多数情况下,你可以直接使用 $wire 从 Alpine 访问 Livewire 属性,而不需要对它们进行绑定。对两个属性进行绑定而不是只依赖一个属性,在处理频繁变化的深层嵌套对象时可能会导致可预测性和性能问题。因此,从版本 3 开始,Livewire 的文档中已淡化了对 $wire.entangle 的推荐。
:::
:::warning 避免使用 @@entangle 指令
在 Livewire 版本 2 中,推荐使用 Blade 的 @@entangle 指令。在 v3 中情况已不同。$wire.entangle() 是更好的选择,因为它更健壮,并且可以避免某些移除 DOM 元素时的问题。
:::
在 JavaScript 构建中手动打包 Alpine
默认情况下,Livewire 和 Alpine 的 JavaScript 会自动注入到每个 Livewire 页面中。
这对于简单的设置来说很理想,但你可能希望将自己的 Alpine 组件、stores 和插件包含到项目中。
通过自己的 JavaScript 打包在页面上包含 Livewire 和 Alpine 是很简单的。
首先,你需要在布局文件中包含 @livewireScriptConfig 指令,如下所示:
<html>
<head>
<!-- ... -->
@livewireStyles
@vite(['resources/js/app.js'])
</head>
<body>
{{ $slot }}
@livewireScriptConfig <!-- [tl! highlight] -->
</body>
</html>
这允许 Livewire 为你的 bundle 提供应用正常运行所需的配置。
现在,你可以在 resources/js/app.js 文件中导入 Livewire 和 Alpine,如下所示:
import { Livewire, Alpine } from '../../vendor/livewire/livewire/dist/livewire.esm';
// 在此注册任何 Alpine 指令、组件或插件...
Livewire.start()
以下是在应用中注册名为 "x-clipboard" 的自定义 Alpine 指令的示例:
import { Livewire, Alpine } from '../../vendor/livewire/livewire/dist/livewire.esm';
Alpine.directive('clipboard', (el) => {
let text = el.textContent
el.addEventListener('click', () => {
navigator.clipboard.writeText(text)
})
})
Livewire.start()
现在,x-clipboard 指令将可用于 Livewire 应用中的所有 Alpine 组件。